深入理解JVM - 阶段总结与回顾(二)
前言
上一文为:深入理解JVM - 阶段总结与回顾(一),阶段总结一主要内容为JVM的一些调优知识储备以及前面的文章回顾,这一节将会总结一些JVM常见的调优套路,帮助大家在实际情况中遇到问题的时候不必慌张,这里要特别强调一点:平时写代码要经常回顾和逻辑梳理,对于一些不确定的方法一定要点开源码进行解读。
之前有评论说思维导图更直观一些,所以后续的文章都会尽量附上思维导图节省大家的时间。
思维导图:
幕布地址:幕布思维导图
前文回顾:
上一节我们给了两个简单的案例,讲解了「FULL GC」的常见排查套路,根据排查套路可以帮助我们快速找到排查的方向并且及时的定位到真正的问题点,这对于排查线上问题是一项很重要的技能。
另外我们还讲述了String.split()
是如何让JVM频繁进行FULL GC的,我们用图表一步步分析出根本结果,并且解读源码分析了这个方法在JDK版本升级的改动,由于升级后源代码在切割字符串之后创建「SubList」对象(底层为数组)导致这一问题的产生,这里JDK要背一部分锅,因为它违反了 「向下兼容」这一规则,但是更多情况下是由于开发人员对于方法了解程度不够,最终导致代码逻辑产生内存泄漏!
以上就是上一节的所有内容。
阶段总结:
深入理解JVM - 解读GC日志
主要内容还是以讲解如何阅读日志,同时不同的机器运行的结果不同,文章更多的是介绍如何解读参数。
深入理解JVM - 实战JVM工具(上)
这篇文章主要介绍一下常用的JVM工具,当然介绍这些工具是没有意义的,因为不去使用吃个饭基本就会忘光,所以这篇文章主要为使用工具实操一下大致如何监控和调优代码。
深入理解JVM - 实战JVM工具(下)
- 介绍三个JVM调优的案例,一步一步分析问题和解决办法。
- 总结分析思路和解决流程,自我思考和反思。
- 总结和个人感想。
深入理解JVM - 案例实战
- 排查Full Gc的套路是什么,这里用一个电商案例来进行说明。
- spilt()方法是如何造成内存泄露的?如何通过可视化图形分析出问题。以及如何从源代码层面发现根本问题
概述
- 第二阶段的文章回顾和总结
- 你真的熟悉老年代么?对象在什么时候会进入到老年代?老年代的GC又是如何触发的
- 频繁的FULL GC通常有什么原因?这里列举一些常见的分析手法。
- 优化JVM需要注意哪些点?同时需要注意JVM的优化有哪些内容
你真的熟悉老年代回收么?
传统的JVM模型采用固定分代的形式,首先我们来回顾一下老年代的回收触发条件。
对象什么时候进入老年代
新生代是如何回收内存的?
讲述老年代回收之前,我们回顾一下新生代是如何分配内存的,没错,就是采用的 「改良复制算法」,新生代使用的是eden+2个survior区域进行内存的布局,默认情况下是8:1:1,这个值是可以改变的,我们可以使用参数:「-XX:SurvivorRatio=8」进行改变,最后我们来看下他的结构图:
改良复制算法在eden区域占满之后,触发一次YGC,会把存活对象复制到S1区域,之后清空掉整个eden区,这个过程是非常快的,而到了下一次YGC,会把S1中的存活对象和Eden区域的存活对象复制到S2区域,并且清空掉S1区域和EDEN区域。所以新生代回收的特点是:「内存分为三块区域,每次只使用其中的两块,留存一块作为备用切换」
以上是新生代的回收流程。当新生代触发YGC的时候,存活对象根据动态判断条件会进入老年代,那么老年代的FULL GC是如何处理的?他的触发条件是什么?这里的内容要反复回顾,因为十分的重要:
- 一个对象躲过了15次垃圾回收,年龄一到就进入老年代
- 对象超过新生代的总大小,超过一定的阈值,会直接往老年代分配
- 一次Young GC之后存活对象太多了,由于Survior区域无法存放,这批对象直接进入老年代
- 对象进入到Survior区域之后,Survior突然发现对象的占用内容超过50%,此时会根据年龄排序,把大于Survior区域的50%的年龄N的对象进入老年代,比如年龄2,3,4的对象,年龄为3的对象占比超过50%,意味着年龄3、4会进入老年代。
老年代GC是如何触发的?
老年代的FULL GC如何触发呢,在之前的文章有表格讲述触发老年代的时间,这里再次总结一下:
- 在CMS收集器,老年代自身有一个阈值,在JDK6之后默认是占满老年代空间92%之后将会进行触发。但是需要注意的是这个百分比「不是固定的」,在JVM中会根据实际的老年代占用情况提前完成垃圾回收。
- YGC之前,会先「判断老年代最大连续可用内存空间大小是否大于新生代历次进入老年代的平均大小」,如果不符合要求会在YGC之前触发一次FULL GC,回收掉一部分老年代对象,然后执行YGC。
- 如果YGC存活对象太多,Survior区域放不下,如果要放入老年代,要是此时老年代也放不下,就会触发Full GC,回收老年代一批对象,再把这些年轻代的存活对象放入老年代中。
正常情况下一般多少次GC:
正常情况下FULL GC的频率在几十分钟一次,几小时一次,这个GC频率还算是比较可以接受的,当然每次FULL GC最好在几百毫秒内。
如果线上的系统有这个表现基本不需要太关心优化的问题。
如何观察JVM内存模型:
分析手法:
这里可以按照下面的过程进行分析:
- EDEN区域的对象增长速率有多快?
- YGC的频率有多高
- 一次YGC多长的耗时
- 老年代增长的速率多高
- Full GC频率多高
- 一次Full GC耗时
频繁FULL GC的几种表现:
- 机器CPU负载高
- 频繁FULL GC报警
- 系统无法处理请求或者过慢。
频繁FULL GC一般有哪些原因:
之前讲述的案例比较多,这里集中讲解:
- 系统承载高并发请求,或者处理数据量过大,导致YGC频繁,YGC过后对象太多,内存分配不合理,Survior区域过小,「对象频繁进入老年代」,频繁FULL GC
- 系统短时间加载大量的对象,出现很多「短命大对象」,并且新生代无法存放只能进入老年代,必然频繁FULL GC
- 内存泄露,莫名其妙「创建大量对象」,导致对象无法回收,并且占用老年代无法回收,也会触发FULL GC
- 永久代「加载过多类」导致的FULL GC,多数情况是由于反射的错误使用导致。
- 误调用「System.gc()」
优化JVM应该注意点:
- 最好有一套JVM通用模板:这里不是指随便百度一套模板就拿来用,而是要根据当前的系统分析业务系统会产生多少对象名字啊什么时候会产生FULL GC,新生代要分配多少内存,老年代要分配多少内存
- 不要随便DUMP日志:自己试验可以随便用,但是一旦到了线上环境,不仅需要找运维沟通,使用这个命令需要在业务访问流量的低峰的时候再使用,需要非常小心谨慎的对待。
写在最后
JVM的第二个阶段算是完成了,后续的文章内容将会围绕分区溢出以及实际案例中存在的问题进行讲解。