目录
什么是新生代、老年代、对象的创建到消亡在内存中是如何演绎的?
2、吞吐量优先垃圾回收器,Parallel Scavenge收集器
如何判断对象可以回收?
1、引用计数法(python使用)
概念:只要一个技术被其他变量锁引用,那么就让这个对象的引用计数+1,如果被引用了两次,那么引用计数再+1,如果某一个变量不在引用它,那么计数-1,当这个对象引用计数变为0的时候,那么就意味着没有变量在引用它了,那么这时候就变成一个垃圾被回收。
缺点:不能解决循环引用问题,如果两个对象互相引用,两个对象的引用计数都是1,导致无法被回收
2、可达性算法分析(java使用)
概念:首先要确定一系列根对象(肯定不能当成垃圾回收的对象),在垃圾回收之前会对堆内存中的对象进行一边扫描,然后判断哪个对象是被根对象直接或者间接的引用,那么就不能被回收,反之,可以作为垃圾被回收。
五种引用
1、强引用
当有强引用对对象引用时,full gc也无法回收对象
2、软引用
当有软引用对对象引用,无其他强引用时,此时内存紧张并且发生新生代 gc,当新生代gc结束之后发现内存还是紧张,那么该对象就会被回收,也就是第二次回收才会考虑软引用
使用场景:
在某些时候加载一些很大但是不是主要的资源,强引用可能会造成内存堆内存溢出的问题,在这里先做一个测试(堆内存调整为20m -Xmx20m)
List<byte[]> list = new ArrayList<>();
for (int i =0;i<5;i++){
byte[] bytes = new byte[1024*4*1024];
list.add(bytes);
}
}
很容易就出现了堆内存溢出的问题,由于需要加载的资源不是很重要,这时候可以采用软引用进行加载,如果内存紧张的时候第二次gc就会清除之前的软引用对象
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i =0;i<5;i++){
//采用软引用
SoftReference<byte[]> bytes = new SoftReference<>(new byte[1024*4*1024]);
System.out.println(i+" "+bytes.get());
list.add(bytes);
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
根据上面结果可以看到并没有出现内存溢出现象,但是由于内存紧张,只存入了1个不必要对象,后期如果有需要在重新加载就好了
存在问题:
上图中也看,当软引用的对象被回收,而软引用本身还存在,为null,虽然占用的内存不是很多,但是最好也要做到当软引用的对象被回收时,自身也跟着一起被回收,这时候就要配合引用队列使用
List<SoftReference<byte[]>> list = new ArrayList<>();
ReferenceQueue<byte[]> queue = new ReferenceQueue();
for (int i =0;i<5;i++){
//将引用队列和软引用绑定后,如果软引用的引用对象被回收,就会将软引用加入引用队列中
SoftReference<byte[]> bytes = new SoftReference<>(new byte[1024*4*1024],queue);
System.out.println(i+" "+bytes.get());
list.add(bytes);
}
Reference<? extends byte[]> poll = queue.poll();
while (poll!=null){ //当poll不为空时,表示有一个没有引用对象的软引用在队列中,需要删除掉
list.remove(poll);
poll=queue.poll();
}
for (SoftReference<byte[]> softReference : list) {
System.out.println(softReference.get());
}
}
可以看到,此时list中只有一个软引用。
3、弱引用
当有弱引用对对象引用,无其他引用时,只要发生了full gc,不论什么条件都会被回收
值得说明的是,当引用的对象被回收后,软弱引用都会进入引用队列进行扫描查看是否有强引用正在使用着软弱引用对象,如果没有就把软弱引用本身也回收掉释放内存
使用场景:
使用场景基本与软引用相同,但是当发生full gc 的时候 所有的弱引用对象都会被回收掉
WeakReference<byte[]> bytes = new WeakReference<>(new byte[1024*4*1024]);
4、虚引用
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动,例如ByteBuffer直接内存的删除是由指向ByteBuffer的虚引用Cleaner调用unsafe对象的方法回收直接内存的,但是虚引用cleaner必须要在引用队列中才能被使用
主要配合ByteBuffer使用,当被引用对象回收时,会将虚引用引入引用队列中,由Reference Handler线程定时调用虚引用的相关方法unsafe.freeMemory释放直接内存
5、终结器引用finalize()
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
1. 第一次标记并进行一次筛选。
筛选的条件是此对象是否有必要执行finalize()方法。
当对象没有覆盖finalize方法,对象将直接被回收。
2. 第二次标记
如果这个对象覆盖了finalize方法,finalize方法是对象脱逃死亡命运的最后一次机会,如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
注意:一个对象的finalize()方法只会被执行一次,也就是说通过调用finalize方法自我救命的机会就一次。
垃圾回收算法
1、标记清除算法
标记阶段:根据根对象扫描整个堆,查看其他对象是否被跟对象直接引用,如果确实被引用了那么就标记保留下来,如果没有被引用,那就被当作垃圾等待回收
清除阶段:整个清除阶段并不是把每个字节都进行一个清零操作,而是把对象占用内存的起始和结束的地址记录下来,放在一个叫做空闲的地址列表中,下次在给对象分配内存的时候,就去空闲地址列表中去找看下是否有足够的空间容纳新对象,如果有就进行内存分配。
优缺点:
优点:速度快 ,因为它只对空闲内存的起始结束地址做一个记录就完成了
缺点:容易产生内存碎片,因为清除操作并不会真正清空内存,只是做了一个地址记录,于是就存在大大小小各种空闲内存,而又的时候传入一个数组对象,需要连续的一块内存,这时候单个空闲内存无法满足内存大小,但是所有的空闲内存加起来却可以满足。空闲内存总和足够,但是因为内存空间不连续造成新对象无法分配。
2、标记整理算法
标记阶段:根据根对象扫描整个堆,查看其他对象是否被跟对象直接引用,如果确实被引用了那么就保留下来,如果没有被引用,则视作为垃圾对象
整理阶段:在清除垃圾之前会将可用的内存空间向前移动,让它更为紧凑,而后面连续的空间就更多一点,就避免了标记清除的内存碎片问题
优缺点:
优点:没有内存碎片
缺点:速度较慢,涉及到内存区块的移动和拷贝,内存地址的改变整理等
3、复制算法
会使用空间,第一块是正在使用的内存空间,假设名为from ,另一块是空闲的内存空间,假设名为to,先在from中通过跟对象扫描找到所有直接引用的对象,然后整理复制到to中的一块连续的内存空间上,from内存空间中剩下的就都是需要回收的对象,直接全部回收,然后再把from和to进行对换,以此循环。两块内存
先标记出直接引用对象
将跟对象直接引用的对象整理到to区,from区剩余的都是垃圾
回收from区的垃圾对象
交换from区和to,保持from区总是占用的,to区总是空闲的,方便下一次复制
优缺点:
优点:没有内存碎片
缺点:占用双倍内存空间
什么是新生代、老年代、对象的创建到消亡在内存中是如何演绎的?
在java中有的对象需要长时间使用,有的对象使用周期较短。对于需要长时间使用的,我们把他放在老年代中,对于使用周期较短的我们可以放到新生代中。新生代中包含三个部分:伊甸园、幸存区from、幸存区to
新创建的对象都分配到伊甸园当中,当伊甸园的内存空间逐满了的时候,就会触发一次新生代的垃圾回收,也叫做Minor GC。所用的回收算法是标记复制算法,将伊甸园和幸存区from中存活的对象放入幸存区to区域中,成为垃圾的对象就会进行回收,进入幸存区to区域的对象都会加上一个标记,生代年龄+1(如果生代年龄达到了阈值(最大15,用于存放年龄的有4位,当都为最大时是1111,转化为10进制就是15),就会把对象放入老年代中),当作完上面的操作后,就会将幸存区to和幸存区from的区域位置对换,而此时伊甸园是空的,幸存区to也是空的,幸存区from存放的存活的对象。
当伊甸园区已经满了,而这时候幸存区from也满了,这时候会尝试直接放入老年代中,如果老年代也满了就会开始触发一次full GC,将新生代、老年代都进行一次大回收。
特别说明:
当触发minor GC的时候,因为会涉及到对象地址的变动,为了防止在变动的过程中其他线程访问对象造成不必要的错误,会引发一个stop the world,暂停其他线程的活动,只允许垃圾回收线程工作,等垃圾回收结束,用户线程财恢复运行。由于新生代中大部分都是垃圾,所以涉及到整理和复制的对象都不是很多,整个过程时间非常短。
当大对象入堆的时候,如果已经超过了伊甸园的内存大小,那么这个对象就会不经过复制到幸存区而直接进入老年代中
垃圾回收相关参数
- 堆初始大小 -Xms 当内存占用达到-Xms会进行gc并扩容,一般设置-Xms=-Xmx
- 堆最大大小 -Xmx
- 新生代大小 -Xmn
- 幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
- 幸存区比例 -XX:SurvivorRatio=ratio
- 晋升阈值 -XX:MaxTenuringThreshold=threshold
- 晋升详情 -XX:+PrintTenuringDistribution
- GC详情 -XX:+PrintGCDetails -verbose:gc
- FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC
垃圾回收器
1、串行垃圾回收器 , Serial收集器
特点:单线程、适用于堆内存较小的个人电脑环境
开启指令:新生代采用-XX:+UseSerialGC开启,老年代采用-XX:+UseSerialOldGC开启,新生代中使用Serial回收器,采用标记+复制算法。老年代中使用Serial Old 回收器采用标记+整理算法
过程:多个线程工作时,所有线程都会找到一个安全点,然后暂停工作(stw),这时候只有一条垃圾回收线程在运行,当垃圾回收线程完成之后,其他线程才开始工作
2、吞吐量优先垃圾回收器,Parallel Scavenge收集器
特点:多线程、多个垃圾回收线程并行执行,适用于堆内存较大的多核cpu环境,让单位时间内STW的时间最短,例如STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高。Parallel收集器其实就是Serial收集器的多线程版本
开启指令:新生代采用-XX:+UseParallelGC 开启,老年代采用XX:+UseParallelOldGC,新生代中采用标记+复制算法,老年代中采用标记+整理算法
相关指令:
-XX:+UseAdptiveSizePolicy //GC自适应调节策略
要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)
-XX:GCTimeRatio=ratio // 吞吐量占 1/(1+radio)
-XX:MaxGCPauseMillis=ms // 最大暂停毫秒数,默认值200ms
-XX:ParallelGCThreads=n //设置用于垃圾回收的线程数
过程:多个线程工作时,所有线程都会找到一个安全点,然后暂停工作,多条垃圾回收线程在运行,当垃圾回收线程完成之后,其他线程才开始工作
3、ParNew收集器
ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。
开启指令:新生代使用:-XX:+UseParNewGC
4、响应时间优先垃圾回收器CMS
特点:多线程、垃圾回收线程与其他用户线程可以并发执行,适用于堆内存较大的多核cpu环境,尽可能让单次的STW的时间最短,例如一小时内的STW:0.1、0.1、0.1、0.1 四次,旨在每次都是最低
- -XX:+UseConcMarkSweepGC:启用cms
- -XX:ConcGCThreads:并发的GC线程数,一般设置cpu数的四分之一
- -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
- -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
- -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
- -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
- -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
- -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
- -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
缺点:
- 对CPU资源敏感(会和服务抢资源);
- 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了);
- 它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数-XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收
- 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
- 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。
- 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
- 并发重置:重置本次GC过程中的标记数据。
在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。漏标的问题主要引入了三色标记算法来解决。
三色标记算法是把Gc roots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:
- 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
- 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
- 白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
产生问题:
多标-浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。
漏标-读写屏障
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB) 。
增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。(cms使用)
原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)(G1使用)
说到原始快照可能会有疑问,被删除了不就应该清理吗?
事实是这样的,满足原始快照的方式必须是该白色对象被黑色对象引用着(扫描完毕后才引用的),因为黑色对象已经被扫描过了,而后面有一个灰色对象将其断开,扫描灰色对象的时候可能会将其当作垃圾清理,就会造成黑色对象引用的对象被清理,会使程序出现严重错误。
4、G1(Garbage First)垃圾回收器
特点:G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征.同时注重吞吐量,默认是暂停200ms,超大的堆内存会被均分成多个Region,大小是1-32m之间的2的幂指数,
整体上是标记+整理算法,避免的内存碎片的产生,两个区之间用的是复制算法
开启指令:-XX:UseG1GC
相关指令:-XX:G1HeapRegionSize=size //调节每个区的大小
-XX:MaxGCPauseMillis=time //设置暂时时间
细节讲解:
G1将Java堆划分为多个大小相等的独立区域(Region),JVM目标是不超过2048个Region(JVM源码里TARGET_REGION_NUMBER 定义),实际可以超过该值,但是不推荐。
一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。
G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。
G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
G1收集器一次GC(主要值Mixed GC)的运作过程大致分为以下几个步骤:
- 初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快 ;
- 并发标记(Concurrent Marking):同CMS的并发标记
- 最终标记(Remark,STW):同CMS的重新标记
- 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了ZGC,Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。
被视为JDK1.7以上版本Java虚拟机的一个重要进化特征。它具备以下特点:
- 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
- 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。
毫无疑问, 可以由用户指定期望的停顿时间是G1收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 不过, 这里设置的“期望值”必须是符合实际的, 不能异想天开, 毕竟G1是要冻结用户线程来复制对象的, 这个停顿时
间再怎么低也得有个限度。 它默认的停顿目标为两百毫秒, 一般来说, 回收阶段占到几十到一百甚至接近两百毫秒都很正常, 但如果我们把停顿时间调得非常低, 譬如设置为二十毫秒, 很可能出现的结果就是由于停顿目标时间太短, 导致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 导致垃圾慢慢堆积。 很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引发Full GC反而降低性能, 所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
G1垃圾收集分类
- YoungGC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC
- MixedGC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC
- Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)
G1收集器参数设置
- -XX:+UseG1GC:使用G1收集器
- -XX:ParallelGCThreads:指定GC工作的线程数量
- -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
- -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
- -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
- -XX:G1MaxNewSizePercent:新生代内存最大空间
- -XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
- -XX:MaxTenuringThreshold:最大年龄阈值(默认15)
- -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
- -XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
- -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
- -XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
G1垃圾收集器优化建议
假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久才会做年轻代gc,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。
或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。
所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc.
什么场景适合使用G1
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间特别长,超过1秒
- 8GB以上的堆内存(建议值)
- 停顿时间是500ms以内
跨代引用问题:
对于传统情况判断老年代是否引用新生代是通过扫描整个老年代的方法,这个工作量可谓不小,对于跨代引用,在g1中会再将老年代区划分为多个card,以card表的形式存在,每一个card大约512kb,而如果某个card引用了新生代的对象,这个card就称为脏卡,而在伊甸园区中有一个Remembered Set用于保存对应的脏card,在引用变更时会通过post-write barrier+dirty car queque来记录要变更的指令,后面通过concurrent refinement threads 异步更新remenbered set ,将对应的card标记为脏card
jdk8u20字符串去重
开启指令:-XX:+UseStringDeduplication
过程现在有两个new出来的字符串对象,都是以char数组的形式存在的。在jdk8u20中,会将所有新分配的字符串放入一个队列中,当新生代回收的时候,G1并发检查是否有字符串重复,如果他们的值是一样的,那就让他们引用同一个char数组,
优点:节省了大量内存
缺点:由于是在新生代回收的时候进行的,所以会略微增加新生代回收的时间
jdk8u40并发标记类卸载
所有类经过并发标记过后,就知道哪些类不再被使用,当一个类加载器的所有的类都不再使用,则卸载它所加载的所有类
jdk8u60回收巨型对象
一个对象大于region的一半时,就称之为巨型对象
G1不会对巨型对象进行拷贝
回收时被优先考虑
当老年代卡表中对巨型对象的引用为0时,就可以在新生代的垃圾回收中处理掉