JVM垃圾收集器G1
G1收集器
- G1是面向服务器的垃圾收集器,主要针对配备多核处理以及大容器内存的机器,极高概率满足GC停顿时间要求,还具备高吞吐量特征;
-
G1将Java堆划分为多个大小相等的独立区域Region,JVM最多可以有2048个Region;
-
一般Region大小等于堆大小/2048。
比如堆大小为4096M,则Region大小为2M。
可以使用参数“-XX:G1HeapRegionSize”指定region大小。 -
G1保留了年轻代与老年代的概念,但是年轻代与老年代的物理内存不再是连续的,而是一系列分散的Region集合。
-
默认年轻代与堆内存的占比为5%,如果堆大小为4096,那么年轻代占据200M左右的内存,对应大概是100个Region。
可以通过“-XX:G1NewSizePercent"调整。 -
年轻代中的Eden和Survivor的比例默认是8:1:1,。
-
一个Region的身份可能会发生变化,可能先是老年代,后变为年轻代。
因为对Region进行了垃圾回收,变为了空Region,然后创建对象又分配到该Region,所以变为了年轻代。 -
G1可对大对象进行特殊处理;G1专门分配大对象的区域为Humongous区,而不是直接进入老年代。G1对于大对象的判定规则就是大于Region容量的一半。
如果一个Region为2M,一个对象大于1M,会分配到Humongous区。如果对象的大小为Region的几倍,会使用多个连续的Humongous区域来存储这个大对象。 -
Humongous专门存放短期巨型对象,不用直接进入老年代,避免老年代提前爆满,从而出发Full GC、
-
Full GC的时候,除了收集年轻代与老年代,也会收集Humongous区。
G1的垃圾回收过程
- 初始标记
暂停所有的其他工作线程,并记录GC Root直接引用的对象,速度快。(同CMS) - 并发标记
从GCroot直接引用的对象开始,遍历标记整个对象图,这个过程和工作线程并发运行,耗时比较长。(同CMS) - 最终标记
由于并发标记期间,应用程序还在跑,部分标记完的对象,其标记可能会发生变动。最终标记就是修正这部分标记发生变动的对象。(同CMS) - 筛选回收
筛选回收阶段会对各个Region的回收价值与成本进行排序,根据用户所期望的GC停顿时间(-XX:MaxGCPauseMillis)来制定回收计划,通过之前的回收成本计算得知,可能回收其中800个Region需要200ms,那么只回收这800个Region。尽量把GC导致的停顿时间控制在我们的指定范围内。这个阶段也可以做到与用户程序并发执行,但是因为只回收一部分Region,时间是用户控制,而且停顿用户线程将大幅度提高收集效率。不管是年轻代还是老年代,都是使用复制算法,将Region中存活的对象复制熬另一个Region中,这种不会像CMS产生很多内存碎片从而需要整理内存。G1采用复制算法回收几乎不会有太多的内存碎片。(注意:CMS回收阶 段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并 发收集,Shenandoah可以看成是G1的升级版本)
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回 收时间有限情况下,G1当然会优先选择后面这个Region回收。
这种使用Region划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限时间内尽可能高的收集效率;
G1特点
1. 并行与并发:充分利用CPU、多核环境下的硬件优势,使用多个CPU缩短STW时间。
2. 分代收集:G1不需要与其他收集器配合就可管理整个堆内存,但是还是保留了分代的概念
3. 空间整合:与CMS的标记-清理不同,G1整体上是基于标记-整理算法实现的收集器;从局部看、是基于复制算法实现。
4. 可预测的停顿时间:G1相对于CMS的另一个大优势、降低停顿时间是G1与CMS共同关注点,G1除了追求低停顿外,还建立了可预测的停顿时间模型,能指定长度为毫秒的时间片段(XX:MaxGCPauseMillis)。
当然设置停顿时间应该设置合理的停顿时间,使得G1在吞吐量与延迟取得平衡。
如果设置的停顿时间短(几毫秒到几十毫秒)、那么每次回收的垃圾就会减少,而程序生成垃圾的速度大于回收垃圾的速度,进而堆内存爆满的速度变快,如果G1回收效果差,最终导致触发Full GC,暂停所有工作线程执行一次独占式清理。
G1垃圾收集分类
Young GC
当Eden区满后,Young GC不一定立刻被触发,G1先计算Eden区回收需要多长时间,如果回收时间远小于停顿时间,就增加年轻代的Region,继续给新创建的对象分配内存,不会立马做YoungGC,当下一次Eden区满后,再次计算回收时间是否接近停顿时间,是则触发Young GC进行回收。
Mixed GC
非Full GC;老年代对占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值就触发;回收所有的Young和部分Old(根据停顿时间确定Old区域垃圾收集的优先顺序)以及大对象区域,正常情况下,G1的垃圾收集先做MixedGC,主要使用赋值算法,需要把各个Region中存活的对象拷贝到别的Region里去,拷贝过程如果发现没有足够的空Region能够承载拷贝对象就会触发一次Full GC;
Full GC
停止应用程序,然后采用单线程进行标记,清理和压缩整理,清理出一批Region来供下一次MixedGC使用。这个过程非常耗时。
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:MaxGCPauseMillis停顿时间的设置
如果设置时间过长,导致系统运行很久,Eden区域过多,年轻代占用堆比例过高,触发Young GC时,会有很多存活对象,如果存活对象大于S区的一半,触发动态年龄判定规则,导致一些对象进入老年代。
设置时间过短,导致回收区域数量变少,回收垃圾变少,垃圾对象堆积越来越多,且还需要不断新创建独享,这样就会频繁触发MixedGC;
G1使用场景
- 50%以上的堆被存活对象占用
- 对象分配和晋升速度变化大;
- 垃圾回收时间特别长
- 8G以上的堆内存
- 停顿时间500ms以内;
每秒几十万并发的系统如何优化JVM
像Kafka,每秒处理几万到几十万消息,一般部署Kafka需要大内存机器(64G),也即是说可以给年轻代分配30-40个G的内存来支撑并发处理。
对于Eden区的Young GC很快,对于G1之外的收集器(Serial,ParNew,Parallel)来说。但是对于三四十G的内存,每次收集垃圾最快也要几秒钟。让用户等几秒钟是致命的。
而Kafka的并发量放满三十个G的Eden也就一两分钟。
假设一个请求需要1KB内存(真正业务算少了),那么30万请求,那么每秒就有几个G的内存使用,每秒就300M内存,一分钟就18个G,两分钟左右就满了。然后一次YoungGC让用户等几秒,然后再处理新消息,显然不合理。
这使可以使用G1手机器,设置置 -XX:MaxGCPauseMills 为50ms,设置50ms能够回收3-4个G内存,50ms的卡顿是可以完全接受的,用户几乎无感知,那么系统可以一边进行垃圾收集,一边处理业务。
如何选择垃圾收集器
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
- 如果允许停顿时间超过1秒,选择并行或者JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
- 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC 下图有连线的可以搭配使用
JDK8默认使用Parallel 收集器组合;
JDK8默认使用G1
安全点与安全区域
安全点
就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比 如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。
这些特定的安全点位置主要有以下几种:
- 方法返回之前
- 调用某个方法之后
- 抛出异常的位置
- 循环的末尾
- 大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程 时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和 安全点是重合的。
安全区域
Safe Point 是对正在执行的线程设定的。 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。 因此 JVM 引入了 Safe Region。 Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。