常见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 等
这个问题虽然初级,但是发生的概率还真不小,尤其是在一些规范不太健全的情况下。
爆雷!又一A股被立案调查,2万股民要懵了?中国基金报记者南深11月8日晚,国瑞科技公告,于2022年11月8日收到立案告知书(编号证监立案字0382022050号),因公司涉嫌信息披露违法违规,根据中华人民共和国证券法中华
神农氏炎帝为了人民的幸福一直不停探索着神农氏是中国古代汉族神话传说中的人物。作为五氏出现的最后一位神祇,他的出现以后,结束了一个时代。因以农业为主,他的部落称神农部落。中华大地经过三年的辛勤努力,中华世界有了很大的进步
太拼了!黄珊珊晒怀孕大肚照谈少子化建立友善托育环境比发钱更重要据台媒报道,前台北市副市长黄珊珊今(9)天公布20年前的怀孕大肚照,指出这几天看到小朋友在幼儿园被不当管教,回想台北市政府做了很多努力,包括强制业者安装监控,畅谈她要让市民有安心养
访谈为什么说高质量发展是首要任务访谈嘉宾关成华北京师范大学创新发展研究院院长教授张俊伟国务院发展研究中心宏观经济研究部研究员罗来军中国人民大学经济学院教授博士生导师人民论坛网(记者王卓怡曲统昱)大国经济,举世瞩目
中国空间站梦天实验舱8个科学柜陆续开机即将开始在轨测试随着梦天实验舱成功发射升空并顺利完成交会对接和转位,中国空间站T字基本构型在轨组装完成。目前,梦天实验舱8个科学实验柜陆续加电开机,即将开始在轨测试。在梦天实验舱装载的8个科学实验
收音机测评德生H501x便携式短波旗舰收音机详实测试报告出炉德生H501x便携式短波收音机综述我最近在SWLingPost上搜索我对德生H501x的评论,想把它发给读者,这时我意识到我还没有在这里发表!让我们解决这个问题以下对德生H501x
求求你,别再读错我的姓!尉迟(wich)恭,你好!你说什么?有胆你再说一遍!生活中,你肯定会碰到这样的情况念错了别人的名字,尚可求得原谅可如果念错了别人的姓那就尴尬咯中国有几百个姓氏有的是多音字有的是生僻
早安日历每日一签11062022年11月06日,星期天,农历十月十三,霜降第十五天。有些风景,如果你不站在高处,你永远体会不到它的魅力,有些路,如果你不去启程,你永远不知道它是多么的美丽!无论生活有多艰难
三缸奇骏获2022沃德十佳动力,比亚迪没上榜,国产超车没成功?文宝拉2022沃德十佳动力公布,和以往不同的是,今年沃德十佳动力中燃油发动机仅有3个席位,剩下的都被纯电车型或者插电车型占据,这可能也是我们熟知的沃德十佳发动机变更为沃德十佳动力的
别马云君夫维马君,国之猗陶。其躬纤短,其貌寝亟。其形萎缩,其状骇瞩。其言煌煌,其质灿灿。黔首闭塞,劳顿仍贫。闻君哲语,襟畅神怡。曩者往昔,岁月青葱。啸聚人物,肇创根基。一十九人,筚路蓝缕。
人生都是命,半点不由人夜已深深,寒气渐浓,全城仿佛都在寂静中沉睡,没有一丝声息,妻儿进入了梦乡,以往大街上车水马龙的喧嚣声这时都没有了声音,马路上没有车辆,也没有行人,夜安静的让人压抑烦躁。窗外灯火孤独