JVM实战高手
2022-06-17 22:18:36 0 举报
AI智能生成
JVM是中、高级程序员修炼的必备知识,通过思维导图的形式对JVM的各种内部原理进行剖析解读,即使JVM小白,看得懂,学得会。
作者其他创作
大纲/内容
JVM核心知识图解<br>(精华提炼)
JVM类加载机制
类加载过程
加载
主要负责查找并且加载类的二进制数据文件,其实就是class文件
连接
验证
根据Java虚拟机规范,校验“.class”字节码文件的内容
准备
给类变量(static修饰的变量)分配内存空间,并默认一个初始值<br>
解析
把符号引用替换为直接引用
初始化
干什么
执行类初始化的代码,如:<br>1. 类变量的赋值(代码编写阶段给定的值)<br>2. static静态代码块
初始化规则(初始化时机)
<ol><li>包含“main()”方法的主类,必须立马初始化<br></li><li>比如通过“new XXXObject()”来实例化类的对象时<br></li><li>如果初始化类时,发现父类没有初始化,那么优先加载并初始化父类<br></li></ol>
初始化时机
<ol><li>当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)<br></li><li>当调用某个类的静态方法时<br></li><li>当使用某个类或接口的静态字段时<br></li><li>调用JavaAPI中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时<br></li><li>当初始化某个子类时<br></li><li>当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)<br></li></ol>
类加载器
启动类加载器
负责加载Java安装目录下的“lib”目录中的核心类库
扩展类加载器
负责加载Java安装目录下的“lib\ext”目录中的类
应用类加载器
负责加载“classpath”环境变量所指定的路径中的类,即你写的那些Java代码
自定义类加载器
自定义类加载器,根据需求加载你的类
双亲委派机制
对于一个加载类的具体请求,首先要委派给自己的父类加载器去加载(直到传导给顶层的类加载器),如果父类加载器无法完成加载请求时子类加载器才会尝试加载,这就叫“双亲委派”
作用:<br>防止重复加载同一个“.class”<br>
JVM内存区域
内存划分
<font color="#e57373">线程共享:堆空间、元数据区</font>
<font color="#e57373">线程私有:Java虚拟机栈、程序计数器、本地方法栈</font>
对象的内存分配过程
https://blog.csdn.net/qq_43332570/article/details/113365866
对象在内存中的分布
https://developer.aliyun.com/article/915706
JVM参数配置
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:CMSInitiatingOccupancyFaction=92 <br>-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 <br>-XX:+CMSParallelInitialMarkEnabled<br>-XX:+CMSScavengeBeforeRemark <br>-XX:+DisableExplicitGC -XX:+PrintGCDetails -Xloggc:gc.log <br>-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom
<font color="#e57373">JVM参数配置说明:</font><br>首先是各个内存区域的大小分配,这个是需要你精心调优的<br>其次是两种垃圾回收器的指定<br>接着是常规性的CMS垃圾回收参数,可以帮助优化偶尔发生的Full GC性能<br>然后就是要打印出GC日志,分析每次GC的具体情况<br>最后就是在OOM的时候需要自动dump内存快照<br>
CMS垃圾回收器参数
“-XX:CMSInitiatingOccupancyFaction=92”,老年代空间使用超过92%时,自行触发Full GC
“-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0”,每次Full GC后都整理一下内存碎片
“-XX:+CMSParallelInitialMarkEnabled”,这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行
“-XX:+CMSScavengeBeforeRemark”,这个参数会在CMS的“重新标记”阶段之前,先尽量执行一次Young GC
JVM内存设置举例
2核4G机器<br>有点紧凑
JVM进程最多就是2G内存,<br>但是这2G还得分配给元空间、栈内存、堆内存几块区域,<br>那么堆内存最多也就个1G多的内存空间,然后还得分给新生代和老年代,<br><b>最后新生代可能也就几百MB的内存了</b>
导致运行几百秒之后,新生代内存空间就满了,<br>然后频繁的触发Young GC,影响线上系统的性能稳定性<br>
4核8G机器
JVM进程至少可以分配4G以上内存,<br>新生代至少也可以分配到2G内存空间<br>
大大降低GC的频率
举个例子:<br>-Xms和-Xmx设置为3G,给整个堆内存3G内存空间,<br>-Xmn设置为2G,给新生代2G内存空间<br>
JVM垃圾回收机制
对象什么时候被回收
可达性分析法
以“GC Roots”为根节点,从根节点开始,根据引用关系向下搜索,<br>如果某个对象到GC Roots之间没有任何引用关系,则该对象就不可能再被使用了。
哪些内容可以作为GC Roots
方法的参数、局部变量、临时变量等
实际上就是虚拟机栈-栈帧中的本地变量表中引用的那些对象
类的静态属性引用的对象
常量引用的对象
如字符串常量池中引用的对象
被同步锁(synchronized)持有的对象
本地方法栈中引用的对象
JVM内部的引用,如:基本数据类型对应的Class对象、异常对象(NullPointExcepiton、OutOfMemoryError)、系统类加载器等
“非死不可”?
即使对象被判定为不可达的对象,也不是“非死不可”的
finalize()方法
它是Object类中提供的一个方法,<br>在GC准备释放对象所占用的内存空间之前,首先会调用该方法<br>
finalize()方法是对象逃脱死亡命运的最后一次机会。
Java引用类型
强引用(StrongReference)
平时创建对象和数组时的引用。强引用在任何时候都不会被垃圾收集器回收
软引用(SoftReference)
系统内存紧张时才会被JVM回收
弱引用(WeakReference)
一旦JVM执行垃圾回收操作,弱引用就会被回收
<font color="#e57373">ThreadLocal中的key就是弱引用</font>
虚引用(PhantomReference)
它是所有引用类型中最弱的一种。一个对象是否关联到虚引用,完全不会影响该对象的生命周期,也无法通过虚引用来获取一个对象的实例。
为对象设置一个虚引用的唯一目的是在该对象被垃圾收集器回收的时候能够收到系统通知。
<font color="#e57373">频繁垃圾回收问题</font>
在系统高峰期,可能会出现系统请求处理缓慢,<br>然后导致新生代中积压了很多对象,回收缓慢,频繁触发 Young GC,多次 Young GC后对象被转移到老年代,<br>一旦老年代的对象越来越多,造成频繁触发老年代垃圾回收,极大的影响系统性能。
垃圾回收算法
标记-清除算法<br>(最基础的收集算法)
标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。<br>这是一种最基础的收集算法,后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而得到的。<br>
最大的缺点:内存空间的碎片化问题
标记-复制算法<br>(新生代)<font color="#e57373"></font>
它实际上就是将还存活着的对象复制到空闲区
新生代垃圾回收原理
新生代被分成一个Eden区,两个Survivor区,默认比例为8:1:1
工作过程:<br>刚开始对象都分配在Eden区,如果Eden区快满了,触发Young GC,<br>此时会把Eden区存活的对象复制到空的Survivor0区,<br>接着清空Eden区,然后继续分配对象到Eden区,如果Eden区快满了,再次触发YoungGC,<br>此时会把Eden区和Survivor0区的存活对象,复制到Survivor1区,并清空Eden区和Survivor0区<br>就这样一直循环使用这三块区域
整个垃圾回收的过程中全程会进入Stop the Wold状态,也就是暂停系统工作线程
标记-复制算法在对象存活率较高时还要进行大量的复制操作,效率将会降低。所以这种算法不适合老年代使用
标记-整理算法<br>(老年代)
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,<br>而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,<br>而且这种对象移动操作必须全程暂停用户应用程序才能进行。
移动对象会使程序的吞吐量更大,但会使内存回收变得更为复杂
不移动对象可以使收集器的效率更高,但会增加内存分配与访问时的复杂度
垃圾回收算法应用
新生代垃圾回收时使用的就是标记-复制算法
Parallel Scavenge收集器是基于标记-整理算法的(关注吞吐量)
CMS收集器是基于标记-清除算法的(关注延迟)
垃圾收集器
JDK 8中的垃圾收集器
Serial与Serial Old垃圾收集器
Parallel Scavenger与Parallel Old垃圾收集器
JDK 8的默认垃圾收集器
ParNew和CMS垃圾收集器
ParNew收集器<br>(新生代)
主要是为了配合 CSM 的垃圾收集而提供的新生代的垃圾收集器
CMS收集器<br>(老年代)
主要目标是为了提高用户体验,即尽可能多的缩短 STW 的时间
垃圾回收的各个阶段
初始标记<br><font color="#e57373">(标记GC Roots引用对象)</font>
此阶段会让系统中的工作线程停止工作,进入“Stop the World”状态
标记出所有GC Roots直接引用的对象
并发标记<br><font color="#e57373">(追踪GC Roots,但会创建新对象)</font>
此阶段会让系统线程随意创建任意对象,继续运行,<br>此期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,成为垃圾对象。<br>垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪
其实就是对老年代所有对象进行GC Roots追踪,最耗时<br>但是此阶段是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。<br>
重新标记<br><font color="#e57373">(标记变动过的对象)</font>
再次进入“STW”状态,对在第二阶段中被系统程序运行变动过的少数对象进行标记,运行速度很快
并发清理<br><font color="#e57373">(清理标记的垃圾对象)</font>
此阶段需要清理掉之前标记的垃圾对象,非常耗时,<br>但是此阶段是跟系统程序并发运行的,所以也不影响系统程序的运行
内存碎片问题
CMS采用“标记-清理”算法,每次都是将标记出来垃圾对象一次性回收掉,<br>这样会导致大量的内存碎片产生
CMS碎片整理
“-XX:+UseCMSCompactAtFullCollection”参数,默认开启<br>作用:在Full GC后再次进入“STW”状态,进行碎片整理工作
“-XX:CMSFullGCsBeforeCompaction”参数,默认是0<br>作用:间隔多少次Full GC后,执行一次内存碎片整理工作
G1垃圾收集器
统一收集新生代和老年代
JDK9后默认的垃圾回收器
新生代、老年代如何进行垃圾回收
<font color="#e57373">新生代对象进入老年代的4钟常见条件</font>
对象年龄
动态对象年龄判断
大对象直接进入老年代
Young GC后Survivor区无法存放所有存活对象,此时部分存活对象会进入老年代
老年代空间分配担保规则
空间担保就是以<font color="#f44336"><b>历次晋升到老年代的对象的平均大小</b></font>为参考依据,尝试进行Young GC<br>如果Young GC后,剩余存活对象比老年代可用空间大,那么担保失败,直接触发Full GC,<br>要是Full GC后,老年代还是没有足够空间存放新生代的存活对象,那么就会出现“OOM”内存溢出
老年代触发垃圾回收的时机
Young GC前,检查老年代最大可用连续空间小于新生代所有对象的总空间,<br>并且<font color="#f44336">没有开启空间担保机制</font>,此时要直接触发Full GC<br>(但是一般情况下默认打开空间担保机制)
Young GC前,检查发现很可能Young GC之后要进入老年代的对象太多了(历次晋升老年代的平均值 > 老年代可用空间),<br>老年代可能放不下,此时需要提前触发Full GC,然后再进行Young GC<font color="#f44336">(不满足空间担保规则)</font>
Young GC后,发现剩余对象太多老年代放不下<font color="#f44336">(空间担保失败)</font>
JVM优化到底在优化什么<br><font color="#e57373">JVM优化的方法论</font>
系统真正的最大问题,就是因为内存分配、JVM参数设置不合理,<br>导致对象频繁进入老年代,频繁触发老年代垃圾回收,<br>最后导致系统出现频繁卡死现象<br>
所谓JVM优化,<br>就是<font color="#e57373">尽可能让对象在新生代分配和回收,<br>尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收</font><br>同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收<br>
GC日志分析及定位与解决各种JVM GC问题
面试
你们生产环境的系统的JVM参数怎么设置的?为什么要这么设置?
聊聊你在生产环境中的JVM优化经验
说说你在生产环境解决过的JVM OOM问题
优秀博文
JVM内存模型和类加载运行机制
垃圾回收器和回收策略
内存分配和垃圾回收调优
性能调优工具
1. 上帝视角看JVM
第一节<br>Java代码到底是如何运行起来的?
运行流程梳理
<ol><li>将写好的“.java”代码<b>打包编译</b>成“.class”字节码文件<br></li><li>使用诸如“java -jar”之类的命令来负责运行,使用“java”命令会启动一个<b>JVM进程</b><br></li><li>在JVM启动时或者类运行时,通过<b>类加载器</b>把“.class”字节码文件加载到JVM内存中<br></li><li>JVM基于<b>字节码执行引擎</b>,执行加载到内存中的那些类<br></li></ol>
思考:如何对“.class”文件处理保证不被别人拿到后反编译获取源码?
1. 在编译时对字节码加密,或者做混淆处理<br>也有第三方公司做的商业级字节码文件加密
2. 然后在类加载时,采用自定义的类加载器解密文件即可
第二节 <br>JVM类加载机制
什么时候从“.class”字节码文件加载类到JVM内存
当你使用到某个类时,如“StringUtils”,此时就会从“.class”字节码文件中加载对应的类到JVM内存中
类的生命周期<br>(加载 + 使用 + 卸载)
加载
主要负责查找并且加载类的二进制数据文件,其实就是class文件
连接
验证
根据Java虚拟机规范,校验“.class”字节码文件的内容
文件格式验证:包括魔数、本版号等(需要了解Class的文件结构)
元数据验证:主要就是语义分析,比如是否有父类,是否实现了父类或接口中所有要求实现的方法等
字节码验证:指令级的语义验证
符号引用验证,如通过全限定性类名是否能够找到对应的类
准备
给类变量(static修饰的变量)分配内存空间,并默认一个初始值<br>
解析
把符号引用替换为直接引用
初始化
干什么
执行类初始化的代码,如:<br>1. 类变量的赋值代码<br>2. static静态代码块
初始化规则(初始化时机)
<ol><li>包含“main()”方法的主类,必须立马初始化<br></li><li>比如通过“new XXXObject()”来实例化类的对象时<br></li><li>如果初始化类时,发现父类没有初始化,那么优先加载并初始化父类<br></li></ol>
初始化时机
<ol><li>当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)<br></li><li>当调用某个类的静态方法时<br></li><li>当使用某个类或接口的静态字段时<br></li><li>调用JavaAPI中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时<br></li><li>当初始化某个子类时<br></li><li>当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)<br></li></ol>
使用
卸载
类加载器
启动类加载器
负责加载Java安装目录下的“lib”目录中的核心类库
扩展类加载器
负责加载Java安装目录下的“lib\ext”目录中的类
应用类加载器
负责加载“classpath”环境变量所指定的路径中的类,即你写的那些Java代码
自定义类加载器
自定义类加载器,根据需求加载你的类
双亲委派机制
什么意思呢?
对于一个加载类的具体请求,首先要委派给自己的父类加载器去加载(直到传导给顶层的类加载器),如果父类加载器无法完成加载请求时子类加载器才会尝试加载,这就叫“双亲委派”<br>
具体的委派逻辑在java.lang.ClassLoader类的loadClass()方法中实现。
作用:<br>防止重复加载同一个“.class”<br>
双亲委派模型图示
拓展阅读:<br>JVM的双亲委派机制<br>
思考:Tomcat的类加载机制应该怎么设计,才能把我们动态部署进去的war包中的类,<br>加载到Tomcat自身运行的JVM中,然后去执行那些我们写好的代码呢?
思路:<br>1. Tomcat自定义了很多类加载器;<br>2. Tomcat打破了双亲委派机制,<br> 每个WebApp负责加载自己对应Web应用的class文件,<br> 不会传导给上层类加载器去加载
tomcat需要破坏双亲委派模型的原因:<br><ol><li>tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离;<br></li><li>同一个第三方类库的相同版本在不同web应用可以共享<br></li><li>tomcat自身依赖的类库需要与应用依赖的类库隔离<br></li><li>jsp需要支持修改后不用重启tomcat即可生效为了上面类加载隔离和类更新不用重启,定制开发各种的类加载器<br></li></ol>
Tomcat类加载机制
Common、Catalina、Shared等自定义类加载器,<br>用来加载Tomcat自己的核心基础类库
WebApp类加载器,<br>每个部署的Web应用都有一个对应的WebApp类加载器
JSP类加载器
第三节<br>JVM中有哪些内存区域,及其作用
到底什么才是JVM的内存区域划分
栈空间(线程私有)<br><font color="#e57373">随着线程的创建而创建,随着线程的结束而死亡</font><br>
程序计数器
记录当前执行的字节码指令的位置
Java虚拟机栈
每次函数调用都会创建对应的<b><font color="#ff0000">栈帧</font></b>
局部变量表
一组变量值存储空间,用于存放方法参数和方法内定义的局部变量
局部变量可以保存的数据类型
boolean
byte
char
short
int
float
reference
表示对一个对象实例的引用
returnAddress
操作数栈
动态链接
方法返回地址
每次函数调用结束后都会有一个栈帧被弹出
return或抛出异常,不管以哪种返回方式结束都会导致栈帧被弹出
本地方法栈
调用native方法
调用本地操作系统中的底层类库
堆空间(所有线程共享)
会存放几乎所有的对象及数组
由于对象或数组会不断地创建和死亡,所以这是Java垃圾收集器收集的主要区域
Java将堆空间划分为<font color="#f44336">年轻代堆空间</font>和<font color="#f44336">老年代堆空间</font>,后面内容中详细介绍<font color="#000000"></font><font color="#000000"></font>
直接内存
<font color="#e57373">直接内存并不是Java虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域</font>
直接内存的分配不会受到Java堆的限制,但既然是内存,就会受到本机总内存及处理器寻址空间的限制
元空间也叫Metaspace<br>
<font color="#e57373">元空间使用的就是直接内存</font>
元空间位于堆外,因此它的内存大小取决于系统内存而不是堆大小,<br>当然也可以使用MaxMetaspaceSize参数来限定它的最大内存
元空间主要用来存放类的元数据信息
元数据信息:用于记录一个Java类在JVM中的信息
Klass结构
可以理解为类在HotSpot VM内部的对等表示
Method与ConstMethod
保存Java方法的相关信息,包括方法的字节码、局部变量表、异常表和参数信息等
ConstantPool
保存常量池信息
注解
提供与程序有关的元数据信息,但是这些信息并不属于程序本身
方法计数器<br>
记录方法被执行的次数,用来辅助JIT决策
其他一些占用内存比较小的元数据信息
类的元数据信息不是一个Java对象,<br>它不在堆中而是在Metaspace中
例如:保存类的元数据信息的Klass、Method、ConstMethod与ConstantPool等实例<br>都是在元空间上分配内存
每个类加载器都会在元空间得到自己的存储区域,<br>当一个类加载器被垃圾收集器标记为不再存活时,这块存储区域将会被回收。
HotSpot VM负责管理元空间的内存,包括分配、回收及释放内存等
堆外内存
JDK 1.4中新加入的NIO类引入了一种基于通道(Channel)与缓存区(Buffer)的I/O方式
通过DirectByteBuffer对象作为这块内存的引用进行操作,在一些使用场景中可以显著的提升性能
核心内存区域的全流程串讲(详见专栏内容)
思考:在Java堆内存中存放的对象实例,到底会占用多少内存?<br>一般怎么来计算和估算系统创建的对象对内存占用的压力呢?
一个对象对内存空间的占用,大致分为两块:<br>1. 一个是对象自己本身的一些信息<br>2. 一个是对象的实例变量作为数据占用的空间
第四节<br>为什么要垃圾回收?<br>
一个方法执行完毕后会怎么样?
1. 当方法里的代码执行完毕,那么方法就执行完毕了,<br>那么此时就会把方法对应的栈帧从当前线程的Java虚拟机栈里<b><font color="#ff0000">出栈</font></b><br>
2. 一旦方法的栈帧出栈,那么那个栈帧里的局部变量也就没有了<br>
<font color="#ff0000">在Java堆内存里创建的对象,都是占用内存资源的,而且内存资源有限。</font>
不再需要的那些对象应该怎么处理?
没有任何方法的局部变量在引用某个实例对象,但是他还占着内存资源,那么我们应该怎么处理呢?
JVM的垃圾回收机制
它是一个后台自动运行的线程,<br>这个线程会在后台不断检查JVM堆内存中的各个实例对象。<br>那些不再被引用的对象实例,即JVM中的“垃圾”,<br>就会定期的被后台垃圾回收线程清理掉,不断释放内存资源<br>
思考:加载到方法区的类会被垃圾回收吗?什么时候被回收呢?
方法区中的类会被回收,满足以下条件就可以被回收:<br>首先该类的所有实例对象都已经从Java堆内存中被回收;<br>其次加载这个类的ClassLoader已经被回收;<br>最后,对该类的Class对象没有任何引用
章节思考
实例变量是和类变量一同在初始化阶段赋值的吗?
实例变量得在你创建类的实例对象时才会初始化
类在初始化之后是不是就有实例了呢?
类的初始化阶段,仅仅是初始化类而已,跟对象无关,<br>用new关键字才会构造一个对象出来
每个线程都有Java虚拟机栈,里面也有方法的局部变量等数据,<br>这个Java虚拟机栈需要进行垃圾回收吗?为什么?
其实栈帧出栈后,里面的局部变量直接就从内存中清理掉了。<br>另外,JVM中的垃圾回收针对的是新生代、老年代、方法区(永久代),不会针对方法的栈帧
作业:<br>写一段代码,把代码运行时候,<br>JVM的整个工作原理画出来
执行多个方法的调用时,如何把方法的栈帧压入线程的Java虚拟机栈?
栈帧里如何放局部变量?
如何在Java堆里创建实例对象?
如何让局部变量引用那个实例对象?
方法运行完之后如何出栈?
垃圾回收是如何运行的?
2. JVM内存空间配置
第一节<br>JVM分代模型
对象生存周期
大部分对象都是存活周期极短的
方法内创建的对象,分配在Java堆内存后,迅速使用完就会被垃圾回收<br>
只有少数对象是长期存活在<b>堆内存</b>的
需要一直生存在Java堆内存里,让程序后续不停的去使用
JVM的分代模型
JVM将<b>Java堆内存</b>划分为了两个区域
<b>年轻代</b>
通常情况下,对象被创建后会优先进入到年轻代。
该区域主要存放的是创建、使用完之后立马就要回收的对象
<b>老年代</b>
存放的是创建完以后一直长期存在的对象
为什么需要这么区分呢?
根据对象存活时间的差异,需要使用不同的垃圾回收算法回收对象,<br>所以需要区分不同区域来存放不同的对象
什么是永久代?
在JDK8之前的版本中,称其为<b>永久代(方法区)</b>
在JDK8以后,该区域叫“Metaspace”,即元空间
第二节<br>对象在JVM内存中如何分配?如何流转?
大部分正常对象都<b>优先在新生代分配内存</b>
到底什么情况下会触发<b>新生代的垃圾回收</b>?
默认触发条件
需要分配新的对象到新生代时,<br>发现新生代内存空间不足
新生代内存空间的垃圾回收,称之为“Minor GC”,<br>有的时候也叫“<b>Young GC</b>”
长期存活的对象会躲过多次垃圾回收
10多次垃圾回收之后,还没被回收掉,<br>那么它将被转移到Java堆内存的老年代中去
这个次数是可以通过参数配置的
老年代里的那些对象会被垃圾回收吗?
答案是肯定的,也需要被垃圾回收
<i>其他一些复杂机制</i>
<i>新生代垃圾回收之后,因为存活对象太多,导致大量对象直接进入老年代</i>
<i>特别大的超大对象直接不经过新生代就进入老年代</i>
<i>动态对象年龄判断机制</i>
<i>空间担保机制</i>
思考:看一看你负责的系统中短生存周期的对象都有什么,长生存周期的对象都有什么
第三节<br>如何设置JVM内存大小
JVM的核心参数
-Xms:Java堆内存的大小
-Xmx:Java堆内存的最大大小
-Xmn:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
<strike>-XX:PermSize:永久代大小<br>-XX:MaxPermSize:永久代最大大小</strike><br>
JDK 1.8以前的版本
-XX:MetaspaceSize:元空间大小<br>-XX:MaxMetaspaceSize:元空间最大大小<br>
JDK 1.8及以后的版本
-Xss:每个线程的栈内存大小
参数设置实例
java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M -jar App.jar
思考:Tomcat、Spring Boot部署启动系统的时候,JVM参数如何设置?
Tomcat是在bin目录下的catalina.sh中加入JVM参数
Spring Boot可以在启动的时候加上JVM参数
第四节<br>如何设置JVM堆内存大小
系统分析<br>以每日百万交易的支付系统中的核心业务流程为例
系统的压力到底集中在哪里呢?
架构层面
高并发访问
高性能处理请求
大量支付单数据需要存储
<b>JVM层面</b>
每天JVM内存里面会频繁的创建和销毁100万个支付单
系统每秒钟需要处理多少笔支付订单?
大致估算
<ol><li>根据2/8法则,大部分请求发生在一天的某些时间段,比如中午的12-13点以及晚上的18-19点<br></li><li>80万请求 / (2 * 3600) ≈ 111,即<b>每秒请求100+</b><br></li></ol>
系统部署
假设系统部署3台机器,则处理能力大概是<b>30笔/台/秒</b>
每个支付订单处理要耗时多久?
订单支付请求处理完毕,引用回收,则这些订单在JVM的新生代里就是没人引用的垃圾对象
请求耗时越长,对象在新生代存在时间就越长
每个支付订单大概需要多大的内存空间?
直接根据类中的实例变量的类型计算就可以
例如:Integer类型就是4个字节;Long类型就是8个字节<br>等其他类型
假设支付订单对象占据500字节的内存空间
每秒发起的支付请求需要占用多少内存?
每秒30个支付订单,大概占据的内存空间就是 30 * 500字节 = 15000字节<br>大概15Kb左右
系统内存占用预估
<b>前面的估算只是针对一个核心业务流程来分析的<br>其实真实系统运行中,肯定还会创建其他的对象,<br>可以根据实际情况将计算结果扩大10~20倍</b>
<ol><li>那么每秒钟创建出来的被栈内存的局部变量引用的对象大致占据的内存空间就在几百KB~1MB之间。<br></li><li>然后下一秒继续来新的请求创建大概1MB的对象放在新生代里,接着变成垃圾,再来下一秒。<br></li><li>循环多次之后,新生代里垃圾太多,就会触发Young GC回收掉这些垃圾。<br></li></ol>这就是一个完整系统的大致JVM层面的内存使用模型。<br>
支付系统的JVM堆内存应该怎么设置?
2核4G机器<br>有点紧凑
JVM进程最多就是2G内存,<br>但是这2G还得分配给方法区、栈内存、堆内存几块区域,<br>那么堆内存最多也就个1G多的内存空间,然后还得分给新生代和老年代,<br><b>最后新生代可能也就几百MB的内存了</b>
导致运行几百秒之后,新生代内存空间就满了,<br>然后频繁的触发Young GC,影响线上系统的性能稳定性<br>
4核8G机器
JVM进程至少可以分配4G以上内存,<br>新生代至少也可以分配到2G内存空间<br>
大大降低GC的频率
举个例子:<br>-Xms和-Xmx设置为3G,给整个堆内存3G内存空间,<br>-Xmn设置为2G,给新生代2G内存空间<br>
频繁垃圾回收问题
在系统高峰期,可能会出现系统请求处理缓慢,<br>然后导致新生代中积压了很多对象,回收缓慢,频繁触发 Young GC,多次 Young GC后对象被转移到老年代,<br>一旦老年代的对象越来越多,造成频繁触发老年代垃圾回收,极大的影响系统性能。
第五节<br>如何设置JVM栈内存与元空间大小
如何合理设置元空间大小
一般设置几百MB
如何合理设置栈内存大小
一般默认就是比如512KB到1MB
<i>栈内存什么时候会发生内存溢出</i>
3. JVM中的垃圾回收算法
第一节<br>对象什么时候会被垃圾回收
系统运行创建的对象优先分配在新生代,<br>当新生代对象越来越多,快满了的时候,<br>会触发垃圾回收,把没人引用的对象回收掉,<br>释放内存空间<br>
哪些对象可以回收,哪些对象不能回收呢?<br>
可达性分析法<br>
是什么<br>
以“GC Roots”为根节点,从根节点开始,根据引用关系向下搜索,如果某个对象到GC Roots之间没有任何引用关系,则该对象就不可能再被使用了。
哪些内容可以作为GC Roots
方法的参数、局部变量、临时变量等
实际上就是虚拟机栈-栈帧中的本地变量表中引用的那些对象
类的静态属性引用的对象
常量引用的对象
如字符串常量池中引用的对象
本地方法栈中引用的对象
JVM内部的引用,如:基本数据类型对应的Class对象、异常对象(NullPointExcepiton、OutOfMemoryError)、系统类加载器等
被同步锁(synchronized)持有的对象
<b style=""><font color="#ff0000">注意:类的实例变量不是GC Roots</font></b>
“非死不可”?
即使对象被判定为不可达的对象,也不是“非死不可”的
finalize()方法
它是Object类中提供的一个方法,<br>在GC准备释放对象所占用的内存空间之前,首先会调用该方法<br>
finalize()方法是对象逃脱死亡命运的最后一次机会。
Java引用类型
强引用
所谓强引用,就是我们最常见的普通对象引用,<br>如:Object a = new Object();<br>
只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。
软引用<br>SoftReference<br>
内存充足时垃圾回收器不会回收它,而在内存不足时才会回收它
软引用非常适用于创建缓存
弱引用<br>WeakReference
无论内存充足与否,垃圾回收器都会回收它
<b><font color="#ff0000">ThreadLocal的key就是弱引用</font></b>
虚引用<br>PhantomReference
任何时候都可能被回收
第二节<br>新生代垃圾回收机制
“半区复制”算法
把新生代划分为两块区域,使用其中一块区域存放对象,<br>当该区域快满发生垃圾回收时,将存活的对象复制到未使用的区域,<br>然后对之前的区域进行全部回收
这种方式内存空间使用率低,需要进行分区优化
新生代区域划分
分成一个Eden区,两个Survivor区,<br>默认比例为8:1:1
优点:内存空间利用率高,避免出现内存碎片
Young GC过程
在进行Young GC前,首先会检查老年代最大可用连续空间是否大于新生代所有对象的总空间<br><font color="#b71c1c">(空间担保机制)</font>
<font color="#e57373">工作流程:</font><br>刚开始对象都分配在Eden区,如果Eden区快满了,触发Young GC,<br>此时会把Eden区存活的对象复制到空的Survivor0区,<br>接着清空Eden区,然后继续分配对象到Eden区,如果Eden区快满了,再次触发YoungGC,<br>此时会把Eden区和Survivor0区的存活对象,复制到Survivor1区,并清空Eden区和Survivor0区<br>就这样一直循环使用这三块区域
整个垃圾回收的过程中全程会进入Stop the Wold状态,也就是暂停系统工作线程
新生代垃圾回收的各种“万一”怎么处理?
万一垃圾回收过后,存活下来的对象在另外一块Survivor区域中放不下咋整?
万一我们突然分配了一个超级大的对象,新生代找不到连续内存空间来存放,此时咋整?
到底一个存活对象要在新生代里这么来回倒腾多少次之后才会被转移都老年代去?
第三节<br>老年代垃圾回收机制
新生代对象进入老年代的4钟常见条件
对象年龄
躲过15次Young GC后进入老年代
可以通过“-XX:MaxTenuingThershold”参数设置
默认值为15
最大也只能是15
Java对象的年龄是存储在对象头中的,在Java对象头中只有4bit用来表示对象年龄。<br>
4位2进制最大可以表示到15
动态对象年龄判断
将所有对象按年龄从小到大进行累加,如果累加和大于Survivor空间的一半,<br>那么年龄大于或等于该年龄的对象就可以直接进入老年代,<br>无须等到“-XX:MaxTenuingThershold”中要求的年龄。
大对象直接进入老年代
可以通过“-XX:PretenureSizeThreshold”参数设置<br>单位:字节数,比如“1048576”字节,就是1MB
尽量避免大对象在内存里来回复制
Young GC后存活对象大于Survivor区内存空间,<br>部分对象进入Survivor区,其余存活对象就直接进入老年代
老年代空间分配担保规则
Young GC前,<br><b>检查老年代最大可用连续空间</b><br><font color="#f44336">是否大于</font><br><b>新生代所有对象的总空间</b>
小于时<br>处理空间担保机制
JDK1.7以前<br>看JVM是否开启了空间担保机制<br>可以通过“-XX:-HandlePromotionFailure”参数设置
没有空间担保,则直接<font color="#ffb74d">触发一次Full GC</font>
有空间担保
JDK1.7及以后<br>默认开启空间担保
<b>检查老年代最大可用连续空间</b><br><font color="#f44336">是否大于</font><br><b>历次晋升到老年代的对象的平均大小</b>
大于历次平均值,<br>则直接<font color="#e65100">触发一次Full GC</font>
小于历次平均值,<br>冒险尝试进行Young GC
Young GC后<br>可能出现如下三种情况
存活对象的大小 < Survivor区大小<br><font color="#000000"></font>
Survivor区大小 < 存活对象的大小 < 老年代可用空间<br>
Survivor区大小 < 存活对象的大小 && 老年代可用空间 < 存活对象的大小
大于时,<br>则此次Young GC是安全的,即使Young GC后所有对象都存活,<br>Survivor区放不下,也可以转移到老年代去
总而言之
空间担保就是以<font color="#f44336"><b>历次晋升到老年代的对象的平均大小</b></font>为参考依据,尝试进行Young GC<br>如果Young GC后,剩余存活对象比老年代可用空间大,那么担保失败,直接触发Full GC,<br>要是Full GC后,老年代还是没有足够空间存放新生代的存活对象,那么就会出现“OOM”内存溢出
老年代触发垃圾回收的时机
Young GC前,检查老年代最大可用连续空间小于新生代所有对象的总空间,<br>并且<font color="#f44336">没有开启空间担保机制</font>,此时要直接触发Full GC<br>(但是一般情况下默认打开空间担保机制)
Young GC前,检查发现很可能Young GC之后要进入老年代的对象太多了(历次晋升老年代的平均值 > 老年代可用空间),<br>老年代可能放不下,此时需要提前触发Full GC,然后再进行Young GC<font color="#f44336">(不满足空间担保规则)</font>
Young GC后,发现剩余对象太多老年代放不下<font color="#f44336">(空间担保失败)</font>
思考:<br>梳理GC的全流程<br>
到底什么时候会尝试触发Minor GC?
触发Young GC之前会如何检查老年代大小,涉及哪几个步骤和条件?
什么时候在Young GC之前就会提前触发一次Full GC?
Full GC的算法是什么?
Young GC过后可能对应哪几种情况?
哪些情况下Young GC后的对象会进入老年代?
第四节<br>JVM中常见的垃圾回收器
OpenJDK 8中的垃圾回收器
G1收集器既可以收集年轻代的内存空间,也可以收集老年代的内存空间。<br>——《深入剖析Java虚拟机》(基础卷)
OpenJDK 8默认使用Parallel Scavenger与Parallel Old垃圾收集器
Seria和Seria Old垃圾回收器<br><b><font color="#fbc02d">基本不再使用</font></b><br>
Serial收集器
是什么
HotSpot中Client模式下<br>默认的新生代垃圾收集器
回收方式
复制算法
串行回收
STW机制
Serial Old收集器
是什么
HotSpot中Client模式下<br>默认老年代垃圾收集器
回收方式
标记-压缩算法
串行回收
STW机制
Serial Old收集器在Server模式下的两个主要用途:
与新生代的Parallel Scavenge配合使用
作为老年代CMS收集器的后备垃圾回收方案
使用“-XX:+UseSerialGC”参数开启
即年轻代用Serial GC,老年代用Serial Old GC
ParNew和CMS垃圾回收器
ParNew收集器<br>
用在新生代的垃圾回收器
主要是为了配合 CSM 的垃圾收集而提供的新生代的垃圾收集器
CMS收集器
用在老年代的垃圾回收器
主要目标是为了提高用户体验,即尽可能多的缩短 STW 的时间
G1垃圾回收器
统一收集新生代和老年代
JDK9后默认的垃圾回收器
第五节<br>“Stop the World”
在垃圾回收时,JVM会直接停止系统程序的所有工作线程,让垃圾回收线程专心进行垃圾回收的工作
章节思考
线上系统能否做到消灭Full GC,如何优化JVM参数配置
没有Full GC是有可能的,除非你做到几乎没什么对象进入老年代,那就自然不会触发Full GC
参见第三周答疑第一个<br>
4. JVM垃圾回收器的工作原理、参数设置、性能调优
第一节<br>JVM新生代垃圾回收器ParNew是怎么工作的<br>
采用“复制”算法
最常用的新生代垃圾回收器ParNew
多线程垃圾回收器<br>能够充分利用服务器多核CPU的优势<br>
启动系统时如果要指定使用ParNew垃圾收集器,该怎么配置呢
使用“-XX:UseParNewGC”参数项
ParNew垃圾回收器默认情况下的线程数量
跟CPU核数一样<br>一般不用手动调节<br>
使用“-XX:ParallelGCThreads”参数,也可以设置线程的数量
第二节<br>JVM老年代垃圾回收器CMS是怎么工作的<br>
采用“标记清理”算法
先将垃圾对象都标记出来,<br>然后一次性把垃圾对象回收掉
标记方法
先通过追踪 GC Roots的方法,看看各个对象是否被 GC Roots给引用了,<br>如果是的话,那就是存活对象,否则就是垃圾对象。
问题:会造成大量的垃圾碎片
CMS执行垃圾回收的过程
初始标记
此阶段会让系统中的工作线程停止工作,进入“Stop the World”状态
标记出所有GC Roots直接引用的对象
并发标记
此阶段会让系统线程随意创建任意对象,继续运行,<br>此期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,成为垃圾对象。<br>垃圾回收线程,会尽可能的对已有的对象进行GC Roots追踪
其实就是对老年代所有对象进行GC Roots追踪,最耗时<br>但是此阶段是跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。<br>
重新标记
再次进入“STW”状态,对在第二阶段中被系统程序运行变动过的少数对象进行标记,运行速度很快
并发清理
此阶段需要清理掉之前标记的垃圾对象,非常耗时,<br>但是此阶段是跟系统程序并发运行的,所以也不影响系统程序的运行
第三节<br>如何设置线上系统的垃圾回收相关参数
CMS垃圾回收期间的细节问题
并发回收垃圾导致的CPU资源紧张
并发标记阶段,需要对GC Roots进行深度追踪,因为老年代存活对象比较多,所以耗时较高<br>并发清理阶段,需要把垃圾对象从随机的内存位置清理掉,也比较耗时。<br>所以以上两个阶段,CMS的垃圾回收线程比较耗费CPU资源。
CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4
Concurrent Mode Failure问题
如果CMS垃圾回收期间,系统程序要放入老年代的对象<br>大于了可用内存空间,此时会如何?
这个时候,会发生Concurrent Mode Failure
此时就会自动用“Serial Old”垃圾回收器替代CMS,<br>也就是直接将系统程序进入到“STW”状态,重新进行GC Roots追踪,标记出垃圾对象,<br>然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。
设置老年代的预留空间
保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代
“-XX:CMSInitiatingOccupancyFaction”参数,JDK 1.6里面默认的值是92%
内存碎片问题
CMS采用“标记-清理”算法,每次都是将标记出来垃圾对象一次性回收掉,<br>这样会导致大量的内存碎片产生
CMS碎片整理
“-XX:+UseCMSCompactAtFullCollection”参数,默认开启<br>作用:在Full GC后再次进入“STW”状态,进行碎片整理工作
“-XX:CMSFullGCsBeforeCompaction”参数,默认是0<br>作用:间隔多少次Full GC后,执行一次内存碎片整理工作
触发老年代GC的时机
触发老年代空间担保规则(三种情况),见第三部分 第三节
内存使用率超过92%,也需要直接触发Full GC<br>可以通过“-XX:CMSInitiatingOccupancyFaction”参数设置
第四节<br>如何优化新生代垃圾回收参数
案例背景
单日上亿请求的电商系统<br>部署3台4核8G的机器<br>
内存使用模型预估
主要业务的吞吐量
业务实例占用空间大小预估
每秒种内存开销
内存到底如何分配
内存分配方案
堆内存3G(新生代1.5G,老年代1.5G),那么默认情况下Eden区1.2G,Survivor区150M<br>Java虚拟机栈1M,几百个线程大概会有几百M<br>元数据区(永久代)256M
<font color="#f57f17">JVM参数:<br>-Xms3072M -Xmx3072M -Xmn1536M <br>-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M <br>-Xss1M<br>-XX:SurvivorRatio=8</font>
内存运行情况
根据上面的配置,大概只需要20秒,就会把Eden区塞满,然后进行Young GC<br>
Young GC后存活对象约100M,放入到S1区域中
再次运行20秒后,Eden区满了,再次垃圾回收Eden区和S1区中的对象,<br>大约100M的对象会进入S2区
新生代垃圾回收优化:Surviver区够用吗
Survivor区不够用
Young GC后存活对象在100M左右,有可能会突破150M
100M存活对象进入Survivor区,会造成同一年龄的对象超过了Survivor区的50%
调整新生代和老年代的大小
新生代2G,老年代1G(此时Eden区1.6G,Survivor区200M)
降低新生代对象进入老年代的概率
<font color="#f57f17">JVM参数:<br>-Xms3072M -Xmx3072M -Xmn2048M <br>-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M <br>-Xss1M<br>-XX:SurvivorRatio=8</font><br>
新生代对象躲过多少次垃圾回收后进入老年代
按照“-XX:MaxTenuringThreshold”参数设置,默认值15
结合系统的运行模型,可以适当降低躲过垃圾回收的次数,<br>让应该进入老年代的对象,尽快进入老年代
<font color="#f57f17">JVM参数:<br>-Xms3072M -Xmx3072M -Xmn2048M <br>-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M <br>-Xss1M<br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=5</font><br>
多大的对象直接进入老年代
结合自己系统中到底有没有创建大对象来决定,<br>一般设置1M足以
<font color="#f57f17">JVM参数:<br>-Xms3072M -Xmx3072M -Xmn2048M <br>-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M <br>-Xss1M<br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=5 <br>-XX:PretenureSizeThreshold=1M</font><br>
指定垃圾回收器
新生代使用ParNew,老年代使用CMS
<font color="#b71c1c">JVM参数(初步优化参数):<br>-Xms3072M -Xmx3072M -Xmn2048M <br>-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M <br>-Xss1M<br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=5 <br>-XX:PretenureSizeThreshold=1M<br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC</font><br>
优化总结
ParNew垃圾回收器的核心参数
新生代内存大小
Eden和Survivor的比例,给Survivor区充足的空间
结合系统运行模型,合理设置“-XX:MaxTenuringThreshold”,让长期存活的对象,尽快进入老年代
思考:如何估算系统运行模型
每秒占用多少内存?
多长时间触发一次Young GC?
一般Young GC后有多少存活对象,Survivor区能放的下吗?
会不会频繁因为Survivor区放不下导致对象进入老年代?
会不会因动态年龄判断规则进入老年代?
第五节<br>如何优化老年代垃圾回收参数
在当前优化的背景下,<br>新生代对象什么时候会进入到老年代
对象超过临界年龄
小概率情况下,存活对象超过Survivor区的50%
大对象
多久会触发一次Full GC
Young GC前,检查“老年代可用内存空间” < “历次Young GC后进入老年代的平均对象大小”
某次Young GC后存活对象太多,但是老年代可用内存空间不足
“-XX:CMSInitiatingOccupancyFaction”参数设置<br>老年代空间使用超过92%<br>
老年代GC的时候会发生“Concurrent Mode Failure”吗
CMS垃圾回收之后进行内存碎片整理的频率应该多高
<font color="#b71c1c">JVM参数(最终调优):<br>-Xms3072M -Xmx3072M -Xmn2048M<br>-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M<br>-Xss1M<br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=5 <br>-XX:PretenureSizeThreshold=1M <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:CMSInitiatingOccupancyFaction=92 <br>-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0</font><br>
总结
Full GC优化的前提是Young GC的优化
Young GC的优化的前提是合理分配内存空间
合理分配内存空间的前提是对系统运行期间的内存使用模型进行预估
章节思考
5. G1垃圾回收器
第一节<br>G1垃圾回收器的设计思想
ParNew + CMS的组合有哪些痛点?
Stop the World
G1垃圾回收器
最大特点
把JVM堆内存拆分为多个大小相等的Region,<br>同时也会有新生代和老年代的概念(逻辑概念)
堆大小除以2048,计算出每个Region的大小为多大
可以设置一个垃圾回收的预期停顿时间
如何做到对垃圾回收导致的系统停顿可控的?
追踪每个Region里的回收价值
1. 清楚每个Region中有多少是垃圾对象
2. 对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾
<font color="#f44336"><b>在有限的时间内尽量回收更多的垃圾对象</b></font>
Region可能属于新生代也可能属于老年代
不需要对新生代和老年代进行内存空间分配了
新生代和老年代各自的内存区域是不停的变动的,<br>由G1自动控制
更多技术细节
G1是如何工作的?
对象什么时候进入新生代的Region?
什么时候触发Region GC?
什么时候对象进入老年代的Region?
什么时候触发老年代的Region GC?
第二节<br>G1垃圾回收器的工作原理<br>
如何设定G1对应的内存
到底有多少个Region呢?<br>每个Region的大小是多大呢?
默认情况下是G1垃圾回收器自动计算和设置
用堆大小除以2048
比如说堆大小是4G(4096MB),此时除以2048个Region,<br>每个Region的大小就是2MB
手动方式指定
通过“-XX:G1HeapRegionSize”参数设置
新生代占比
默认新生代对堆内存的占比是5%
通过“-XX:G1NewSizePercent”来设置新生代初始占比,保持默认值即可
在系统运行中,JVM会不停的给新生代增加更多的Region,<br>但是新生代最多不会超过60%,可以通过“-XX:G1MaxNewSizePercent”来设置
新生代还有Eden和Survivor的概念吗
新生代里还是有Eden和Survivor的划分的
可以通过“-XX:SurvivorRatio=8”参数来设置Eden区和Survivor区的比例
随着对象不停的在新生代里分配,属于新生代的Region会不断增加,<br>Eden和Survivor对应的Region也会不断增加。
G1的新生代垃圾回收
触发新生代GC
新生代达到了设定的占据堆内存的最大大小60%
复制算法来进行垃圾回收,并且系统进入“STW”状态
目标GC停顿时间
G1执行GC的时候最多可以让系统停顿多长时间
通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms
对象什么时候进入老年代
判断对象年龄
通过“-XX:MaxTenuringThreshold”参数来设定临界年龄
动态年龄判定规则
存活对象在Survivor区中放不下
大对象Region
G1提供了专门的Region来存放大对象
判断条件:对象是否超过了一个Region的50%
大对象如果太大,可能会横跨多个Region来存放
总结
G1垃圾回收器会根据系统运行情况,动态的把Region分配给新生代(Eden区、Survivor区)、老年代、和大对象,<br>新生代和老年代都有各自的最大占比,当新生代的Eden区满的时候,触发新生代垃圾回收
新生代采用复制算法进行垃圾回收,但是不能超过预设的GC停顿时间,<br>所以G1需要清楚每个Region的回收效率
对象进入老年代
当对象在新生代达到临界年龄,<br>或者是触发了动态年龄判定规则,<br>或者是存活对象在Survivor区中放不下
第三节<br>G1的老年代的垃圾回收机制<br>
什么时候触发新生代+老年代的混合垃圾回收
老年代占据了堆内存45%的Region时,触发混合垃圾回收
G1垃圾回收的过程
初始标记
仅仅标记一下GC Roots直接能引用的对象,<br>系统程序会进入到“STW”状态,但是该过程速度很快
并发标记
该阶段运行系统程序运行,从GC Roots追踪所有存活对象,<br>此阶段要追踪全部存活对象,所以很耗时
此阶段还要重新处理原始快照中在并发阶段有引用变动的对象
最终标记
系统程序会进入到“STW”状态
用于处理并发标记阶段结束后,仍然遗留在原始快照中的记录
筛选回收<br>(混合回收)
对各个Region的回收价值和成本进行排序,<br>然后根据用户期望的停顿时间来制定回收计划
自由选择多个Region构成回收集,<br>然后把这部分Region中的存活对象复制到空的Region中<br>最后清理掉整个旧Region的全部空间
这里的操作涉及到存活对象的移动,必须暂停用户线程,<br>由多条收集器线程并行完成
G1垃圾回收器的一些参数
“-XX:G1MixedGCCountTarget”参数
默认值8<br>在混合回收阶段执行几次混合回收<br>
“-XX:G1HeapWastePercent”参数
默认值5%<br>空闲Region的数量达到了堆内存的5%,此时会立即停止混合回收
“-XX:G1MixedGCLiveThresholdPercent”参数
默认值85%<br>存活对象低于85%的Region才可以进行回收<br>
思考:
如果使用G1垃圾回收的时候,应该值得优化的是什么地方?
什么时候可能会导致G1频繁的触发Mixed混合垃圾回收?
如何尽量减少Mixed GC的频率?
第四节<br>G1垃圾回收器性能优化
案例背景
部署:4核8G的机器
每秒600个请求
JVM内存配置
堆内存4G,新生代默认初始占比5%,最大占比60%,<br>每个Java线程的栈内存为1M,元数据区(永久代)内存256M
JVM参数:<br>-Xms4096M -Xmx4096M <br>-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M <br>-Xss1M<br>-XX:+UseG1GC<br>
GC停顿时间设置
通过-XX:MaxGCPauseMills”参数设定,默认值200毫秒
新生代GC如何优化
合理设置“-XX:MaxGCPauseMills”参数
Mixed GC如何优化
思考:
G1垃圾回收器到底在什么场景下适用呢?
章节思考
6. Young GC和Full GC
第一节<br>Java系统最怕的是什么?<br>JVM GC导致系统卡死无法访问
JVM核心内存区——堆内存
1. 存放系统中创建出来的对象<br>2. 通常分为新生代和老年代,系统中创建的对象优先放在新生代<br>3. 新生代快塞满时,垃圾回收器会清理新生代的垃圾对象,也就是那些没有GC Roots引用的对象
新生代垃圾回收
1. 通过复制算法进行回收<br>2. 新生代进行垃圾回收时,会停止系统程序的运行,进入“STW”状态<br>3. 只允许后台垃圾回收器的多个垃圾回收线程工作,执行垃圾回收
系统卡顿问题<br>
垃圾回收期间停止系统程序的运行,这个叫做“Stop the World”
Young GC对系统的性能影响分析
影响有多大
通常来说影响不大,<br>一次新生代GC可能也就耗费几毫秒,几十毫秒
什么情况下影响比较大
当系统部署在大内存机器上的时候,比如说32核64G的机器,<br>此时分配给新生代Eden区的内存可能在30G~40G左右
如何解决大内存机器Young GC过慢问题
用G1垃圾回收器
频繁触发Full GC问题
老年代GC比较耗时
比如说CMS就要经历初始标记、并发标记、重新标记、并发清理、碎片整理几个环节,过程非常的复杂,<br>G1同样也是如此
对象进入老年代的条件
对象年龄足够大
大对象直接进入
动态年龄判断
空间担保机制
第二节<br>GC名称解析<br>
Minor GC / Young GC<br>
专门针对新生代的GC
Full GC / Old GC
所谓老年代GC,称之为“Old GC”更合适一些<br>在之前的分析中,我们把老年代的GC称之为Full GC,其实也是可以的,<br>只不过是字面意思的不同说法<br>
对于Full GC,更合适的说法是它是针对新生代、老年代、永久代的全体内存空间的垃圾回收,<br>所以就是对JVM进行的一次整体垃圾回收<br>
Major GC
一般很少使用,容易与“Old GC”、“Full GC”概念混淆
Mixed GC(混合垃圾回收)
Mixed GC是G1中特有的概念,其实说白了,主要就是说在G1中,<br>一旦老年代占据堆内存的45%了,就要触发 Mixed GC,<br>此时对年轻代和老年代都会进行回收。
第三节<br>Young GC和Full GC在什么情况下会发生<br>
Young GC的触发时机
一般就是在新生代Eden区域满了以后就会触发
Old GC和Full GC的触发时机
Old GC触发时机
老年代空间没办法存放更多对象时,务必执行Old GC对老年代进行垃圾回收
Full GC触发时机
在很多JVM的实现机制里,其实在上述几种条件达到的时候,他触发的实际上就是Full GC,<br>这个Full GC会包含Young GC、Old GC和永久代的GC
永久代满了以后怎么办
假如存放类信息、常量池的永久代满了之后,就会触发一次Full GC,<br>永久代中的垃圾一般是很少的,因为里面存放的都是一些类,还有常量池之类的东西,<br>这些东西通常来说是不需要回收的<br>
第四节<br>JVM频繁触发Young GC对系统性能的危害性
通常Young GC哪怕发生的频繁一些,一般都对系统造成不了太大的影响<br>只有在你机器内存特别大的时候,要注意Young GC可能会导致比较长时间的停顿,<br><font color="#f44336">此时针对大内存机器通常建议采用G1垃圾回收器。</font>
第五节<br>为啥频繁发生Full GC<br>
每次Young GC后,Survivor区域放不下存活对象
此时可以尝试调整新生代的内存比例,<br>保证每次Young GC后存活对象可以放入到Survivor区域
计算类的系统,非常吃内存
更换高配置的机器
可以尝试使用G1垃圾回收器,<br>减少每次GC时的停顿时间
章节思考
7. JVM GC日志
第一节<br>频繁Young GC场景体验
JVM参数示范
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 <br>-XX:NewSize=5242880 -XX:MaxNewSize=5242880<br>-XX:SurvivorRatio=8 <br>-XX:PretenureSizeThreshold=10485760 <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
说明:<br>“-XX:InitialHeapSize”和“-XX:MaxHeapSize”就是初始堆大小和最大堆大小<br>“-XX:NewSize”和“-XX:MaxNewSize”是初始新生代大小和最大新生代大小<br>“-XX:PretenureSizeThreshold=10485760”指定了大对象阈值是10MB<br>新生代使用ParNew垃圾回收器,老年代使用CMS垃圾回收器<br>GC日志的打印(打印详细的GC日志、每次GC发生的时间、日志输出到文件)<br>
程序代码示例
public class Demo1 {<br> public static void main(String[] args) {<br> byte[] arr1 = new byte[1024 * 1024];<br> arr1 = new byte[1024 * 1024];<br> arr1 = new byte[1024 * 1024];<br> arr1 = null;<br><br> byte[] arr2 = new byte[2 * 1024 * 1024];<br> }<br>}
GC日志文件
第二节<br>Young GC日志应该怎么看
查看程序的JVM参数
CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 ……<br>这一行就是此处运行程序采取的JVM参数
一次GC的概要说明
0.218: [GC (Allocation Failure) 0.219: [ParNew: 4096K->512K(4608K), 0.0042920 secs] 4096K->1411K(9728K), 0.0053582 secs] <br>[Times: user=0.00 sys=0.00, real=0.01 secs]
为啥会发生一次GC呢?
GC (Allocation Failure)
说明:<br>对象分配失败,触发一次Young GC
此处GC是什么时候发生的呢?
本行开始位置的数字:“0.218”
说明:<br>系统运行后过了多少秒发生了本次GC
使用哪种垃圾回收器进行的垃圾回收
ParNew: 4096K->512K(4608K), 0.0042920 secs
解读
“ParNew”
使用ParNew垃圾回收器执行的GC<br>
“4096K->512K(4608K)”
新生代可用空间4608K,即4.5M(Eden + 1个空闲Survivor区)
对新生代执行了一次GC,GC之前使用了4096K,GC之后有512K对象存活
“0.0042920 secs”
本次GC耗时4.2毫秒,仅仅回收了3.5M的对象
Java堆的内存情况
4096K->1411K(9728K), 0.0053582 secs
整个Java堆内存是总可用空间9728K,即9.5M(新生代4.5M+老年代5M)<br>GC前整个Java堆内存里使用了4096K,GC后Java堆内存使用了1411K<br>
本次GC消耗的时间
[Times: user=0.00 sys=0.00, real=0.01 secs]
这里全部是0.00 secs,也就是说本次GC就耗费了几毫秒
GC过后的堆内存使用情况
JVM退出的时候打印出来的当前堆内存的使用情况
par new generation total 4608K, used 2183K<br> eden space 4096K, 52% used<br> from space 512K, 8% used<br> to space 512K, 0% used
concurrent mark-sweep generation total 5120K, used 1407K
Metaspace used 3516K, capacity 4500K, committed 4864K, reserved 1056768K<br> class space used 389K, capacity 392K, committed 512K, reserved 1048576K
思考:
JDK 1.8以后的Metaspace和Classspace区域分别存放的是什么内容
GC日志中这里的used、capacity、committed、reserved几个字段,都表示什么含义?
第三节<br>对象进入老年代场景体验
动态年龄判定规则
JVM参数示范
-XX:NewSize=10485760 -XX:MaxNewSize=10485760 <br>-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 <br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=15 <br>-XX:PretenureSizeThreshold=10485760 <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
堆内存20M<br>新生代10M(Eden区8M,每个Survivor区1M)<br>老年代10M
示例代码
public class Demo2 {<br> public static void main(String[] args) {<br> byte[] arr1 = new byte[2 * 1024 * 1024];<br> arr1 = new byte[2 * 1024 * 1024];<br> arr1 = new byte[2 * 1024 * 1024];<br> arr1 = null;<br><br> byte[] arr2 = new byte[128 * 1024];<br><br> byte[] arr3 = new byte[2 * 1024 * 1024];<br> arr3 = new byte[2 * 1024 * 1024];<br> arr3 = new byte[2 * 1024 * 1024];<br> arr3 = new byte[128 * 1024];<br> arr3 = null;<br> <br> byte[] arr4 = new byte[2 * 1024 * 1024];<br> }<br>}<br>
GC日志
在第二次GC日志中发现 “ParNew: 7183K->0K(9216K)”<br>新生代中没有存活对象了
其实是因为Survivor区域中的对象都是存活的,<br>而且总大小超过50%了,而且年龄都是1岁
此时的老年代内存空间
concurrent mark-sweep generation total 10240K, used 802K
总结
通过上面的示例可以证明:<br>此时Survivor区域的对象触发了动态年龄判定规则,虽然没有达到15岁,但是全部进入了老年代<br>
<font color="#d32f2f">所以说如果每次Young GC过后存活的对象太多进入Survivor,特别是超过了Survivor 50%的空间(也就是说设置的Survivor区太小了),<br>很可能下次Young GC的时候就会让一些对象触发动态年龄判定规则进入老年代中。</font>
此时JVM内存空间示意图
Young GC后Survivor区放不下存活对象,<br>就直接进入老年代<br>
JVM参数示范
-XX:NewSize=10485760 -XX:MaxNewSize=10485760 <br>-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 <br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=15 <br>-XX:PretenureSizeThreshold=10485760 <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
堆内存20M<br>新生代10M(Eden区8M,每个Survivor区1M)<br>老年代10M
示例代码
public class Demo3 {<br> public static void main(String[] args) {<br> byte[] arr1 = new byte[2 * 1024 * 1024];<br> arr1 = new byte[2 * 1024 * 1024];<br> arr1 = new byte[2 * 1024 * 1024];<br><br> byte[] arr2 = new byte[128 * 1024];<br> arr2 = null;<br><br> byte[] arr3 = new byte[2 * 1024 * 1024];<br> }<br>}
GC日志
一次Young GC后,会留下一个2M的数组和700多K的未知对象,<br>显然,Survivor区放不下这些对象,那这些对象会全部进入老年代吗?
存活的对象并没有全部进入老年代
Heap<br> par new generation total 9216K, used 2835K<br> eden space 8192K, 26% used<br> <font color="#e57373">from space 1024K, 68% used</font><br> to space 1024K, 0% used
concurrent mark-sweep generation total 10240K, used 2050K
第四节<br>Full GC日志应该怎么看
老年代的GC是如何触发的
模拟场景:新生代存活对象太多,老年代放不下,触发CMS的Full GC
JVM参数示范
-XX:NewSize=10485760 -XX:MaxNewSize=10485760 <br>-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 <br>-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 <br>-XX:PretenureSizeThreshold=3145728 <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
关键参数“-XX:PretenureSizeThreshold=3145728”,大对象阈值为3MB
示例代码
public class Demo4 {<br> public static void main(String[] args) {<br> byte[] arr1 = new byte[4 * 1024 * 1024];<br> arr1 = null; <br><br> byte[] arr2 = new byte[2 * 1024 * 1024];<br> byte[] arr3 = new byte[2 * 1024 * 1024];<br> byte[] arr4 = new byte[2 * 1024 * 1024];<br> byte[] arr5 = new byte[128 * 1024];<br><br> byte[] arr6 = new byte[2 * 1024 * 1024];<br> <br> }<br>}
GC日志,及过程描述
首先,代码开始直接分配了一个4M的大对象,该对象会直接进入老年代,接着这个对象失去了引用;<br>然后又连续分配了4个数组(3个2M的数组,1个128K的数组);<br>最后又分配了一个2M的对象,此时Eden区已经放不下了,直接触发一次Young GC
① Young GC
ParNew (promotion failed): 7420K->8254K(9216K)
Eden区存在7420K的对象,但是都被变量引用了,无法回收;<br>此时需要把这些对象放入到老年代中,但是老年代此时明显放不下,<br>那么CMS垃圾回收器需要进行一次Full GC
② 执行CMS垃圾回收器的Full GC
[CMS: 8194K->6927K(10240K), 0.0055882 secs] 11516K->6927K(19456K), [Metaspace: 2719K->2719K(1056768K)], 0.0087523 secs
Full GC其实就是对老年代进行Old GC,<br>同时一般会跟一次Young GC关联,<br>还会触发一次元数据区(永久代)的GC
老年代的Old GC
CMS: 8194K->6927K(10240K)
老年代从8M多的对象占用,变成了6M多的对象占用,<br>这是为什么呢?
一定是Young GC后,2个2M的数组成功进入了老年代
但是当继续放1个2M的数组和1个128K的数组到老年代时,发现放不下,<br><font color="#f44336">此时触发了CMS的Full GC</font>
Full GC后,会回收掉那个4M的数组,接着1个2M的数组和1个128K的数组进入到老年代
③ CMS Full GC完成后
Full GC后,新生代的对象都进入了老年代,<br>所以此时最后一行代码就可以在新生代分配2M的数组了
Heap<br><font color="#e57373"> par new generation total 9216K, used 2130K<br> eden space 8192K, 26% used</font><br> from space 1024K, 0% used<br> to space 1024K, 0% used
思考
模拟另外几种老年代GC的场景
触发Young GC之前,判断老年代可用空间小于历次Young GC后升入老年代的对象的平均大小,<br>就会在Young GC之前,提前触发Full GC
老年代使用率达到了92%的阈值,也会触发Full GC
章节思考
通过系统的GC日志,分析每次GC后对象的变化情况
8. 定位和解决各种JVM GC问题
第一节<br>摸清系统的JVM运行情况<font color="#d32f2f"><b>(jstat)</b></font>
功能强大的jstat
JVM内的Eden、Survivor、老年代的内存使用情况
Young GC和Full gC的执行次数以及耗时
常用的命令
<font color="#d32f2f">jstat -gc [服务进程的PID]</font>
查看JVM的内存和GC情况
输出内容说明(单位Kb)
S0C:这是From Survivor区的大小<br>S1C:这是To Survivor区的大小<br>S0U:这是From Survivor区当前使用的内存大小<br>S1U:这是To Survivor区当前使用的内存大小<br>EC:这是Eden区的大小<br>EU:这是Eden区当前使用的内存大小<br>OC:这是老年代的大小<br>OU:这是老年代当前使用的内存大小<br>MC:这是方法区(永久代、元数据区)的大小<br>MU:这是方法区(永久代、元数据区)的当前使用的内存大小<br>YGC:这是系统运行迄今为止的Young GC次数<br>YGCT:这是Young GC的耗时<br>FGC:这是系统运行迄今为止的Full GC次数<br>FGCT:这是Full GC的耗时<br>GCT:这是所有GC的总耗时
jstat -gccapacity [服务进程的PID]:堆内存分析
jstat -gcnew [服务进程的PID]:年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
jstat -gcnewcapacity [服务进程的PID]:年轻代内存分析
jstat -gcold [服务进程的PID]:老年代GC分析
jstat -gcoldcapacity [服务进程的PID]:老年代内存分析
jstat -gcmetacapacity [服务进程的PID]:元数据区内存分析
使用jstat工具可以对JVM进程做哪些分析
新生代对象增长的速率
<font color="#d32f2f">jstat -gc [服务进程的PID] 1000 10</font><br>每隔1秒更新出最新的jstat统计信息,共执行10次
通过前后两次输出的信息,可以推断出系统大概每秒增加多少对象
<font color="#b71c1c">使用上面的命令基本上你可以对线上系统的高峰和日常两个时间段内的对象增长速率有很清晰的了解。</font>
Young GC的触发频率及每次耗时
jstat会告诉你迄今为止系统已经发生了多少次Young GC以及这些Young GC的总耗时
每次Young GC后有多少对象是存活 / 进入老年代
老年代对象增长的速率
Full GC的触发频率及每次耗时
可视化监控工具
JConsole
VisualVM
开源的监控系统
第二节<br>摸清系统的对象分布<b><font color="#d32f2f">(jmap和jhat)</font></b>
jmap命令
查看堆内存中各个区域的情况
jmap -heap [服务进程的PID]
查看系统运行时的对象分布
jmap -histo [服务进程的PID]
生成堆内存快照
<font color="#b71c1c"><b>jmap -dump:live,format=b,file=dump.hprof [服务进程的PID]</b></font>
jhat命令<br>在浏览器中分析堆内存快照<br>
jhat -port 7000 dump.hprof
上面的命令会启动一个jhat服务,默认HTTP端口号:7000
第三节<br>对线上系统进行JVM监控<br>
简单方式
使用jstat、jmap、jhat等工具去看看线上系统的JVM运行是否正常,有没有频繁Full GC的问题
监控系统
常见的有Zabbix、OpenFalcon、Ganglia
第四节<br>如何定位和解决频繁Young GC问题<br>
JVM参数示范
-XX:NewSize=104857600 -XX:MaxNewSize=104857600 <br>-XX:InitialHeapSize=209715200 -XX:MaxHeapSize=209715200 <br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=15 <br>-XX:PretenureSizeThreshold=3145728 <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
说明:<br>堆内存200M<br>新生代100M(Eden区80M,每个Survivor区10M),老年代100M
程序代码示例
// 模拟系统按照每秒钟100个请求,每个请求加载300KB数据的方式不停的运行<br>public class Demo5 {<br> public static void main(String[] args) throws Exception {<br> Thread.sleep(30000);<br> while (true) {<br> loadData();<br> }<br> }<br> private static void loadData() throws Exception {<br> byte[] data = null;<br> for (int i = 0; i < 100; i++) {<br> data = new byte[300 * 1024];<br> }<br> Thread.sleep(1000);<br> }<br>}
观察程序的运行状态
<font color="#f44336">jps</font>:查看当前所有Java进程的PID<br><font color="#f44336">jstat -gc [服务进程的PID] 1000 1000</font>:查看JVM的内存和GC情况<br>
JVM运行状态分析
第四节<br>如何定位和解决频繁Full GC问题
JVM参数示范
-XX:NewSize=104857600 -XX:MaxNewSize=104857600 <br>-XX:InitialHeapSize=209715200 -XX:MaxHeapSize=209715200 <br>-XX:SurvivorRatio=8 <br>-XX:MaxTenuringThreshold=15 <br>-XX:PretenureSizeThreshold=20971520 <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
说明:<br>堆内存200M<br>新生代100M(Eden区80M,每个Survivor区10M),老年代100M<br>大对象阈值20MB<br>
程序代码示例
public class Demo6 {<br> public static void main(String[] args) throws Exception {<br> Thread.sleep(30000);<br> while (true) {<br> loadData();<br> }<br> }<br> private static void loadData() throws Exception {<br> <font color="#4caf50">// 分配4个10MB的数组,但是都立马成为垃圾</font><br> byte[] data = null;<br> for (int i = 0; i < 4; i++) {<br> data = new byte[10 * 1024 * 1024];<br> }<br> data = null;<br><br> <font color="#4caf50">// data1和data2两个10MB的数组是被变量引用必须存活的</font><br> byte[] data1 = new byte[10 * 1024 * 1024];<br> byte[] data2 = new byte[10 * 1024 * 1024];<br> <br> <font color="#4caf50">// 此时Eden区已经占用了六七十MB空间了<br><br> // data3变量依次指向了两个10MB的数组</font><br> byte[] data3 = new byte[10 * 1024 * 1024];<br> data3 = new byte[10 * 1024 * 1024];<br> Thread.sleep(1000);<br> }<br>}
观察程序的运行状态
JVM运行状态分析
对JVM性能进行优化
最大的问题是什么
就是每次Young GC过后存活对象太多了,<br>导致频繁进入老年代,频繁触发Full GC
解决方案,<br>例如:
调大新生代的内存空间,增加Survivor的内存即可
-XX:NewSize=209715200 -XX:MaxNewSize=209715200 <br>-XX:InitialHeapSize=314572800 -XX:MaxHeapSize=314572800 <br>-XX:SurvivorRatio=2 <br>-XX:MaxTenuringThreshold=15 <br>-XX:PretenureSizeThreshold=20971520 <br>-XX:+UseParNewGC -XX:+UseConcMarkSweepGC <br>-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
堆内存300M<br>新生代200M,Eden:Survivor:Survivor的比例为2:1:1
章节思考
9. 系统问题排查
排查CPU负载过高的原因
场景一:<br>是你自己在系统里创建了大量的线程,这些线程同时并发运行,<br>而且工作负载都很重,过多的线程同时并发运行就会导致你的机器CPU负载过高<br>
jstack [服务进程ID]:分析该进程下各个线程当前的堆栈状态
top -Hp [服务进程ID]:查看该进程下各个线程的CPU使用情况
场景二:<br>就是你的机器上运行的JVM在执行频繁的Full GC,Full GC是非常耗费CPU资源的,<br>他是一个非常重负载的过程<br>
排查频繁Full GC的问题
原因一:<br>内存分配不合理,导致对象频繁进入老年代,进而引发频繁的Full GC<br>
原因二:<br>存在内存泄漏等问题,就是内存里驻留了大量的对象塞满了老年代,<br>导致稍微有一些对象进入老年代就会引发Full GC<br>
原因三:<br>元空间里的类太多,触发了Full GC<br>
10. 阶段性复习
JVM和GC的运行原理
JVM的内存区域划分
Young GC的过程
对象什么时候进入老年代(4种最常见的情况)
老年代的GC是如何触发的(3种情况)
常见的垃圾回收器
新系统如何设置JVM参数
对系统内存使用模型进行预估
合理分配新生代、老年代的空间,调整好Eden和Survivor的空间比例,<br><br>
原则就是:尽可能让每次Young GC后存活对象远远小于Survivor区域,<br>避免对象频繁进入老年代触发Full GC。
线上频繁Full GC的几种表现及原因
表现
机器CPU负载过高
频繁Full GC报警
系统无法处理请求或者处理过慢
常见原因<br>
系统承载高并发请求,或者处理数据量过大,导致Young GC很频繁,<br>而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区域过小,<br>导致对象频繁进入老年代,频繁触发Full GC
系统一次性加载过多数据进内存,搞出来很多大对象,<br>导致频繁有大对象进入老年代,必然频繁触发Full GC
系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,<br>一直占用在老年代里,必然频繁触发Full GC
Metaspace(永久代)因为加载类过多触发Full GC
JVM参数模板
核心参数就是:<br>内存区域的分配;<br>垃圾回收器的指定;<br>CMS性能优化的一些参数(比如压缩、并发);<br>常见的一些参数,包括禁止System.gc(),打印出来GC日志等
11. 内存溢出(OOM)
第一节<br>哪些区域会发生内存溢出?
1. 用来存放类信息的Metaspace区域<br>(JDK 7中叫永久代)
Metaspace溢出的原因
原因一:Metaspace区域设置太小,对于稍微大型一点的系统,很容易就会不够用<br>推荐值512M,一般是足够的
原因二:使用cglib之类的技术动态生成一些类,一旦代码中没有控制好,导致你生成的类过于多的时候,<br>就很容易把Metaspace给塞满,进而引发内存溢出
避免方法
合理分配Metaspace区域,同时避免无限制的动态生成类
2. 每个线程的虚拟机栈内存
触发场景:方法递归调用
3. 存放对象数据的堆内存
一般引发堆内存溢出的原因,<br>1. 系统负载过高<br>2. 有内存泄漏问题
第二节<br>动手实验,模拟OOM场景<br>
Metaspace区域内存溢出
Caused by: java.lang.OutOfMemoryError: Metaspace。
JVM栈内存溢出
java.lang.StackOverflowError
JVM堆内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
第三节<br>OOM问题监控->定位->解决<br>
OOM异常监控
监控平台,比如:Zabbix、Open-Falcon
比较成熟的系统监控体系
CPU负载
机器磁盘IO的负载
机器内存使用量
JVM监控
各个内存区域的使用量
JVM的Full GC的频率
机器的网络负载
try catch中的异常报错
其实线上机器最容易出问题的主要三大块:CUP、内存和JVM的Full GC问题
JVM内存溢出时自动dump内存快照
-XX:+HeapDumpOnOutOfMemoryError <br>-XX:HeapDumpPath=/usr/local/app/oom
第四节<br>实战案例,OOM解决方案<br>
收藏
0 条评论
下一页