常见GC问题分析与解决
1.GC基础1.1 JVM内存划分
JDK8版本的内存结构如下
JDK8版本的内存结构
GC 主要工作在 Heap 区和 MetaSpace 区(上图蓝色部分 ),在 Direct Memory 中,如果使用的是 DirectByteBuffer,那么在分配内存不够时则是 GC 通过 Cleaner#clean 间接管理。
任何自动内存管理系统都会面临的步骤:为新对象分配空间,然后收集垃圾对象空间,下面我们就展开介绍一下这些基础知识。1.2 分配对象
Java 中对象地址操作主要使用 Unsafe 调用了 C 的 allocate 和 free 两个方法,分配方法有两种:空闲链表(free list):通过额外的存储记录空闲的地址,将随机 IO 变为顺序 IO,但带来了额外的空间消耗。碰撞指针(bump pointer):通过一个指针作为分界点,需要分配内存时,仅需把指针往空闲的一端移动与对象大小相等的距离,分配效率较高,但使用场景有限。1.3 如何定位垃圾引用计数法(Reference Counting):对每个对象的引用进行计数,每当有一个地方引用它时计数器 +1、引用失效则 -1,引用的计数放到对象头中,大于 0 的对象被认为是存活对象。虽然循环引用的问题可通过 Recycler 算法解决,但是在多线程环境下,引用计数变更也要进行昂贵的同步操作,性能较低,早期的编程语言会采用此算法。可达性分析,又称引用链法(Tracing GC):从 GC Root 开始进行对象搜索,可以被搜索到的对象即为可达对象,此时还不足以判断对象是否存活/死亡,需要经过多次标记才能更加准确地确定,整个连通图之外的对象便可以作为垃圾被回收掉。目前 Java 中主流的虚拟机均采用此算法。1.4 常见垃圾回收算法Mark-Sweep(标记-清除):回收过程主要分为两个阶段,第一阶段为追踪(Tracing )阶段,即从 GC Root 开始遍历对象图,并标记(Mark )所遇到的每个对象,第二阶段为清除(Sweep )阶段,即回收器检查堆中每一个对象,并将所有未被标记的对象进行回收,整个过程不会发生对象移动。整个算法在不同的实现中会使用三色抽象(Tricolour Abstraction )、位图标记(BitMap )等技术来提高算法的效率,存活对象较多时较高效。Mark-Compact (标记-整理):这个算法的主要目的就是解决在非移动式回收器中都会存在的碎片化问题,也分为两个阶段,第一阶段与 Mark-Sweep 类似,第二阶段则会对存活对象按照整理顺序(Compaction Order )进行整理。主要实现有双指针(Two-Finger)回收算法、滑动回收(Lisp2 )算法和引线整理(Threaded Compaction )算法等。Copying(复制):将空间分为两个大小相同的 From 和 To 两个半区,同一时间只会使用其中一个,每次进行回收时将一个半区的存活对象通过复制的方式转移到另一个半区。有递归(Robert R. Fenichel 和 Jerome C. Yochelson提出 )和迭代(Cheney 提出 )算法,以及解决了前两者递归栈、缓存行等问题的近似优先搜索算法。复制算法可以通过碰撞指针的方式进行快速地分配内存,但是也存在着空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高。1.5 垃圾收集器
目前在 Hotspot VM 中主要有分代收集和分区收集两大类
分代收集和分区收集1.5.1分代收集器ParNew:一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过 -XX:ParallelGCThreads 参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。CMS:以获取最短回收停顿时间为目标,采用"标记-清除"算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除。1.5.2 分区收集器G1:一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。ZGC:JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。Shenandoah:由 Red Hat 的一个团队负责开发,与 G1 类似,基于 Region 设计的垃圾收集器,但不需要 Remember Set 或者 Card Table 来记录跨 Region 引用,停顿时间和堆的大小没有任何关系。停顿时间与 ZGC 接近,下图为与 CMS 和 G1 等收集器的 benchmark。1.5.3 各版本JVM中GC变化JDK9: 设置G1为JVM默认垃圾收集器JDK10:并行全垃圾回收器 G1,通过并行Full GC, 改善G1的延迟。目前对G1的full GC的实现采用了单线程-清除-压缩算法。JDK10开始使用并行化-清除-压缩算法。JDK11:推出ZGC新一代垃圾回收器(实验性),目标是GC暂停时间不会超过10ms,既能处理几百兆的小堆,也能处理几个T的大堆。JDK14 :删除CMS垃圾回收器;弃用 ParallelScavenge + SerialOld GC 的垃圾回收算法组合;将 zgc 垃圾回收器移植到 macOS 和 windows 平台JDk 15 : ZGC (JEP 377) 和Shenandoah (JEP 379) 不再是实验性功能。默认的 GC 仍然是G1。JDK16:增强ZGC,ZGC获得了 46个增强功能 和25个错误修复,控制stw时间不超过10毫秒1.6 常用工具1.6.1命令行终端标准终端类:jps、jinfo、jstat、jstack、jmap功能整合类:jcmd、vjtools、arthas、greys1.6.2可视化界面简易:JConsole、JVisualvm、HA、GCHisto、GCViewer进阶:MAT、JProfiler1.7 GC常用参数-Xmn -Xms -Xmx -Xss 年轻代 最小堆 最大堆 栈空间-XX:+UseTLAB 使用TLAB,默认打开-XX:+PrintTLAB 打印TLAB的使用情况-XX:TLABSize 设置TLAB大小-XX:+DisableExplictGC System.gc()不管用 ,FGC-XX:+PrintGC-XX:+PrintGCDetails-XX:+PrintHeapAtGC-XX:+PrintGCTimeStamps-Xloggc:opt/log/gc.log-XX:MaxTenuringThreshold 升代年龄,最大值15-XX:+UseConcMarkSweepGC-XX:ParallelCMSThreads CMS线程数量-XX:CMSInitiatingOccupancyFraction 使用多少比例的老年代后开始CMS收集,默认是68%。-XX:+UseCMSCompactAtFullCollection 在FGC时进行压缩-XX:CMSFullGCsBeforeCompaction 多少次FGC之后进行压缩2、常见问题调优策略2.1 选择合适的垃圾回收器CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。CPU多核,关注吞吐量 ,那么选择PS+PO组合。CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
参数配置: //设置Serial垃圾收集器(新生代) 开启:-XX:+UseSerialGC //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器 开启 -XX:+UseParallelOldGC //CMS垃圾收集器(老年代) 开启 -XX:+UseConcMarkSweepGC //设置G1垃圾收集器 开启 -XX:+UseG1GC2.2 调整内存大小
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
参数配置: //设置堆初始值 指令1:-Xms2g 指令2:-XX:InitialHeapSize=2048m //设置堆区最大值 指令1:`-Xmx2g` 指令2: -XX:MaxHeapSize=2048m //新生代内存配置 指令1:-Xmn512m 指令2:-XX:MaxNewSize=512m2.3 调整内存区域大小比率
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。
参数配置://survivor区和Eden区大小比率 指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6 //新生代和老年代的占比 -XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=22.4 调整对象升老年代的年龄
现象:老年代频繁GC,每次回收的对象很多。
原因:如果升代年龄小,新生代的对象很快就进入老年代了,导致老年代对象变多,而这些对象其实在随后的很短时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进入老年代解决老年代空间不足频繁GC问题。
注意:增加了年龄之后,这些对象在新生代的时间会变长可能导致新生代的GC频率增加,并且频繁复制这些对象新生的GC时间也可能变长。
配置参数://进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,默认值7 -XX:InitialTenuringThreshol=7
2.5 调整大对象的标准
现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。
原因:如果大量的大对象直接分配到老年代,导致老年代容易被填满而造成频繁GC,可设置对象直接进入老年代的标准。
注意:这些大对象进入新生代后可能会使新生代的GC频率和时间增加。
配置参数://新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。 -XX:PretenureSizeThreshold=1000000 2.6 调整GC的触发时机
现象:CMS,G1 经常 Full GC,程序卡顿严重。
原因:G1和CMS 部分GC阶段是并发进行的,业务线程和垃圾收集线程一起工作,也就说明垃圾收集的过程中业务线程会生成新的对象,所以在GC的时候需要预留一部分内存空间来容纳新产生的对象,如果这个时候内存空间不足以容纳新产生的对象,那么JVM就会停止并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运行。这个时候可以调整GC触发的时机(比如在老年代占用60%就触发GC),这样就可以预留足够的空间来让业务线程创建的对象有足够的空间分配。
注意:提早触发GC会增加老年代GC的频率。
配置参数: //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小 -XX:CMSInitiatingOccupancyFraction //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65% -XX:G1MixedGCLiveThresholdPercent=65
三、案例分享3.1 死锁排查示例
代码public class DeadLockTest { private static Object lock1 = new Object(); private static Object lock2 = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock1) { try { System.out.println("thread1 begin"); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock2) { System.out.println("thread1 end"); } } }).start(); new Thread(() -> { synchronized (lock2) { try { System.out.println("thread2 begin"); Thread.sleep(5000); } catch (InterruptedException e) { } synchronized (lock1) { System.out.println("thread2 end"); } } }).start(); System.out.println("main thread end"); } }
启动:java DeadLockTest
使用jps查询当前的java进程,得到对应的程序进程pid
排查死锁,使用jstack的命令,得到的线程栈信息
jstack pid
线程栈信息
从图中就很明显看到一个死锁的提示3.2 CPU缓慢飙高,程序频繁FullGC
代码public class FullGC { private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(10, new ThreadPoolExecutor.DiscardOldestPolicy()); public static void main(String[] args) throws Exception { executor.setMaximumPoolSize(50); while(true) { test(); Thread.sleep(100L); } } private static void test() { List userList = getUserListInfo(); userList.forEach((info) -> { executor.scheduleWithFixedDelay(() -> { info.emptyMethod(); }, 2L, 3L, TimeUnit.SECONDS); }); } private static List getUserListInfo() { List userList = new ArrayList(); for(int i = 0; i < 100; ++i) { User user = new User(); userList.add(user); } return userList; } private static class User { String name; int age; Date birthdate; private User() { this.name = String.valueOf(new Random().nextInt(10)); this.age = new Random().nextInt(10); this.birthdate = new Date(); } public void emptyMethod() { } } }
启动命令:java -Xmx20m -Xms20m -XX:+PrintGC com.andy.jvm.gc.FullGC
排查
问题分析:CPU高一定是某个程序长期占用了CPU资源。先需要找出哪个进程占用CPU高。top 列出系统各个进程的资源占用情况然后根据找到对应进程里哪个线程占用CPU高。top -Hp 进程ID 列出对应进程里面的线程占用资源情况找到对应线程ID后,再打印出对应线程的堆栈信息printf "%x " PID 把线程ID转换为16进制。 jsack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。确认是GC线程再占用CPU先观察垃圾回收的情况jstat -gc PID 1000 查看GC次数,时间等信息,每隔一秒打印一次。 jmap -histo PID | head -20 查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。查询堆信息jmap - histo 4655 | head -20 jmap -dump:format=b,file=xxx pid jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆内存信息到文件。
使用jvisualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。
3.3动态扩容引起的空间震荡
现象:
服务刚刚启动时 GC 次数较多,最大空间剩余很多但是依然发生 GC,这种情况我们可以通过观察 GC 日志或者通过监控工具来观察堆的空间变化情况即可。GC Cause 一般为 Allocation Failure,且在 GC 日志中会观察到经历一次 GC ,堆内各个空间的大小会被调整,如下图所示:
动态扩容引起的空间震荡
解决:
在 JVM 的参数中 -Xms 和 -Xmx 设置的不一致,在初始化时只会初始 -Xms 大小的空间存储信息,每当空间不够用时再向操作系统申请,这样的话必然要进行一次 GC。
另外,如果空间剩余很多时也会进行缩容操作,JVM 通过 -XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 来控制扩容和缩容的比例,调节这两个值也可以控制伸缩的时机。
尽量将成对出现的空间大小配置参数设置成固定的,如 -Xms 和 -Xmx ,-XX:MaxNewSize 和 -XX:NewSize ,-XX:MetaSpaceSize 和 -XX:MaxMetaSpaceSize 等
这个问题虽然初级,但是发生的概率还真不小,尤其是在一些规范不太健全的情况下。
大话西游2全服首只成品新宝宝五方瑞狮,还有顶级敏魔6阶各位大话2的老朋友,除夕快乐!全新投放的新宝宝五方瑞狮已经有大佬玩家迅速打造成了八开变色的成品,咱们赶紧来看看钞能力的效果吧。看起来有点像古代瑞兽的感觉。没想到这次开发组没有另辟蹊
红白机以假乱真的山寨游戏,苦练多年后却发现玩了个寂寞红白机游戏到底有多少款呢?根据目前能收集到的情报显示,磁碟和卡带的数量加起来就有1200多款游戏。这些游戏都是通过任天堂发表出来的,是属于相对比较官方的作品。估计大家都觉得比较好奇
阳康新年开始大吃大喝?中医气血不正,五脏皆病咳嗽气短乏力自汗盗汗失眠焦虑当感染新冠病毒以后,身体可能会出现以上不适感。再机上寒流来袭,低气温的季节里,保暖防寒很关键,特别要注重气血循环,气血养好了,身体自然就好。黄帝内经中说
让自己开心的28个小建议大有学问人生不过三万天,哭也是一天,笑也是一天!那我选择笑着过。01hr学会忘掉一些事情。小时候如果记性不好是会挨骂的,但是长大了有些事有些人,能忘记就是一种幸福。02hr学会知足
观点乘用车市场库存车358万辆,崔东树建议谨慎设定产销节奏文懂车帝原创邢秋鸿懂车帝原创行业近日,乘联会秘书长崔东树发文表示2022年12月全国汽车生产238。3万辆,同比下降18。乘用车生产下降的主要原因是,2022年11月国内乘用车市场
买年货如何提高效率?FlymeAciy建议只需三步,还能省钱新春佳节,除了玩,买年货想必也是大家的日常项目,而网购应该就是年轻人买年货的主要方式,毕竟现在的送货速度都相当快,很多都能当天到,速度非常快,也非常方便。相反,麻烦的反而是选购年货
原来草鱼去刺这么简单,3分钟去除干净,吃鱼跟吃豆腐一样放心大家好,今天给大家分享草鱼去骨去刺的方法。草鱼处理完后,把鱼肚和鱼鳃清洗干净,这样能减少腥味。鱼肚朝前,再靠近鱼鳃的部位切开,按照视频里的方法操作,不需要切太深。在鱼尾处切一刀,不
岁岁皆欢愉事事尽可期(一)人在时间的长河里不过是沧海一粟在浩瀚的宇宙中还不如一粒微尘(二)每天不停地将一只脚放到另一只脚前面不息奔走在追光逐梦的路上(三)日子都是崭新的请不要回头看直面人生憧憬未来想己
五十岁以后,劝自己每个人都是独立的。不要妄图干涉别人的事情!每天对自己讲一遍!不生气,不着急!身体健康是自己的,从今往后,这是最重要的事情。过自己的生活,随性而活!不喜欢做的事情,坚决不做。不为难自
句句扎心的伤感说说文案导语本文是由小红文案馆珍藏的句句扎心的伤感说说文案分享给大家。句句扎心的伤感说说文案(图文1)1以前我以为钱可以买到一切,但是后来我发现,我钱不够。2下次是哪次?改天是哪天?以后是
世上无绝对,绝对由我对观音山上观山水,现世风中现风云!观音是观世音菩萨,现世是现世佛释迦牟尼即如来佛祖。前者观世后者现世,都同是佛界至尊。观音山是指东莞樟木头一个旅游景区的地理名字,现世风是指观音山山上