JVM内存回收策略&GC算法对比

1、如何检测垃圾

垃圾收集器必须要完成两件事情:一个是能够正确的检测出垃圾对象,另一个是能够释放垃圾对象占用的内存空间。

只要某个对象不再被其他活动对象(指的是能够被一个根对象集合到达的对象)引用,那么就可以回收(根节点可达性分析)。

 

根对象集合中又都是些什么对象?

方法中局部变量区中对象的引用:如在前面的staticData方法中定义的lg和o等对象的引用就是根对象集合中的一个根对象,这些根对象直接存储在栈帧中的局部变量区中。

Java操作栈中的对象引用:有些对象是直接在操作栈中持有的,所以操作栈肯定也包含根对象集合。

常量池中的对象引用:每个类都会包含一个常量池,这些常用池中也会包含很多对象引用,如表示类名的字符串就保存在堆中,那么常量池中只会持有这个字符串对象的引用。

本地方法中持有的对象引用:有些对象被传入本地方法中,但是这些对象还没有被释放。

类的Class对象:当每个类被JVM加载时都会创建一个代表这个类的唯一数据类型的Class对象,而这个Class对象也同样存放在堆中,当这个类不再被使用时,在方法区中类数据和这个Class对象同样需要被回收。

 

JVM在做垃圾回收时会检查堆中的所有对象是否都会被这些根对象直接或者间接引用,能够被引用的对象就是活动对象,否则就可以被垃圾收集器回收。

 

2、基于分代的垃圾收集算法

 

(1)、Hotspot中使用的基于分代的垃圾收集方式

算法的设计思路:把对象按照寿命长短来分组,分为年轻代和年老代,新创建的对象被分在年轻代,如果对象经过几次回收后仍然存活,那么再把这个对象划分到年老代。年老代的收集频度不像年轻代那么频繁,这样就减少了每次垃圾收集所要扫描的对象的数量,从而提高了垃圾回收频率。

 

基于分代的堆结构:

Young区  ----(Eden)-------|----(From)-----|----(To)------  [Survivor区]

Old区    -------------------------

Perm区   ---------------

 

JVM将整个堆划分为Young区、Old区和Perm区,分别存放不同年龄的对象,这三个区存放的对象有如下区别:

◎ Young区又分为Eden区和两个Survivor区,其中所有新创建的对象都在Eden区,当Eden区满后会触发minor GC将Eden区仍然存活的对象复制到其中一个Survivor区中,另外一个Survivor区中的存活对象也复制到这个Survivor中,以保证始终有一个Survivor区是空的。

◎ Old区存放的是Young区的Survivor满后触发minor GC后仍然存活的对象,当Eden区满后会将对象存放到Survivor区中,如果Survivor区仍然存不下这些对象,GC收集器会将这些对象直接存放到Old区。如果Survivor区中的对象足够老,也直接存放到Old区。如果Old区也满了,将会触发Full GC回收整个堆内存。

◎ Perm区存放的主要是类的Class对象,如果一个类被频繁地加载,也可能会导致Perm区满,Perm区的垃圾回收也是由Full GC触发的。

 

(2)、三类垃圾收集算法

◎ Serial Collector

◎ Parallel Collector

◎ CMS Collector

 

1、Serial Collector

Serial Collector是JVM在client模式下默认的GC方式。可以通过JVM配置参数-XX:+UseSerialGC来指定GC使用该收集算法。我们指定所有的对象都在Young区的Eden中创建,但是如果创建的对象超过Eden区的总大小,或者超过了PretenureSizeThreshold配置参数配置的大小,就只能在Old区分配了,如-XX:PretenureSizeThreshold= 30720在实际使用中很少发生。

当Eden空间不足时就触发Minor GC,触发Minor GC时首先会检查之前每次Minor GC时晋升到Old区的平均对象大小是否大于Old区的剩余空间,如果大于,则将直接触发Full GC,如果小于,则要看HandlePromotionFailure参数(-XX:-HandlePromotionFailure)的值。如果为true,仅触发Minor GC,否则再触发一次Full GC。其实这个规则很好理解,如果每次晋升的对象大小都超过了Old区的剩余空间,那么说明当前的Old区的空间已经不能满足新对象所占空间的大小,只有触发Full GC才能获得更多的内存空间。

 

2、Parallel Collector

Parallel GC根据Minor GC和Full GC的不同分为三种,分别是ParNewGC、ParallelGC和ParallelOldGC。

1)ParNewGC

可以通过-XX:+UseParNewGC参数来指定,它的对象分配和回收策略与Serial Collector类似,只是回收的线程不是单线程的,而是多线程并行回收。Parallel Collector中还有一个UseAdaptiveSizePolicy配置参数,这个参数是用来动态控制Eden、From Space和To Space的TenuringThreshold大小的,以便控制哪些对象经过多少次回收后可以直接放入Old区。

2)ParallelGC

Server下默认的GC方式,可以通过-XX:+UseParallelGC参数来强制指定,并行回收的线程数可以通过-XX:ParallelGCThreads来指定,这个值有个计算公式,如果CPU和核数小于8,线程数可以和核数一样,如果大于8,值为3+(cpu core*5)/8。

可以通过-Xmn来控制Young区的大小,如-Xmn10m,即设置Young区的大小为10MB。Young区内的Eden、From Space和To Space的大小控制可以通过SurvivorRatio参数来完成,如设置成-XX:SurvivorRatio=8,表示Eden区与From Space的大小为8:1,如果Young区的总大小为10 MB,那么Eden、s0和s1的大小分别为8 MB、1 MB和1 MB。但在默认情况下以-XX:InitialSurivivorRatio设置的为准,这个值默认也为8,表示的是Young:s0为8:1。

当在Eden区中申请内存空间时,如果Eden区不够,那么看当前申请的空间是否大于等于Eden的一半,如果大于则这次申请的空间直接在Old中分配,如果小于则触发Minor GC。在触发GC之前首先会检查每次晋升到Old区的平均大小是否大于Old区的剩余空间,如大于则再触发Full GC。在这次触发GC后仍然会按照这个规则重新检查一次。也就是如果满足上面这个规则,Full GC会执行两次。

在Young区的对象经过多次GC后有可能仍然存活,那么它们晋升到Old区的规则可以通过如下这些参数来控制:AlwaysTenure,默认为false,表示只要Minor GC时存活就晋升到old;NeverTenure,默认为false,表示永远晋升到old区。如果在上面两个都没设置的情况下设置UseAdaptiveSizePolicy,启动时以InitialTenuringThreshold值作为存活次数的阈值,在每次GC后会动态调整,如果不想使用UseAdaptiveSizePolicy,则以MaxTenuringThreshold为准,不使用UseAdaptiveSizePolicy可以设置为 -XX:-UseAdaptiveSizePolicy。如果Minor GC时To Space不够,对象也将会直接放到Old区。

当Old或者Perm区空间不足时会触发Full GC,如配置了参数ScavengeBeforeFullGC,在Full GC之前会先触发Minor GC。

3)ParallelOldGC

可以通过-XX:+UseParallelOldGC参数来强制指定,并行回收的线程数可以通过-XX:ParallelGCThreads来指定,这个数字的值有个计算公式,如果CPU和核数小于8,线程数可以和核数一样,如果大于8,值为3+(cpu core*5)/8。

它与ParallelGC有何不同呢?其实不同之处在Full GC上,前者Full GC进行的动作为清空整个Heap堆中的垃圾对象,清除Perm区中已经被卸载的类信息,并进行压缩。而后者是清除Heap堆中的部分垃圾对象,并进行部分的空间压缩。

GC垃圾回收都是以多线程方式进行的,同样也将暂停所有应用程序。

 

3、CMS Collector

可通过-XX:+UseConcMarkSweepGC来指定,并发的线程数默认为4(并行GC线程数+3),也可通过ParallelCMSThreads指定。

CMS GC与上面讨论的GC不太一样,它既不是上面所说的Minor GC,也不是Full GC,它是基于这两种GC之间的一种GC。它的触发规则是检查Old区或者Perm区的使用率,当达到一定比例时就会触发CMS GC,触发时会回收Old区中的内存空间。这个比例可以通过CMSInitiatingOccupancyFraction参数来指定,默认是92%,这个默认值是通过((100−MinHeapFreeRatio)+(double)(CMSTriggerRatio*MinHeapFreeRatio)/100.0)/100.0计算出来的,其中的MinHeapFreeRatio为40、CMSTriggerRatio为80。如果让Perm区也使用CMS GC可以通过-XX:+CMSClassUnloadingEnabled来设定,Perm区的比例默认值也是92%,这个值可以通过CMSInitiatingPermOccupancyFraction设定。这个默认值也是通过一个公式计算出来的:((100−MinHeapFreeRatio)+(double)(CMSTriggerPermRatio*MinHeapFreeRatio)/100.0)/100.0,其中MinHeapFreeRatio为40,CMSTriggerPermRatio为80。

触发CMS GC时回收的只是Old区或者Perm区的垃圾对象,在回收时和前面所说的Minor GC和Full GC基本没有关系。

这个模式下的Minor GC触发规则和回收规则与Serial Collector基本一致,不同之处只是GC回收的线程是多线程而已。

触发Full GC是在这两种情况下发生的:一种是Eden分配失败,Minor GC后分配到To Space,To Space不够再分配到Old区,Old区不够则触发Full GC;另外一种情况是,当CMS GC正在进行时向Old申请内存失败则会直接触发Full GC。

这里还需要特别提醒一下,在Hotspot 1.6中使用这种GC方式时在程序中显式地调用了System.gc,且设置了ExplicitGCInvokesConcurrent参数,那么使用NIO时可能会引发内存泄漏,这个内存泄漏将在后面介绍。

CMS GC何时执行JVM还会有一些时机选择,如当前的CPU是否繁忙等因素,因此它会有一个计算规则,并根据这个规则来动态调整。但是这也会给JVM带来另外的开销,如果要去掉这个动态调整功能,禁止JVM自行触发CMS GC,可以通过配置参数-XX:+UseCMSInitiatingOccupancyOnly来实现。

 

三种GC优缺点对比:(优秀的硬件建议使用CMS)

三种GC优缺点对比
GC 优点 缺点
Serial Collector (串行) 适合内存有限的情况 回收慢
Parallel Collector (并行) 效率高 当Heap过大时,应用程序暂停时间较长
CMS Collector (并发) Old区回收暂停时间短 产生内存碎片、整个GC耗时较长、比较耗CPU

 

 

JVM参数:

https://www.cnblogs.com/carl10086/p/6082245.html

参考书籍:

《深入分析Java Web技术内幕》

猜你喜欢

转载自blog.csdn.net/zangdaiyang1991/article/details/84942988