java垃圾回收及gc全面解析(全面覆盖cms、g1、zgc、openj9)
一般来说,gc的停顿时间和活跃对象的堆大小成比例,视gc线程的数量,每1GB可能会停顿1-3秒,且cpu数量通常和gc呈现阿姆达尔定律(Amdahl’s Law),而非我们直观计算的线性变化。如下:
体现在gc中的时候,不同cpu数量下的gc成本如下:
使用不同类型的gc将会在停顿和吞吐量之间发生很大的变化(一般都是基于这两个目标之一),不恰当的设置gc甚至可能会导致OOM,但是无论如何都不会存在一个最好的gc,就如linux的cpu调度算法一样,不同的负载类型下都有最好的gc,但是没有打遍天下无敌手的招式。包括azul引以为傲的C4(采用连续性并发压缩实现,完全无STW)也一样,停顿几乎消失了,但是吞吐量降下来了。如下:
和gc相关的核心定义包括:
- Concurrent collector:并发收集器,指的是和应用线程一起运行,不会发生STW(stop the world)。
- Generational Concurrent Garbage Collector:分代并发gc,适合于大量对象朝生夕死,即OLTP环境。如下:
IBM OPENJ9 JVM
ORACLE HOTSPOT JVM
- Parallel collector:指的是多线程。
- Stop-the-world (STW):和Concurrent collector相反,垃圾收集期间,应用线程全部停止。
- Incremental:增量gc,采用分而治之的算法实现。
- Moving:垃圾回收期在gc期间移动存活的对象,并更新指向它们的引用。
- parallelNew:一个新生代收集器,CMS默认的新生代收集器,使用复制算法(如果大量对象不能朝生暮死(一般来说每次Min GC就能干掉大部分,具体间隔见下文“合适发生GC”,也可以使用参数GCPauseIntervalMillis设置最小间隔),不直接在eden区分配就非常重要,否则gc会很厉害)。
- Remeber-Set:不同分代/Region之间对象引用关系的存储容器,所以操作的时候需要维护Remeber-Set。
- Scanvenge:用于新生代扫描。
- parallelScavenge:一个新生代收集器,也使用复制算法,目标是吞吐量优先,而不是响应时间(见下文核心参数一节)。
- parallelOld:一个老年代收集器,目标是吞吐量优先,而不是响应时间(见下文核心参数一节)。parallelScavenge+parallelOld=parallelgc,1.6版本开始提供。
GC的步骤。总体来说,gc的过程分为下列几步:
- Mark:标记。现在gc一般采用是否有指向GC根(gc root,可作为GC root的对象哪些?)决定是否应该回收对象,以便正确识别相互引用的对象。它的工作量跟对象数有关、跟对象大小无关,因为理论上所有可到达的对象都要遍历一遍,只不过大多数jvm实现的时候采用分代区域管理对象,因此扫描的对象大大减少。由于大多数对象都是请求级临时性的,所以大多数很快就会回收,所以eden区留下需要每次gc时重复检测的就很少了(如果很长时间主流内存的,说明需要评估是否应该采用堆外存储)(注:老年代由于存活时间长,所以不会采用拷贝这种算法,而是采用修改引用移动+指针实现)。原因如下:
由于在标记过程中,引用关系是会变的,主要是原来不引用的、现在引用了,所以gc一般采用写屏障来跟踪这些变化。
- Sweep:清理。它跟堆大小有关,无论如何都要扫描一遍。
- Compact:压缩。这一步通常只能STW。大多数商业gc为了尽可能降低延迟,这一步通常选择尽可能的延后执行,除非碎片太大,否则不进行压缩。
不同的垃圾回收器会采用不同方式实现,有的完全分三步,有的合并某些部分。不同的实现会导致不同的后果,包括gc占用的额外内存大小、速度、堆碎片、gc速度等等。
何时发生GC
1、eden区不够,且对象不直接在old区分配,则虚拟机发起Minor GC。
2、old区超过给定阈值(由参数InitiatingHeapOccupancyPercent控制,默认为45%,可配置)或担保分配失败,虚拟机发起Full GC。
核心参数
命令java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version可以打印所有jvm参数默认值,也可以启动时带上。有好几百个选项,比oracle优化器提示以及银行参数还多。
-XX:MaxGCPauseMillis=<nnn>:提示优化器应努力达到的最大暂停时间,gc会据此调整堆栈大小以及gc频率、其它参数,但是它和数据库优化器提示一样,只是尽力遵守。
-XX:GCTimeRatio=nnn:提示优化器应努力达到的最大gc时间占比。公式为1/ (1+nnn)。设置为19代表最多5%用于gc。如果该时间无法达到,gc会考虑加大堆大小(默认初始:1/64物理内存,最大1/4物理内存)。
-XX:UseAdapativeSizePolicy。让gc自动根据上面两个参数的大小动态调整新生代(eden、survivor)、老年代的大小。
-XX:+UseTLAB:是否启用线程本地分配缓冲(类似oracle的private redo buffer),能够降低分配锁争用。
-XX:PretenureSizeThreshold:超过多大的对象直接的old区分配,默认为0,首先在eden区分配。这个就得看情况了,一般来说大对象不应该朝生暮死,但是有些批处理系统就比较复杂了,设置该值的仔细测试,就OLTP而言,该值应该设置。
-XX:NewRatio=2:老生代/新年代默认比例。
-XX:NewSize=2m:新年代默认大小,新生代大小对性能的影响可见https://blogs.oracle.com/poonam/can-young-generation-size-impact-the-application-response-times。
-XX:MaxNewSize=2m:新年代最大大小。-Xmn,相当于NewSize和MaxNewSize同时设置了,如果要设置新生代,推荐使用Xmn。
-XX:ParallelGCThreads=cpu count/jvm数量。设置并行gc线程数,在多JVM环境中,一定要小于cpu count/jvm数量
如果堆已经是最大大小,但是吞吐量未达到,说明堆最大值太小,比如默认值;如果吞吐量达到了,但是暂停太长,可以设置最大暂停时间。但是它俩通常无法同时100%满足,需要取舍(当然如果系统负载很低,通常都能达到。所以重点是负载高的时候)。
影响gc的核心因素是堆大小以及年轻代的比例。
默认情况下,如果服务器线程数小于8,则gc线程数量为8;如果大于8,则为5/8(在某些特殊环境中,则为5/16)。当使用多个gc线程时,堆会产生一些碎片,因为每个gc线程都会都老年代划一部分空间用于临时存储从年轻代移动到老年代的对象(此举是为了降低堆分配的竞争),gc线程数越少、意味着碎片也越少。
主流GC
- HotSpot ParallelGC:jdk 8的默认gc,吞吐量优先。对新生代对象的拷贝(mark-copy)使用STW,对老年代的Mark/Sweep/Compact(mark-sweep-compact)三步骤均采用STW实现,无论新生代还是老年代,都是STW。它和ParallelOldGC的区别在于Compact也并发进行,而非串行进行。其各目标的优先级分别是:1, 首先满足暂停时间目标;2, 满足吞吐量目标; 3, 最后考虑最小化堆大小。
- Concurrent Mark/Sweep collector (CMS):jdk1.5引入。并发标记(准确的说,又分为初始标记、并发标记、重新标记,第1、3通常需要STW)、清理收集器,响应时间优先。其目标是尽可能对老年代的回收并发进行,并避免压缩,最小化延迟。但是一旦老年代碎片化太严重,压缩就需要STW。新生代和ParallelGC一样,拷贝采用STW(在JDK9中被标记为过期)。CMS的过程如下:
其中初始标记和重新标记速度一般非常快,并发标记则慢得多。因为GC过程中用户线程仍然运行,所以CMS的一个缺点是有些不再使用的对象会遗留到下一次才会被回收。当然还有一些和老年代碎片相关的问题也需要注意,在jdk 8u100+之后,g1应该来说要比cms合适了,这里就不细讲了,有兴趣可以一个个参数研究一下。
- Shenandoah GC:JDK12新增的gc。其evacuation阶段工作能通过与正在运行中Java工作线程同时进行(即并发,concurrent),从而减少GC的停顿时间,其主要是为了和zgc以及g1竞争,从其测试来看比g1效果更好,参见https://blog.51cto.com/14230003/2435438。Shenandoah的停顿时间和堆的大小没有任何关系,这就意味着无论你的堆是200MB,2GB还是200GB,停顿时间是一样的。
- g1(Garbage First):其目标是尽可能完全避免full gc,即老年代的STW,优先考虑暂停时间、其次才是吞吐量,所以更像是cms的升级版。它是通过分块(每块的大小可以通过-XX:G1HeapRegionSize设置,默认值根据堆初始和最大值自动计算,确保大约有2048块左右,jvm启动的时候会在一开始打印出来)gc实现的。-G1PrintRegionLivenessInfo(可打印每次标记后分块的大小和实际占用) -G1PrintHeapRegions(可在gc中输出分库的分配和回收情况)。其布局如下所示:
在分代上,不再那么的泾渭分明。
但是和parallel gc一样,一旦这些区域碎片太严重需要压缩,压缩仍然需要STW的方式完成,但是尽可能的避免了region内碎片的产生。新生代和parallelgc以及cms一样,拷贝也需要STW。在jdk 9中被作为默认gc(OLTP下用于代替CMS效果可以),而不是Parallel GC(吞吐量优先,但一定要设置并行gc数量,否则在大型服务器中负载会巨高),因为g1 gc在回收前会先评估对哪些分块进行gc能够得到更高的回收率,因此整体而言,内存需求会比parallel gc要更高,参见https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc。
- zgc:jdk 11引入,适用于20GB以上内存(注意,最好不要一个JVM进程的堆分配超过32GB),jdk 13开始支持释放未使用内存给操作系统。启用方法:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC,-XX:ZUncommitDelay=<seconds>控制内存释放的阈值,我们使用它也是为了能够释放未使用内存给其他进程或JVM使用。其原理介绍参见https://my.oschina.net/u/943305/blog/1838872,https://zhuanlan.zhihu.com/p/56486728,https://blog.csdn.net/j3t9z7h/article/details/87128403
- JDK 12 ShenandoahGC。它是redhat旗下的一个项目,作为JEP 189贡献给openjdk,用于大型配置环境,如20GB以下就不适合使用ShenandoahGC。可参见https://cloud.tencent.com/developer/article/1405874,https://www.linuxidc.com/Linux/2017-01/139427.htm,http://openjdk.java.net/projects/shenandoah/,对该gc LZ目前没有深入研究与测试。
- openj9 optthruput或gencon(默认):相当于parallelgc。
- openj9 optavgpause:有些相当于cms。
- openj9 balanced:相当于oracle g1。
捐献给eclispse基金会后,现在的openj9还可以使用hotspot jvm,意味着可以使用open jdk的gc如zgc。
典型的gc优化策略
参数优化。对gc优化来说,首先最重要的是开启gc相关的日志(-Xlog:gc*=debug)分别观察mark、sweep和compact、新生代、老年代的延时以及回收情况,然后确定gc的大小、暂停时间是不是偏高,并判断相关设置是否最合理。还需要注意的是,不同的m/s/c对内存的要求是不一样的,内存越少、gc所需时间越长,因此应确保留有一定的空闲内存供gc使用(如何设置???)。尤其是要避免老年代的分配失败(Allocation Failure),它通常是频繁的分配大对象所致(在g1中,它要比cms下占用内存更大,可通过jvm选项gc+heap=info在日志中跟踪该信息,在日志中体现为"Humongous regions: X->Y”),也可能是并发标记不够快(此时可以通过参数-XX:ConcGCThreads显示设置标记线程数)。如果是因为System.gc()太多导致且无法避免的话,可以增加参数-XX:+ExplicitGCInvokesConcurrent,让显示gc回收并发进行,这样STW就能够避免,虽然吞吐量可能会有一些下降(前提是负载足够高了)。
- 多JVM。多JVM的缺点是如果使用了一级缓存的话,需要做好同步保障。优点在于每个JVM的GC压力会大大下降。
- largePageHeap。虽然JVM参数-XX:+AlwaysPreTouch可以设置让操作系统预分配内存而不是按需分配,但是其速度会比较慢。因此如果希望JVM内存预分配且常驻内存的话,还不如使用largePageHeap特性(使用largePage的情况下,ZGC是否生效)。
- 堆外存储(mapdb、ehcache,https://www.ehcache.org/documentation/2.8/get-started/storage-options.html)。如果很多数据为了提升性能需要在一级缓存中,且数据不是均衡访问的话(即符合80/20原则),可以考虑堆外缓存和堆内缓存的结合。这样虽然性能略低于直接存储在JVM缓存在,但也远高于在redis中,同时可以大大降低GC的压力。具体需要详细测试性能下降的比例,所以它适合于数据量不小的情况,例如超过10万行。如果堆足够大的话,足够容纳运行所需的工作区的话,直接在内存中也是可以的。不过最好优先考虑多JVM以及大页面堆。
GC日志的详细分析
不同JVM的gc日志差异比较大,这里主要分析CMS、G1、zgc以及openj9 zgc的日志。不同的gc日志选项输出的日志内容差异也比较大,详见gc日志输出深入解析-覆盖CMS、并行GC、G1、ZGC、openj9。
相关参考资料
azul Understanding_Java_Garbage_Collection_v4.pdf及PPT
HotSpot Virtual Machine Garbage Collection Tuning Guide
- 深入理解java虚拟机第二版(翻了一下实战JAVA虚拟机 JVM故障诊断与性能优化,讲真的,如果读者读过深入理解java虚拟机的话,说翻版也不为过;垃圾回收的算法与实现,不针对jvm,更像是普及型)
- Frequently Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine
- https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html
http://dinfuehr.github.io/blog/a-first-look-into-zgc/
- https://blog.csdn.net/jiankunking/article/details/85626279
- http://www.west999.com/cms/wiki/code/2018-07-20/41686.html
java垃圾回收及gc全面解析(全面覆盖cms、g1、zgc、openj9)的更多相关文章
- 转 Java虚拟机5:Java垃圾回收(GC)机制详解
转 Java虚拟机5:Java垃圾回收(GC)机制详解 Java虚拟机5:Java垃圾回收(GC)机制详解 哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无 ...
- Java虚拟机5:Java垃圾回收(GC)机制详解
哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象.那么如何找到这些对象? 1.引用计数法 这个算法的实现是,给对象中添 ...
- Java垃圾回收(GC)机制详解
一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此.所以,垃圾回收是必须的. 二. ...
- java垃圾回收机制GC
记得第一次总结java 的GC的时候,是刚开始在课堂上学习GC的时候,那时候许老师第一节java课 课后老师说同学们可以去深入理解一下java的GC机制: 但是是花费了三四个小时,翻看了<Thi ...
- Java垃圾回收【GC】机制详解
一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此.所以,垃圾回收是必须的. 二. ...
- Java垃圾回收机制(GC)
Java内存分配机制 这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本类型或String ...
- Java垃圾回收(GC)与引用的种类
垃圾回收 GC public class MyObject { private String id; public MyObject(String id) { this ...
- Java垃圾回收System.gc()的理解
System.gc()无法保证GC一定执行 在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式触发Full GC,同时对老年代和新生代进行回收 ...
- Java GC系列(1):Java垃圾回收简介
本文由 ImportNew - 好好先生 翻译自 javapapers. Java的内存分配与回收全部由JVM垃圾回收进程自动完成.与C语言不同,Java开发者不需要自己编写代码实现垃圾回收.这是Ja ...
随机推荐
- Qt 文件选项对话框弹出两次
1 问题 在Qt 5.12.0 版本中,用 QFileDialog 类来做文件选择时候,发现当弹出对话框后,选择完文件后,又弹出文件选择对话框. 2 原因查找 2.1 代码 QFileDialog ...
- springboot yml配置文件注入值
1.编写javabean: package com.example.springboot.bean; import org.springframework.boot.context.propertie ...
- 移动端可视化框架antv f2出现两个legend选项
前天遇到个坑,把我给坑死了 ,在帮朋友做一个微信公众号的项目,使用的vue全家桶,有个模块需要用到数据可视化展现,之前做项目的时候用过antv,比较熟悉,因为是移动端的项目,所以用的是antv f2这 ...
- .NET CORE 中的缓存使用
Net Framewoke的缓存 1.1 System.Web.Caching System.Web.Caching应该是我们最熟悉的缓存类库了,做ASP.NET开发时用到缓存基本都是使用的这个缓存组 ...
- Django 模板语言 条件判断
Django 模板语言 条件判断 ############### 条件 ################## # view 文件 def func(request): ... return rende ...
- golang ---Learn Concurrency
https://github.com/golang/go/wiki/LearnConcurrency 实例1: package main import ( "fmt" " ...
- - instanceof 和 isInstance 强转 类型 class MD
目录 目录 instanceof 和 isInstance 强转 类型 class MD 简介 测试案例 继承关系 测试代码 打印结果 Markdown版本笔记 我的GitHub首页 我的博客 我的微 ...
- Elasticsearch 史上最全最常用工具清单
基础类工具 1.Head插件 1)功能概述: ES集群状态查看.索引数据查看.ES DSL实现(增.删.改.查操作) 比较实用的地方:json串的格式化 2)地址:http://mobz.github ...
- Windows 查看端口占用进程并关闭
当我们在运行一些软件需要特定软件(如tomcat)时,有可能会碰上端口被占用的情况,这时候我们可能就需要更改端口或把占用端口的进程结束掉,因为更换端口可能会导致当前环境产生一些的问题或是需要重新配置其 ...
- 2019 中手游java面试笔试题 (含面试题解析)
本人5年开发经验.18年年底开始跑路找工作,在互联网寒冬下成功拿到阿里巴巴.今日头条.中手游等公司offer,岗位是Java后端开发,因为发展原因最终选择去了中手游,入职一年时间了,也成为了面试官 ...