JAVA GC垃圾收集器的分析
本篇文章主要介绍了"JAVA GC垃圾收集器的分析",主要涉及到JAVA GC垃圾收集器的分析方面的内容,对于JAVA GC垃圾收集器的分析感兴趣的同学可以参考一下。
在 很多人看来,java中内存的动态分配与内存回收已经不用用户担心了,因为它给我们提供了GC自动回收 ,感觉一切都进入了自动化了,但是对于各种内存溢出,内存泄漏问题的出现,我们还是很有必要学习GC的。地球人都知道,Java有个东西叫垃圾收集器,它 让创建的对象不需要像C/C++那样delete、free掉,但你能不能谈谈,GC是在什么时候,对什么东西,做了什么事情?” 如果还不是很了解那我们一起来学习吧。有目的地去学习GC你就不会觉得枯燥,从头到尾,带着“When” "What" "How" 这个疑问去学习,你的思路会很清晰。
一:要想知道GC到底在什么时候开始工作,必须要先了解垃圾回收器是作用在哪一个区域,采用什么样的内存回收的方式,这种方式设计的GC中采用哪些算法,因此这些问题都要先一一详细说明。
1、 先来说GC工作在哪块区域呢?根据JVM运行的内存数据区域下画分,如下图。其实GC是工作在方法区和堆区。程序计数器,虚拟机栈(也就平时所说的栈), 本地方法栈这三区域随着线程而生,随着线程而灭,出栈入栈的操作,在栈中分配配的多少内存都具有确定性,在这几个区域就不用考虑回收问题了,因为方法结束 或线程结束,内存自然就回收了。而堆区和方法区都是线程共享,堆区主要存放对象实例及数组对象,方法区存储已加载的类信息(每个类都有唯一个 Class<?>类对应着)、常量、静态变量等,所以只有在程序运行的时候我们才能知道要创建哪些对象,这部分内存的分配和回收都是动态的, 所以这是就GC关注的区域也是它工作的区域。
2、 就拿现在为java已经实现的JVM中的GC是采用哪种内存回收方式呢?实际有很多种内存回收方式,实现垃圾回收时总会综合使用多种设计方式, 会针对不同的情况采用不同的垃圾回收实现。不同方式设计基础也就是基于不同的算法,垃圾收集又有哪些的算法呢?下面分别说明:
(1)复制算法:将可用内存容量划分为大小相等的两块,每次只使用其中的一块,当这块内存用完了,就要将活着的对象(从根(GC Roots)开始访问每一个关联可到达的对象)复制到另外一块空间上,然后再把已使用的内存空间一次清理掉。如下图:
(2)标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有地被标记的对象。如下图:
(3)标记-整理算法:和标记-清除算差不多,首先标记出所有需要回收的对象,先不直接回收不可达对象,而是让所有地存活的对象都向一端移动 然后再清理掉边界以外的内存空间,如下图:
(4)分代收集算法:这个很简单,根据对象的存活周期的不同将内存划分为几块。
3、现行的垃圾回收器用分代的方式来采用不同的回收设计。根据对象生存时间的长短,把堆内存分 成3个代。Young(年轻代),Old(老年代),Permanent(永久代)。
Young:主要是用来存放新生的对象。
Old:主要存放应用程序中生命周期长的内存对象。
Permanent:是指内存的永久保存区域,.它和存放Instance的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理。
其实在JVM中对于垃圾回收时不论采用哪种算法机制来设计垃圾回收器,都会利弊参半,因此实际上实现垃圾回收时总会综合使用多种设计方式也就是基于多种算法的一个综合应用。java现行JVM中基于不同代的特点之上采用不同的回收算法,从而充分利用种回收算法的优点。
首先提前了解下(Minor GC),(Major GC / Full GC)
GC(Minor GC):指发生在新生代的垃圾收集动作,它不会导致老年代里进行垃圾收集动作发生,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10 倍以上。
再看上图:划分了不同代后,由于GC主要是发生在对象经常消亡的新生代,如有IBM 的专门研究表明,新生代中的对象98%是朝生夕死的,复制算法实现简单,运行高效,对于复制量不是很大的情况下使用比较合理。所以现在商业虚拟机都采用复 制算法来回收新生代。再看新生代内划分为三个小块。因为新对象经过一次GC后存活下来的就会很少了,不需要按照1:1的比例来划分内存空间,而是设计成将 内存分为一块较大的Eden空间各两块较小的Survivor空间(这两个Survivor空间就是Survivor space:From Survivor或To Survivor。这两个Survivor空间是一样大小的。例如,新生代大小是10M(Xmn10M),那么缺省情况下 (-XX:SurvivorRatio=8),Eden Space 是8M,From和To都是1M。),每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地 拷贝到另一块Survivor空间上(如果此时Survivor空间不足怎么办,请看下面),最后清理掉Eden各刚才用过的Survivor的空间。
当 new一个对象时,先在Eden Space上分配,如果Eden Space没有足够的内存空间了,此时就要做一次Minor GC。Minor GC后,要把Eden和From中仍然活着的对象们复制到To空间中去。一般,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
说到这可以说明(如果此时Survivor空间不足怎么办) 这个问题了,不用担心在发生 Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,改为直接进行一次Full GC ,如果小于,说明老年代空间还有足够的空间,则要查看设置参数HandlePromotionFailure是否为true,如果是那只会完成这一次 Minor GC,GC后的新生代中活下来并符合老年代条件的对象就被promote到老年代空间,还有To Survivor空间不能容纳下Minor GC后活着的某个对象也会被promote到老年代空间,还有另一种情况就是如果新进来的一个大对象,Eden也没有足够空间存放的时也会直接把它 promote到老年代空间 ;如果false 则也要改为直接进行一次Fulll GC。对于参数HandlePromotionFailure它是指在Survivor轮换备份的过程中,Survivor无法容纳的对象是否直接进入老 年代。谈到怎么进入老年代,顺便说下另一种情况,并不是所有要进入老年代区域的对象年龄必须达到:MaxTenuringThreshold设定的值的, 如果Survivor空间中相同age(例如,age=5)对象的所占内存空间总和大于等于Survivor空间的一半,那么age>=5的对象在 下一次Minor GC后就可以直接promote到老年代,而不用等到age增长到阈值。
说到这里,再在已经能对GC到底在什么时候开始工作这个问题做个总结了:当在new 对象时,首先考虑在Eden Space上分配内存,如果Eden满了则 进行minor gc,晋升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;
二,对什么东西进行GC呢?
想知道对什么东西,则就要了解java是如何去判定一个对象是否还存活的。这里它的实现要用到根搜索算法(GC Roots Tracing) 基本意思就是通过一系列名为“ GC Roots”的对象作为起点,从这些节点向下开始进行搜索,搜索所经过的路径称为引用链,当一个对象到GC Roots 没有任何引用链时相连(说白了就是从GC Roots到这个对象不可达时),则证明此对象是不可用的。
下图中,对象Object6、Object7、Object8虽然互相引用,但他们的GC Roots是不可到达的,所以它们将会被判定为是可回收的对象。
可作为GC Roots 的对象包括:
1、虚拟机栈(栈帧中的本地变量表)中的引用对象。
2、 方法区中的类静态属性引用的对象
3、 方法区中的常量引用的对象
4、 本地方法栈中JNI的引用对象。
在根搜索算法中不可到达的对象,也并非 是“真正的死”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它将被第一次标记,并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法、当对象没有重写 finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行 finalize()”,finalize()方法是对象逃脱死亡命运的最后一次机会了,如果finalize()方法中没有重新与引用链上的任何一个对 象建立关联,说明这个对象不可能复活了,则进行第二次标记,则这个对象真正要被GC清理掉了。
分析到这里,对于什么东西这个问题也能回答了:从root搜索不可到达的对象,而且经过第一次标记、清理后,仍然没有复活的对象。
四.做了什么事情,就剩下这个问题了,回答这个问题的空间非常大。
也非常有必要了解不同厂商,不同版本的虚拟机提供的垃圾收集器的实现,了解串行回收器,并行回收器,并行整理回收器,并发标志-清理(CMS)回收器,它们的具体实现,其实这些回收器的实现也是基于前面说过那几种收集算法。
可以去查看相关的书集,如《深入理解JVM》,这里不一一分析了。
所 以问及GC做了什么事情,答的空间很大,可以说清楚新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势,串行、并行(整理/不整理碎片)、 CMS等搜集器可作用的年代、特点、优劣势等。