GC 即 Garbage Collection,中文 意思“垃圾回收”,在有GC之前,我们手动去管理内存,如果你忘记标记某一处已经不再使用的内存,那么这块内存将永远不会被系统回收,也就是常说的 “内存泄露”。
以下所有的 GC 介绍,全部基于主流 JVM 虚拟机 Hotspot。
GC判断一个对象存活或死亡就是判断这个对象还存不存在它的引用,常见的两种方式如下
每个对象从创建开始,都会维护一个引用计数器,每当引用一次,那么计数器增加1,引用失效一次,那么计数器减去1,这样实现优点是高效、简单,但是缺点也很明显:无法解决循环依赖,比如下面的代码,虽然 A引用B,B引用A,但是就没有其他地方引用了,因此它们是无效引用,造成内存泄露。Java 自然不会选择这种方式作为判断方式。
A=B B=A 复制代码
将一系列的 GC Roots 对象作为起点,开始向下搜索。可作为 GC Root 的起点有
Java 虚拟机栈(栈桢本地变量表中)引用的对象
本地方法栈中JNI(也就是常说的 Native 方法)
方法中的常量、类静态属性引用的对象
注意: 向下搜索的路径就是引用链
为了方便理解,我画了下面的图片
特别注意:可达性分析仅仅是判断对象是否可达,但还不足以判断对象是否存活或者死亡。可达性分析中判断为不可达的对象,只是被判刑 ≠ 死亡。
不可达对象会存放在 「即将回收」集合中,要判断一个对象是否真正的死亡,还需要经过下面的两个步骤。
可达性分析中标记为不可达的对象,会经历第一次筛选。
筛选标准:判断对象是否需要执行 finalize() 方法,若有必要执行,则筛选进行下一阶段分析,若没必要执行,那么该对象判定为死亡,不筛选,等待系统回收。
当对象无 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过一次的情况下,那么被标记为没必要执行,等待回收。
当对象经过了第一次筛选后没有被回收,将进行第二次筛选。
该对象会被放在一个 F-Queue
的队列中,并由虚拟机自动创建一个名为 Finalizer 的低优先级的线程去执行队列中所有对象的 finalize 方法,这里需要注意的是,finalize 方法只会被执行一次,并且 JVM 并不承诺 finalize 会执行完毕,原因是为了防止 finalize 执行时间过长或者停止执行,导致的内存溢出。
筛选标准: 在执行 finalize 方法的过程中,如果该对象依旧没有和 GC Root 关联起来,那么该对象被判断为死亡,留在即将回收集合,等待回收。
简单理解就是,全局GC(Full GC)和局部GC(Partial GC),分别看一下:
Young GC :只收集 Young Gen(年轻代)的 GC, Young GC 还有一个叫法 叫 Minor GC。
old GC : 只收集 Old Gen(年老代) 的GC 只有垃圾回收器 CMS 的 concament colletton 有这个模式。
mixed GC : 收集整个Young GC的GC和部分的old Gen的GC,只有垃圾回收器 G1 有这个模式。
Full GC是对整个堆来说的,执行Full GC 的时候会回收所有代,包括永久代、年老代、年轻代等等所有的 GC。Full GC 的触发条件有以下几种
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI(Java远程方法调用)调用System.gc。
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误: java.lang.OutOfMemoryError: Java heap space 为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息: java.lang.OutOfMemoryError: PermGen space 为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC
为了方便理解上述枯燥又无味的理论,我写了几行代码,实验了一下
/** * GCRoots 测试:虚拟机栈(栈帧中的局部变量)中引用的对象作为GCRoots * -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails * <p> * 扩展:虚拟机栈中存放了编译器可知的八种基本数据类型,对象引用,returnAddress类型(指向了一条字节码指令的地址) * * @author wangjie */ public class TestGCRoots01 { private int _10MB = 10 * 1024 * 1024; private byte[] memory = new byte[8 * _10MB]; public static void main(String[] args) { method01(); System.out.println("返回main方法"); System.gc(); System.out.println("第二次GC完成"); } public static void method01() { TestGCRoots01 t = new TestGCRoots01(); System.gc(); System.out.println("第一次GC完成"); } } 复制代码
当运行上述代码控制台会输出如下内容
首先该类声明了一个 80M 的数组
private byte[] memory = new byte[8 * _10MB]; 复制代码
然后调用了 method01,method01创建了一个TestGCRoots01的实例,该实例存放在 PSYoungGen 也就是年轻代
method01(); TestGCRoots01 t = new TestGCRoots01(); 复制代码
这时调用了 System.gc(),该实例从PSYoungGen到了ParOldGen也就是年老代
System.gc(); System.out.println("第一次GC完成"); 复制代码
但是并没有被回收,method01执行完毕后返回到了 main 方法,这时又执行了一次 System.gc(),该实例被回收 。
System.out.println("返回main方法"); System.gc(); System.out.println("第二次GC完成"); 复制代码
至此,GC 的基础知识你应该了解了,但是这篇文章仅仅简单分析了一下 GC 和 JVM 的关系,并不涉及到引用链,如果对你理解 GC 有帮助,点赞转发是对我最大的支持。
另外,我为你找到了以下资料,并翻译成了中文供你查阅