08-GC
关注的是黄色部分《内存的分配与回收》
- Java、CPP区别,就在于GC技术和内存动态分配上,C没有GC技术,需要手动的收集
- GC不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和GC技术的Lisp语言诞生
- 关于GC有三个经典问题
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
- GC机制是Java的招牌能力,极大地提高了开发效率。如今,GC几乎成为现代语言的标配,即使经过如此长时间的发展,Java的GC机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对GC提出了新的挑战,也是面试的热点
1. 什么是垃圾?
An object is considered garbage when it can no longer be reached from any pointer in the running program.
- 垃圾:运行程序中没有任何指针指向的Obj
- 如果不及时对内存中的垃圾进行清理,这些垃圾Obj所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其它Obj使用,甚至可能导致内存溢出
1. 磁盘碎片整理
机械硬盘需要进行磁盘整理,同时还有坏道
2. why_GC
- 对于高级语言来说,一个基本认知是如果不进行GC,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样
- 除了释放没用的Obj,GC也可以进行内存碎片整理。以便JVM将整理出的内存分配给新的Obj
- 随着应用程序所应付的业务越来越庞大、复杂,用户也越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化
2. 早期GC
在早期的C/C++时代,GC基本上是手工进行的。开发人员可以使用new关键字进行内存申请,并使用delete关键字进行内存释放
MibBridge *pBridge = new cmBaseGroupBridge();
// 如果注册失败,使用Delete释放该Obj所占内存区域
if (pBridge -> Register(kDestroy) != NO ERROR)
delete pBridge;
这种方式可以灵活控制内存释放时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的疏忽忘记被回收,那么就会产生内存泄漏,垃圾Obj永远无法被清除,随着系统运行时间的不断增长,垃圾Obj所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃
有了GC机制后:
MibBridge *pBridge = new cmBaseGroupBridge();
pBridge -> Register(kDestroy);
现在,除了Java以外,C#、Python、Ruby等语言都使用了自动GC的思想,也是未来发展趋势。可以说,这种自动化的内存分配和GC方式已经成为了现代开发语言必备的标准
3. Java_GC机制
- Minor GC:针对新生代的GC
- Major GC:针对老年代的GC,一般老年代触发GC的同时也会触发Minor GC,也就等于触发了Full GC
- Full GC:新生代 + 老年代同时发生GC
- 事实上Full GC本身不会先进行Minor GC,可以配置,让Full GC之前先进行一次Minor GC,因为老年代很多Obj都会引用到新生代的Obj,先进行一次Minor GC可以提高老年代GC速度
- eg:老年代使用CMS时,设置CMSScavengeBeforeRemark优化,让CMS remark之前先进行一次Minor GC
1. 优点
- 自动内存管理,无需开发人员手动参与内存分配与回收,更专注于业务开发,降低内存泄漏、内存溢出的风险
- 没有GCtor,java也会和CPP一样,各种悬垂指针,野指针,泄露问题让开发头疼不已
2. 缺点
- 对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,严重弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力
- 此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,才能够在遇见
OutOfMemoryError
时,快速地根据错误异常日志定位问题和解决问题 - 当需要排查各种内存溢出、内存泄漏问题时,当GC成为系统达到更高并发量的瓶颈时,就必须对这些“自动化”的技术实施必要的监控和调节
3. GC主要区域
方法区、堆
GCtor可以对年轻代回收,也可以对老年代回收,甚至是全栈、方法区的回收。其中,Heap是GCtor的工作重点区域
从次数上讲:
- 频繁收集Young区
- 较少收集Old区
- 基本不收集Perm区(元空间)
4. System.gc()
- 在默认情况下,通过
System.gc()
或Runtime.getRuntime().gc()
的调用,会显式触发FullGC,同时对老年代、新生代进行回收,尝试释放垃圾Obj占用的内存 - 然而
System.gc()
调用附带一个免责声明,无法保证对GCtor的调用(不能确保立即生效) - JVM实现者可以通过
System.gc()
来决定GC行为。而一般情况下,GC是自动进行的,无须手动触发- eg:在一些特殊情况下,正在编写一个性能基准,可以在运行时调用
System.gc()
- eg:在一些特殊情况下,正在编写一个性能基准,可以在运行时调用
public class SystemGCTest {
public static void main(String[] args) {
new SystemGCTest();
// 提醒jvm的GCtor执行gc,但是不确定是否马上执行
// 与`Runtime.getRuntime().gc();`作用一样
System.gc();
System.runFinalization(); // 强制调用Obj_finalize()方法
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("SystemGCTest.finalize()");
}
}
5. 手动GC
public class LocalVarGC {
/**
* 执行GC,引用未释放,survivor区放不下10m数据,直接放入old区
* 触发MinorGC,没有回收Obj。然后触发FullGC将该Obj存入Old区
*/
@Test
public void localGC1() {
byte[] buffer = new byte[10 * 1024 * 1024]; // 10MB
System.gc();
}
/**
* 执行GC,释放引用,回收该数组在堆区空间
* 触发YoungGC时,已经被回收了
*/
@Test
public void localGC2() {
byte[] buffer = new byte[10 * 1024 * 1024];
buffer = null;
System.gc();
}
/**
* 执行GC,不会被回收。虽然buffer声明在局部作用域中,但是局部变量表中还占着一个slot槽
*/
@Test
public void localGC3() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
System.gc();
}
/**
* 执行GC,buffer声明在局部作用域中;作用域外重新声明一个变量。这个变量就占用了原buffer位置,所以释放了引用,进行回收
* 会被回收。out把buffer的slot槽覆盖了
*/
@Test
public void localGC4() {
{
byte[] buffer = new byte[10 * 1024 * 1024];
}
int out = 10;
System.gc();
}
/**
* localGC1出栈,引用释放,数组已经被回收
*/
@Test
public void localGC5() {
localGC1();
System.gc();
}
}
[GC (System.gc()) [PSYoungGen: 44489K->13980K(153088K)] 44489K->13996K(502784K), 0.0117254 secs] [Times: user=0.07 sys=0.01, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 13980K->0K(153088K)] [ParOldGen: 16K->13690K(349696K)] 13996K->13690K(502784K), [Metaspace: 8481K->8481K(1056768K)], 0.0151797 secs] [Times: user=0.05 sys=0.02, real=0.01 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------
[GC (System.gc()) [PSYoungGen: 44490K->3798K(153088K)] 44490K->3814K(502784K), 0.0040090 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 3798K->0K(153088K)] [ParOldGen: 16K->3450K(349696K)] 3814K->3450K(502784K), [Metaspace: 8442K->8442K(1056768K)], 0.0103119 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------
[GC (System.gc()) [PSYoungGen: 44494K->13996K(153088K)] 44494K->14012K(502784K), 0.0108401 secs] [Times: user=0.07 sys=0.01, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 13996K->0K(153088K)] [ParOldGen: 16K->13690K(349696K)] 14012K->13690K(502784K), [Metaspace: 8479K->8479K(1056768K)], 0.0144075 secs] [Times: user=0.06 sys=0.02, real=0.02 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------
[GC (System.gc()) [PSYoungGen: 44490K->3820K(153088K)] 44490K->3836K(502784K), 0.0064538 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 3820K->0K(153088K)] [ParOldGen: 16K->3450K(349696K)] 3836K->3450K(502784K), [Metaspace: 8480K->8480K(1056768K)], 0.0134044 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
------------------------------------------------------------------------------------------------------------------------------------------------------
[GC (System.gc()) [PSYoungGen: 44490K->3783K(153088K)] 44490K->3799K(502784K), 0.0074064 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 3783K->0K(153088K)] [ParOldGen: 16K->3450K(349696K)] 3799K->3450K(502784K), [Metaspace: 8488K->8488K(1056768K)], 0.0177234 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
[GC (System.gc()) [PSYoungGen: 0K->0K(153088K)] 3450K->3450K(502784K), 0.0012005 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 0K->0K(153088K)] [ParOldGen: 3450K->3450K(349696K)] 3450K->3450K(502784K), [Metaspace: 8488K->8488K(1056768K)], 0.0160055 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
6. 内存溢出
OOM:OutOfMemoryError
- 内存溢出相对于内存泄漏来说,更容易被理解,是引发程序崩溃的罪魁祸首之一
- javadoc中对OOM的解释是,没有空闲内存,并且GCtor也无法提供更多内存
- GC一直在发展,一般情况下,除非应用程序占用内存增长速度非常快,造成GC已经跟不上内存消耗的速度,否则不太容易出现OOM
首先说没有空闲内存的情况,原因有二:
- JVM的堆内存设置不够
- 可能存在内存泄漏问题
- 可能堆的大小不合理。eg:要处理比较可观的数据量,但是没有显式指定Heap大小或指定数值偏小。通过参数
-Xms, -Xmx
来调整
- 代码中创建了大量大Obj,并且长时间不能被GC(存在被引用)
- 对于老版本的Oracle_JDK,因为永久代的大小是有限的,并且JVM对永久代GC(常量池回收、卸载不再需要的类型)非常不积极,所以当不断添加新类型时,永久代出现OOM也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:
java.lang.OutOfMemoryError:PermGen space
- 随着Metaspace的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,异常信息则变成了:
java.lang.OutofMemoryError:Metaspace
- 对于老版本的Oracle_JDK,因为永久代的大小是有限的,并且JVM对永久代GC(常量池回收、卸载不再需要的类型)非常不积极,所以当不断添加新类型时,永久代出现OOM也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:
- 这里面隐含着一层意思是,在抛出
OutOfMemoryError
之前,通常GCtor会被触发,尽其所能去清理出空间- eg:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的Obj等
- 在
java.nio.BIts.reserveMemory()
中,能清楚的看到,System.gc()
会被调用,以清理空间
- 不是在任何情况下GCtor都会被触发的
- eg:去分配一个超大Obj,类似一个超大数组超过堆的最大值,JVM可以判断出GC并不能解决这个问题,直接抛出OOM
7. 内存泄漏
- 严格意义:Obj不会再被程序用到,引用没有断开,GC不能回收
- 宽泛意义:实际情况一些不太好的实践(或疏忽)会导致Obj的生命周期变得很长,甚至导致OOM
尽管内存泄漏并不会立刻引起程序崩溃,但是程序中的可用内存会被逐步蚕食,直至耗尽,最终出现OOM异常,导致程序崩溃(注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小)eg:
- 单例模式
- 单例的生命周期和应用程序是一样长的,所以单例持有外部Obj的引用,这个外部Obj不能被回收,导致内存泄漏
- 一些提供close的资源未关闭导致内存泄漏
- 数据库连接
dataSourse.getConnection()
,网络连接(socket)和IO连接必须手动close,否则不能被回收
- 数据库连接
8. Stop_The_World
STW(stop-the-world)指GC发生过程中,产生应用程序停顿。整个应用程序线程都会被暂停,没有任何响应,有点像卡死
- 可达性分析算法中枚举根节点(GC_Roots)会导致STW
- 分析工作必须在一个能确保一致性的快照中进行。一致性指分析期间整个执行系统看起来像被冻结在某个时间点上
- 分析过程中Obj引用关系还在不断变化,则分析结果的准确性无法保证
- STW和采用哪款GCtor无关。哪怕是G1也不能完全避免STW发生,只能说GCtor越来越优秀,回收效率越来越高,尽可能地缩短STW
- STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉
- 开发中不要用
system.gc()
会导致STW的发生
9. 并行、并发
1. Concurrent
- 在OS中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行
- 并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行
2. Parallel
- 并行:当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行
- 决定并行的因素不是CPU的数量,而是CPU的核心数量,一个CPU多个核也可以并行
- 适合科学计算,后台处理等弱交互场景
3. 并发VS并行
- 并发:指的是多个事情,在同一时间段内同时发生了
- 并行:指的是多个事情,在同一时间点上同时发生了
- 并发的多个任务之间是互相抢占资源的。并行的多个任务之间是不互相抢占资源的
- 只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的
4. GC的并行、并发
- 串行(Serial)
- 相较于并行的概念,单线程执行
- 如果内存不够,则程序暂停,启动GCtor进行GC。回收完,再启动程序的线程
- 并发(Concurrent):指用户线程、GC线程并发执行。用户程序继续运行,GC线程运行于另一个CPU上
- eg:CMS、G1
- 并行(Parallel)
- 指多条GC线程并行工作,产生STW
- eg:ParNew、Parallel_Scavenge、Parallel_Old
10. 安全点、安全区域
1. Safe_Point
程序执行时并非在任意地方都能停顿下来进行GC,只有在特定的位置才能停顿下来
- Safe_Point的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。eg:方法调用、循环跳转和异常跳转等
如何检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断(Preemptive Suspension):(目前没有VM采用了)
- 首先中断所有线程。如果还有线程不在Safe_Point,就恢复线程,让线程跑到Safe_Point
- 主动式中断(Voluntary Suspension):
- 设置一个中断标志,各线程运行到Safe_Point时主动轮询这个标志,如果中断标志为真,则将线程中断挂起(有轮询的机制)
2. Safe_Region
- 背景
- Safe_Point机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safe_Point。但是,线程处于Sleep、Blocked,无法响应JVM的中断请求,“走”到Safe_Point,JVM也不可能等待线程被唤醒
- 安全区域是指在一段代码片段中,Obj的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。Safe_Region看做是被扩展了的Safe_Point
- 执行流程
- 当线程进入到Safe_Region,进行标识,这段时间内发生GC,JVM会忽略被标识的线程
- 当线程即将离开Safe_Region,检查是否已经完成GC,如果完成了,则继续运行;否则必须等待,直到收到可以安全离开Safe_Region的信号
11. Reference
描述这样一类Obj:当内存足够时,能保留在内存中;如果进行GC后内存还很紧张,则可以抛弃这些Obj
- 这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在
java.lang.ref
包中找到- Reference子类中只有FinalRef是包内可见的,其他3种引用类型均为
public
,可以在应用程序中直接使用
- Reference子类中只有FinalRef是包内可见的,其他3种引用类型均为
- 强引用(Strong_Reference):最传统的“引用”的定义,StrongRef关系还存在,GC就永远不会回收掉强引用的Obj
- 软引用(Soft_Reference):在OS发生内存溢出之前,将会把SoftRef_Obj进行第二次回收。如果GC后还没有足够的内存,才会抛出内存溢出异常
- 弱引用(Weak_Reference):WeakRef_Obj只能生存到下一次GC之前。GC即回收
- 虚引用(Phantom_Reference):PhantomRef完全不会对Obj生存时间构成影响,也无法通过PhantomRef来获得Obj的实例。唯一目的就是能在Obj被GC时收到一个系统通知
1. StrongRef
- 在Java程序中,最常见的默认引用类型(普通系统99%以上都是强引用)
- 强引用的Obj是可触及的,GC永远不会回收掉被引用的Obj
- 对于普通Obj,如果没有其他引用关系,只要超过了引用的作用域或者显式地将强引用赋值为null,该Obj即为垃圾
- 相对的,软、弱、虚引用的Obj是软可触及、弱可触及、虚可触及。在一定条件下,都是可以被回收的。强引用是造成Java内存泄漏的主要原因之一
// 局部变量str指向StringBuffer实例所在Heap,通过str可以操作该实例,那么str就是实例的强引用
StringBuffer str = new StringBuffer("hello mogublog");
StringBuffer str1 = str;
// 将`str = null;`则Heap中的Obj也不会被回收,因为还有其它引用指向StringBuffer实例
StrongRef特点:
- 可以直接访问目标Obj
- 所指向的Obj在任何时候都不会被GC,JVM宁愿抛出OOM
- 可能导致内存泄漏
2. SoftRef
一句话概括:内存不够时,才回收
- 用来描述一些还有用,但非必需的Obj(内存溢出前不可达的Obj)。只被SoftRef关联着的Obj,在系统将要发生内存溢出异常前,会对SoftRef_Obj进行第二次回收,如果回收后还没有足够的内存,才会抛出内存溢出异常
- 清理软引用,可选地把引用存放到一个引用队列(Reference_Queue)
- 通常用来实现内存敏感的缓存
- eg:高速缓存就用到软引用。如果还有空闲内存,就可以暂时保留缓存;当内存不足时,则清理掉。这样就保证了使用缓存的同时,不会耗尽内存
JDK1.2之后,提供了SoftReference类来实现软引用
// 声明强引用
Object obj = new Object();
// 创建SoftRef
SoftReference<Object> sf = new SoftReference<>(obj);
obj = null; // 销毁强引用,不然会存在强引用和软引用
3. WeakRef
GC即回收
- 描述非必需Obj,WeakRef_Obj只能生存到下一次GC发生为止
- WeakRef、SoftRef,在构造引用时,也可以指定一个引用队列。这个队列可以跟踪Obj的回收情况
- WeakRef、SoftRef都非常适合来保存那些可有可无的缓存数据
JDK1.2之后,提供了WeakReference类来实现弱引用
// 声明强引用
Object obj = new Object();
// 创建WeakRef
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; // 销毁强引用,不然会存在强引用和弱引用
4. PhantomRef
也称为“幽灵引用”、“幻影引用”,是所有引用类型中最弱的一个
- PhantomRef不会决定Obj的生命周期。随时可能被GC掉
- 不能单独使用,PhantomRef无法获取Obj,
get()
取Obj时,总是null - 存在唯一目的:跟踪GC过程(PhantomRef_Obj被GC时,收到一个系统通知)
- 在创建时必须提供一个Reference_Queue作为参数。当GC一个Obj时,将PhantomRef加入Queue,通知应用程序该Obj的回收情况
- 由于PhantomRef可以跟踪Obj的回收,因此,可以将一些资源释放操作放置在PhantomRef中执行和记录
JDK1.2之后,提供了PhantomReference类来实现虚引用
// 强引用
Object obj = new Object();
// 引用队列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 虚引用(传入引用队列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;
结合虚引用,引用队列,finalize()
进行讲解
/**
* 虚引用的测试
*/
public class PhantomReferenceTest {
public static PhantomReferenceTest obj; // 当前类Obj的声明
static ReferenceQueue<PhantomReferenceTest> phantomQueue = null; // 引用队列
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<PhantomReferenceTest> objt = null;
try {
objt = (PhantomReference<PhantomReferenceTest>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (objt != null) {
System.out.println("追踪GC过程:PhantomReferenceTest实例被GC了");
}
}
}
}
}
// finalize()只能被调用一次!
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类的finalize()方法");
obj = this; // 复活Obj
}
public static void main(String[] args) throws InterruptedException {
Thread t = new CheckRefQueue();
t.setDaemon(true); // 设置为守护线程:当程序中没有被守护线程时,守护线程也就执行结束
t.start();
phantomQueue = new ReferenceQueue<>();
obj = new PhantomReferenceTest();
// 构造了PhantomReferenceTestObj的虚引用,并指定了引用队列
PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<>(obj, phantomQueue);
// 不可获取phantomRef_Obj
System.out.println("phantomRef.get() = " + phantomRef.get());
System.out.println("=====>>>>> 第 1 次 gc");
obj = null;
// 第一次进行GC,由于Obj可复活,GC无法回收该Obj
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
}
System.out.println("=====>>>>> 第 2 次 gc");
obj = null;
System.gc(); // 一旦将Obj回收,就会将此phantomRef存放到引用队列中
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
}
}
}
phantomRef.get() = null
=====>>>>> 第 1 次 gc
调用当前类的finalize()方法
obj可用
=====>>>>> 第 2 次 gc
追踪垃圾回收过程:PhantomReferenceTest实例被GC了
obj是null
5. FinalRef
- 用于实现Obj的
finalize()
,也可以称为终结器引用 - 无需手动编码,其内部配合引用队列使用
- 在GC时,终结器引用入队。Finalizer线程通过终结器引用找到被引用Obj,调用其
finalize()