【JVM 学习笔记 03】:垃圾回收算法和垃圾回收器

【JVM 学习笔记 03】:垃圾回收算法和垃圾回收器

一、新生代的垃圾回收算法:复制算法

所谓的“复制算法“,把新生代内存划分为两块内存区域,然后只使用其中一块内存,待那块内存快满的时候,就把里面的存活对象一次性转移到另外一块内存区域,保证没有内存碎片,接着一次性回收原来那块内存区域的垃圾对象,再次空出来一块内存区域。两块内存区域就这么重复着循环使用。

1.1 复制算法的缺点?

复制算法的缺点其实非常的明显,如果按照上述的思路,假设我们给新生代1G的内存空间,那么只有 512MB的内存空间是可以用的。
另外512MB的内存空间是一直要放在那里空着的,然后512MB内存空间满了,就把存活对象转移到另外一块512MB 的内存空间去
从始至终,就只有一半的内存可以用,这样的算法显然对内存的使用效率太低了。

1.2 复制算法的优化:Eden区和Survivor区

实际上真正的复制算法会做出如下优化,把新生代内存区域划分为三块:
1个 Eden 区,2个 Survivor 区,其中 Eden 区占80%内存空间,每一块 Survivor 区各占10%内存空间,比如说 Eden 区有800MB内存,每 一块Survivor区就100MB内存,如下图。
在这里插入图片描述
平时可以使用的,就是 Eden 区和其中一块 Survivor 区,那么相当于就是有900MB的内存是可以使用的。

刚开始对象都是分配在Eden区内的,如果Eden区快满了,此时就会触发垃圾回收。此时就会把Eden区中的存活对象都一次性转移到一块空着的Survivor区。接着Eden区就会被清空,然后再次分配新对象到Eden区里,Eden区和一块Survivor区里是有对象的,其中Survivor区里放的是上一次 Minor GC后存活的对象。

如果下次再次 Eden 区满,那么再次触发 Minor GC,就会把 Eden 区和放着上一次 Minor GC后存活对象的 Survivor 区内的存活对象,转移到另外一块Survivor区去。

接着新对象继续分配在Eden区和另外那块开始被使用的Survivor区,然后始终保持一块Survivor区是空着的,就这样一直循环使用这三块内存区域。

最大的好处,就是只有10%的内存空间是被闲置的,90%的内存都被使用上了
无论是垃圾回收的性能,内存碎片的控制,还是说内存使用的效率,都得到优化。

二、老年代的垃圾回收算法

2.1 新生代里的对象一般在什么场景下会进入老年代?

  1. 躲过15次GC之后进入老年代
    对象每次在新生代里躲过一次GC被转移到一块 Survivor 区域中,此时他的年龄就会增长一岁。默认的设置下,当对象的年龄达到15岁的时候,也就是躲过15次GC的时候,他就会转移到老年代里去。
    这个具体是多少岁进入老年代,可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁。

  2. 动态对象年龄判断
    这里跟这个对象年龄有另外一个规则可以让对象进入老年代,不用等待15次GC过后才可以。
    假如说当前放对象的 Survivor 区域里,一批对象的总大小大于了这块 Survivor 区域的内存大小的50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。
    实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了 Survivor 区 域的50%,此时就会把年龄n及以上的对象都放入老年代。

  3. 大对象直接进入老年代
    有一个JVM参数,就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。

他的意思就是,如果你要创建一个大于这个大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个大对象放到老年代里去。压根儿不会经过新生代。

之所以这么做,就是要避免新生代里出现那种大对象,然后屡次躲过GC,还得把他在两个Survivor区域里来回复制多次之后才能进入老年代,这是很耗费时间的。

2.2 Minor GC后的对象太多无法放入Survivor区怎么办?

如果在Minor GC之后发现剩余的存活对象太多了,没办法放入另外一块Survivor区,
在这里插入图片描述
这个时候就必须得把这些对象直接转移到老年代去,如下图所示。
在这里插入图片描述

2.3 老年代空间分配担保规则

首先,在执行任何一次Minor GC之前,JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。
为啥检查这个呢?因为最极端的情况下,可能新生代Minor GC过后,所有对象都存活下来了,新生代所有对象全部要进入老年代。

如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,因为即使Minor GC之 后所有对象都存活,Survivor区放不下了,也可以转移到老年代去。

但是假如执行Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,那么这个时候是不是有可能在Minor GC之后新生代的对象全部存活下来,然后全部需要转移到老年代去,但是老年代空间又不够?

所以假如Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,就会看一个“-XX:HandlePromotionFailure”的参数是否设置了,如果有这个参数,那么就会继续尝试进行下一步判断。

下一步判断,就是看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。
举个例子,之前每次Minor GC后,平均都有10MB左右的对象会进入老年代,那么此时老年代可用内存大于10MB。这就说明,很可能这次Minor GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的,看下图。
在这里插入图片描述
如果上面那个步骤判断失败了,或者是“-XX:-HandlePromotionFailure”参数没设置,此时就会直接触发一次“Full GC”,就是对老年代进行垃圾回收,尽量腾出来一些内存空间,然后再执行Minor GC。

如果上面两个步骤都判断成功了,那么就是说可以冒点风险尝试一下Minor GC。此时进行Minor GC有几种可能。
第一种可能,Minor GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor 区域即可。
第二种可能,Minor GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小 的,此时就直接进入老年代即可。
第三种可能,很不幸,Minor GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内 存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触 发一次“Full GC”。
Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。

因为这个时候必须得把老年代里的没人引用的对象给回收掉,然后才可能让Minor GC过后剩余的存活对象进入老年代 里面。
如果要是Full GC过后,老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的 “OOM”内存溢出了
因为内存实在是不够了,你还是要不停的往里面放对象,当然就崩溃了。

2.4 老年代垃圾回收触发时机

简单来说,一句话总结,对老年代触发垃圾回收的时机,一般就是两个:
要不然是在Minor GC之前,一通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需要提前触发Full GC然后再带着进行Minor GC;
要不然是在Minor GC之后,发现剩余对象太多放入老年代都放不下了。

2.5 老年代垃圾回收算法:标记整理算法

首先标记出来老年代当前存活的对象,这些对象可能是东一个西一个的。接着会让这些存活对象在内存里进行移动,把存活对象尽量都挪动到一边去,让存活对象紧凑的靠在一起,避免垃圾 回收过后出现过多的内存碎片,然后再一次性把垃圾对象都回收掉。
在这里插入图片描述
老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。
如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。

JVM优化:
所谓JVM优化,就是尽可能让对象都在 新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收。

一个日处理上亿数据的计算系统的JVM 优化分析过程

捋清楚。

三、JVM中都有哪些常见的垃圾回收器,各自的特点是什么?

  1. Serial和Serial Old垃圾回收器: 分别用来回收新生代和老年代的垃圾对象
    工作原理就是单线程运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们系统直接卡死不动,然 后让他们垃圾回收,这个现在一般写后台Java系统几乎不用。

  2. ParNew和CMS垃圾回收器:ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他 们都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。

  3. G1垃圾回收器:统一收集新生代 和老年代,采用了更加优秀的算法和设计机制。

JVM的痛点:Stop the World

在垃圾回收的这个过程
因为在垃圾回收的时候,尽可能要让垃圾回收器专心致志的干工作,不能随便让我们写的Java系统继续对象了,所以此时JVM会在后台 直接进入“Stop the World”状态。
也就是说,他会直接停止我们写的Java系统的所有工作线程,让我们写的代码不再运行!
然后让垃圾回收线程可以专心致志的进行垃圾回收的工作,如下图所示。
在这里插入图片描述
一旦垃圾回收完毕,就可以继续恢复我们写的Java系统的工作线程的运行了,然后我们的那些代码就可以继续运行,继续在Eden 中创建新的对象。

3.1 最常用的新生代垃圾回收器:ParNew

一般来说,在没有最新的G1垃圾回收器之前,通常大家线上系统都是ParNew垃圾回收器作为新生 代的垃圾回收器,当然现在即使有了G1,其实很多线上系统还是用的ParNew。

通常运行在服务器上的Java系统,都可以充分利用服务器的多核CPU的优势,假设服务器 是4核CPU,如果对新生代垃圾回收的时候,理论上4核CPU就可以支持4个垃圾回收线程并行执行,可以提升4倍的性 能!如果仅仅使用单线程进行垃圾回收,会导致没法充分利用CPU资源。

新生代的ParNew垃圾回收器主打的就是多线程垃圾回收机制,另外一种Serial垃圾回收器主打的是单线程垃圾回收,他们俩都是回收新生代的,唯一的区别就是单线程和多线程的区别,但是垃圾回收算法是完全一样的。

ParNew垃圾回收器如果一旦在合适的时机执行Minor GC的时候,就会把系统程序的工作线程全部停掉,禁止程序继续运行创建新的对象,然后自己就用多个垃圾回收线程去进行垃圾回收。
在这里插入图片描述

如何为线上系统指定使用ParNew垃圾回收器?

使用“-XX:+UseParNewGC”选项,JVM启动之后对新生代进行垃圾回收的,就是 ParNew垃圾回收器了。

ParNew垃圾回收器默认情况下的线程数量

ParNew垃圾回收器默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的。一般不用我们手动去调节,因为跟CPU核数一样的线程数量,是可以充分进行并行处理的。

如果一定要自己调节ParNew的垃圾回收线程数量,也是可以的,使用“-XX:ParallelGCThreads”参数即可, 通过他可以设置线程的数量
但是建议一般不要随意动这个参数。

思考题:到底是用单线程垃圾回收好,还是多线程垃圾回收好? 到底是Serial垃圾回收器好还是ParNew垃圾回收器好?

启动系统的时候是可以区分服务器模式和客户端模式的,如果你启动系统的时候加入“-server”就是服务器模式,如 果加入“-cilent”就是客户端模式。他们俩的区别就是,如果你的系统部署在比如4核8G的Linux服务器上,那么就应该用服务器模式,如果你的系统是运 行在比如Windows上的客户端程序,那么就应该是客户端模式。

服务器模式通常运行我们的网站系统、电商系统、业务系统、APP后台系统之类的大型系统,一般都是多核CPU,所以此时如果要垃圾回收,用ParNew更好,因为多线程并行垃圾回收,充分利用多核CPU资源,可以提升性能。如果此时用了单线程垃圾回收,那么就有一些CPU是被浪费了,根本没用上。

如果你的Java程序是一个客户端程序,比如类似百度云网盘的Windows客户端,或者是印象笔记的Windows客 户端,运行在Windows个人操作系统上,这种操作系统很多都是单核CPU,此时你如果要是还是用ParNew来进行垃圾回收,就会导致一个CPU运行多个线程, 反而加重了性能开销,可能效率还不如单线程好。
因为单CPU运行多线程会导致频繁的线上上下文切换,有效率开销,建议采用Serial垃圾回收器,单CPU单线程垃圾回收即可, 反而效率更高。

3.2 老年代常用的垃圾回收器:CMS

CMS采用的是标记清理算法,其实就是我们之前给大家讲过的一个算法,先通过追踪GC Roots的方法,看看各个对象是否被GC Roots给引用了,如果是的话,那就是存活对象,否则就是垃圾对象。先将垃圾对象都标记出来,然后一次性把垃圾对象都回收掉。

这种方法最大的问题,就是会造成很多内存碎片。

3.2.1 CMS如何实现系统一边工作的同时进行垃圾回收?

如果停止一切工作线程“Stop the World”,然后慢慢的去执行“标记-清理”算法,会导致系统卡死时间过长,很多响应无法处理。
所以CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的。

CMS在执行一次垃圾回收的过程一共分为4个阶段:
初始标记
并发标记
重新标记
并发清理

  1. 初始标记 :首先,CMS要进行垃圾回收时,会先执行初始标记阶段,这个阶段会让系统的工作线程全部停止,进入“Stop the World”状态,标记出来所有GC Roots 直接引用的对象。方法的局部变量和类的静态变量是GC Roots。但是类的实例变量不是GC Roots。

  2. 并发标记:这个阶段会让系统线程可以随意创建各种新对象,继续运行。
    在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。在这个过程中,垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪。这个过程中,在进行并发标记的时候,系统程序会不停的工作,他可能会各种创建出来新的对象,部分对象可能成为垃圾。对老年代所有对象进行GC Roots追踪,其实是最耗时的,需要追踪所有对象是否从根源上被GC Roots引用了,但是这个最耗时的阶段,是跟系统程序并发运行的,所以其实这个阶段不会对 系统运行造成影响的

  3. 重新标记阶段:让系统程序停下来,再次进入“Stop the World”阶段。
    然后重新标记下在第二阶段里新创建的一些对象,还有一些已有对象可能失去引用变成垃圾的情况。

  4. 并发清理:重新恢复系统程序的运行,清理掉之前标记为垃圾的对象即可。这个阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行。

3.2.2 CMS垃圾回收的细节问题

  1. 消耗CPU资源
    CMS垃圾回收器虽然能在垃圾回收的同时让系统同时工作,但是在并发标记和并发清理两个耗时的阶段,垃圾回收线程和系统工作线程同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分。

  2. Concurrent Mode Failure问题
    在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象
    但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是“浮动垃圾”。因为他虽然成为了垃圾,但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们。

所以为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。“-XX:CMSInitiatingOccupancyFaction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是 92%。
也就是说,老年代占用了92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间,系统程序把一些新对象放入老年代中。

如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,会发生Concurrent Mode Failure,就是说并发垃圾回收失败,此时就会自动用“Serial Old”垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追 踪,标记出来全部垃圾对象,不允许新的对象产生
然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。

  1. 内存碎片问题
    老年代的CMS采用“标记-清理”算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样会导致大量的内存碎片产生。
    如果内存碎片太多,会导致后续对象进入老年代找不到可用的连续内存空间了,太多的内存碎片实际上会导致更加频繁的Full GC。

CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了,他意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片 连续内存空间,避免内存碎片。
还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理。内存碎片整理完之后,存活对象都放在一起,然后空出来大片连续内存空间可供使用。

思考题:为什么老年代的垃圾回收速度会比新生代的垃圾回收速度慢很多倍?到 底慢在哪里?

新生代执行速度其实很快,因为直接从GC Roots出发就追踪哪些对象是活的就行了,新生代存活对象是很少的,这个速度是极快的, 不需要追踪多少对象。
然后直接把存活对象放入Survivor中,就一次性直接回收Eden和之前使用的Survivor了。

CMS的Full GC,在并发标记阶段,他需要去追踪所有存活对象,老年代存活对象很多,这个过程就会很慢;

其次并发清理阶段,他不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢;

然后,还得执行一次内存碎片整理,把大量的存活对象给挪在一起,空出来连续内存空间,这个过程还得“Stop the World”,那就更慢了。

万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象了,引发了“Concurrent Mode Failure”问题,那更是麻烦,还得 立马用“Serial Old”垃圾回收器,“Stop the World”之后慢慢重新来一遍回收的过程,这更是耗时了。

案例优化分析:

1.一个日处理上亿数据的计算系统的JVM优化案例分析。

2. 每日上亿请求量的电商系统的优化。

面试题:ParNew + CMS 的GC ,如何保证只做young GC,JVM 参数如何配置?

首先上线系统之后,要借助一些工具观察每秒钟会新增多少对象在新生代里,然后多长时间触发一次Minor GC,平均每 次MInor GC之后会有多少对象存活,Survivor区是否可以放的下。
这里的关键点就是必须让Survivor区放下,而且不能因为动态年龄判定规则直接升入老年代。然后只要Survivor区可以放下,那么下次 Minor GC后还是存活这么多对象,依然可以在另外一块Survivor区放下,基本就不会有对象升入老年代里去。

四、 G1 垃圾回收器

4.1 ParNew + CMS带给我们的痛点是什么?

无论是新生代垃圾回收,还是老年代垃圾回收,都会或多或少产生“Stop the World”现象,对系统的运行是有一定影响的。
所以其实之后对垃圾回收器的优化,都是朝着减少“Stop the World”的目标去做的。
在这个基础之上,G1垃圾回收器就应运而生了,他可以提供比“ParNew + CMS”组合更好的垃圾回收的性能。

4.2 G1垃圾回收器

G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,G1 最大的一个特点,就是把Java堆内存拆分为多个大小相等的 Region。然后G1也会有新生代和老年代的概念,但是只不过是逻辑上的概念
也就是说,新生代可能包含了某些Region,老年代可能包含了某些Reigon,如下图。
在这里插入图片描述
G1最大的一个特点,就是可以让我们设置一个垃圾回收的预期停顿时间

4.3 G1是如何做到对垃圾回收导致的系统停顿可控的?

G1如果要做到这一点,他就必须要追踪每个Region里的回收价值。他必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾?
在这里插入图片描述

简单来说,G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region中可以 回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时 间内尽量回收尽可能多的垃圾对象。
这就是G1的核心设计思路。

在G1中虽然把内存划分为了很多的 Region,但是其实还是有新生代、老年代的区分。而且新生代里还是有Eden和Survivor的划分的。他们会各自占据不同的Region。只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden 和 Survivor 对应的Region也会不断增加。

实际上新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。

4.4 如何设定G1对应的内存大小?

G1对应的是一大堆的Region内存区域,每个Region的大小是一致的。

默认情况下自动计算和设置的,我们可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。然后JVM启动的时候一旦发现你使用的是G1垃圾回收器,可以使用“-XX:+UseG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048。
因为JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说1MB、2MB、4MB之类的。
比如说堆大小是4G,那么就是4096MB,此时除以2048个 Region ,每个 Region 的大小就是2MB。大概就是这样子来决定Region的数量和大小的,一般保持默认的计算方式就可以。

如果通过手动方式来指定,则是“-XX:G1HeapRegionSize”。

刚开始的时候,默认新生代对堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个是可以通过“XX:G1NewSizePercent”来设置新生代初始占比的,其实维持这个默认值即可。
因为在系统运行中,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“XX:G1MaxNewSizePercent”。

而且一旦Region进行了垃圾回收,此时新生代的 Region 数量还会减少,这些其实都是动态的。
刚开始就是一部分的Region是属于新生代的。
在这里插入图片描述

4.5 G1的新生代垃圾回收

随着不停的在新生代的Eden对应的Region中放对象,JVM就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例 60%。
一旦新生代达到了设定的占据堆内存的最大大小60%,比如都有1200个Region了,里面的Eden可能占据了1000个Region,每个 Survivor是100个Region,而且Eden区还占满了对象,此时如下图所示。
在这里插入图片描述
这个时候还是会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收,进入一个“Stop the World”状态
然后把Eden对应的Region中的存活对象放入S1对应的Region中,接着回收掉Eden对应的Region中的垃圾对象。

但是这个过程跟之前是有区别的,因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms。

那么G1就会通过之前说的,对每个Region追踪回收他需要多少时间,可以回收多少对象来选择回收一部分的Region,保证GC停顿时间控制在指定范围内,尽可能多的回收掉一些对象。

4.6 对象什么时候进入老年代?

跟原来一样,满足以下条件:
(1)对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,他就 会进入老年代
(2)动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%

4.7 大对象Region

和之前的规则不一样,G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。

在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2MB,只要一个大对 象超过了1MB,就会被放入大对象专门的Region中
而且一个大对象如果太大,可能会横跨多个Region来存放。
在这里插入图片描述
大对象 Region 触发垃圾回收的时机:新生代、老年代在回收的时候,会顺带带着大对象Region一起回收,所以这就是在G1内存模型下对大对象的分配和回收的策略。

4.8 新生代+老年代的混合垃圾回收的触发时机?

G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,他的默认值是45%
意思就是说,如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。

此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象。,因为我们设定了对GC停顿时间的目标,所以会从新生代、老年代、大对象里各自挑选一些Region,保证用指定 的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收.

4.9 G1垃圾回收的过程

  1. 首先会触发一个“初始标记”的操作,这个过程是需要进入“Stop the World”的,仅仅只是标记一下GC Roots直接能引用的对象, 这个过程速度是很快的。

  2. 接着会进入“并发标记”的阶段,这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象。
    这个并发标记阶段还是很耗时的,因为要追踪全部的存活对象。但是这个阶段是可以跟系统程序并发运行的,所以对系统程序的影响不太大。而且JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。

  3. 最终标记阶段,这个阶段会进入“Stop the World”,系统程序是禁止运行的,但是会根据并发标记 阶段记录的 那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象.

  4. “混合回收“阶段,这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。
    接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我 们指定的范围内。

4.10 G1垃圾回收器的一些参数

-XX:G1MixedGCCountTarget ,在一次混合回收的过程中,最后一个阶段执行几次混合 回收,默认值是8次
意味着最后一个阶段,先停止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Region,反 复8次。

反复回收多次,是停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。

-XX:G1HeapWastePercent ,默认值是5% 。在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他 Region,然后这个Region中的垃圾对象全部清理掉,在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

-XX:G1MixedGCLiveThresholdPercent”,他的默认值是85%,意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收。

4.11 回收失败时的Full GC

如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region 里去
此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发 一次失败。

一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。

G1天生适合大内存机器的JVM运行,可以完美解决大内存垃圾回收时间过长的问题。

4.12 百万级用户的在线教育平台,如何基于G1垃圾优化垃圾回收性能?

案例分析和总结。

五、相关名词解释

(1)Minor GC / Young GC
Minor GC / Young GC, 这两个名词是等价的。在年轻代中的Eden内存区域被占满之后,实际上就需要触发年轻代的gc,或者是新生代的 gc。此时这个新生代gc,其实就是所谓的“Minor GC”,也可以称之为“Young GC”,这两个名词,说白了,就专门针对新生代的gc。

(2)Full GC?Old GC?
其实所谓的老年代gc,称之为“Old GC”更加合适一些,因为从字面意义上就可以理解,这就是所谓的老年代gc。
但是在这里之所以我们把老年代GC称之为Full GC,其实也是可以的,只不过是一个字面意思的多种不同的说法。
为了更加精准的表述这个老年代gc的含义,一律把老年代gc称之为Old GC,后续也如此定义。
所以在这里,大家务必捋清这个概念,在跟面试官聊的时候,如果说到所谓的老年代gc,为了避免歧义,建议用 Old GC来指代。

(3)Full GC
对于Full GC,其实这里有一个更加合适的说法,就是说Full GC指的是针对新生代、老年代、永久代的全体内存空间的垃圾回收,所以称之为Full GC。
从字面意思上也可以理解,“Full”就是整体的意思,所以就是对JVM进行一次整体的垃圾回收,把各个内存区域的垃 圾都回收掉。
但是如果从字面意义上来理解,建议大家日后在外面跟别人交谈的时候,把Full GC理解为针对JVM内所有内存区域的 一次整体垃圾回收。

(4)Major GC
还有一个名词是所谓的Major GC,这个其实一般用的比较少,他也是一个非常容易混淆的概念
有些人把Major GC跟Old GC等价起来,认为他就是针对老年代的GC,也有人把Major GC和Full GC等价起来,认为 他是针对JVM全体内存区域的GC。
所以针对这个容易混淆的概念。如果听到有人说这个Major GC的概念,可以问清楚,他到底 是想说Old GC呢?还是Full GC呢?

(5)Mixed GC
Mixed GC是G1中特有的概念,其实说白了,主要就是说在G1中,一旦老年代占据堆内存的45%了,就要触发Mixed GC,此时对年轻代和老年代都会进行回收。这个概念很好理解,只要知道是G1中特有的名词即可。

面试题:ParNew+CMS的GC,如何保证只做YoungGC,JVM参数如何配置?

答:需要深度结合线上系统的实际运行来看。
首先上线系统之后,要借助一些工具观察每秒钟会新增多少对象在新生代里,然后多长时间触发一次Minor GC,平均每次MInor GC之后会有多少对象存活,Survivor区是否可以放的下。
这里的关键点就是必须让Survivor区放下,而且不能因为动态年龄判定规则直接升入老年代。然后只要Survivor区可以放下,那么下次 Minor GC后还是存活这么多对象,依然可以在另外一块Survivor区放下,基本就不会有对象升入老年代里去。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值