JVM思维导图
2021-10-27 11:26:40 310 举报
AI智能生成
JVM(Java虚拟机)是Java技术的核心和基础,它是Java能保证“一次编写,到处执行”的关键。JVM思维导图主要包含五个部分:类加载器、运行时数据区、执行引擎、本地方法接口和垃圾收集。类加载器负责将.class文件加载到内存,运行时数据区分为堆、栈、方法区等,执行引擎负责执行字节码指令,本地方法接口用于调用底层操作系统的API,垃圾收集则负责回收不再使用的内存。这五个部分相互协作,使得Java程序能在JVM上高效运行。
作者其他创作
大纲/内容
内存结构
线程共享区域
堆
Eden区
Survivor(from)区 设置survivor是为了减少送到老年代的对象
Survivor(to)区 设置两个Survivor区是为了解决碎片问题
eden:survovir:survivor=8:1:1
方法区
运行时常常量池
静态变量
final类型量
类信息
线程私有区域
虚拟机栈
栈帧
动态链接
操作数栈
局部变量表
方法返回地址
异常
线程请求的栈深度大于虚拟机所允许的深度 StackOverflowError
JVM动态扩展时无法申请到足够的内存时 OutMemoryError
程序计算器
如果程序执行的是Java方法,则指当前线程执行的字节码的行数
此内存区域是唯一一个不会出现OutOfMemoryError情况的区域
如果正在执行的是Natvie方法,这个计算器值则为空
本地方法栈
和虚拟机栈类似,这里主要是为使用到的Native方法服务的
JVM调优
常见参数
Xms
堆内存大小
Xmx
堆内存最大大小
Xmn
年轻代大小
Xss
Java虚拟机栈大小
-XX:PermSize
永久代大小 JDK1.8后变为-XX:MetaspaceSize
-XX:MaxPermSize
永久代最大大小 JDK1.8后变为-XX:MaxMetaspaceSize
-XX:PretenureSizeThreshold
设置大对象的最大值,超过该值,直接进入老年代
-XX:MaxTenuringThreshold
设置年龄阈值
-XX:+UseParNewGC
新生代使用的垃圾回收算法
-XX:+UseConcMarkSweepGC
老年代使用的垃圾回收算法
-XX:ParallelGCThreads
设置ParNew垃圾回收线程数量(一般不要动)
-XX:CMSInitiatingOccupancyFaction
老年代对象占多少时就是垃圾回收
-XX:+UseCMSCompactAtFullCollection
默认打开
-XX:CMSFullGCsBeforeCompaction=0
经过对少次Full GC就会进行一次碎片整理,默认是5
-XX:+CMSParallellnitiallMarkEnabled
这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行。
-XX:+CMSScavengeBeforeRemark
这个参数会在CMS的重新标记阶段之前,先尽量执行一次Young GC。
这样做有什么作用呢?
其实大家都记得,CMS的重新标记也是会Stop the World的,所以所以如果在重新标记之前,先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象。
所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。
这样做有什么作用呢?
其实大家都记得,CMS的重新标记也是会Stop the World的,所以所以如果在重新标记之前,先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象。
所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。
-XX:SurvivorRatio
Eden区占比
G1相关参数
-XX:+UseG1Gc
使用G1垃圾收集器
-XX:MaxGCPauseMills
设置最大停顿时间,默认是200ms
-XX:G1HeapRegionSize
设置G1收集器的Region的大小
年轻代先关的参数
-XX:MaxG1NewSizePercent
新生代最大内存的大小
-XX:G1NewSizePercent
新生代所占内存的大小
老年代相关的参数
-XX:InitiatingHeapOccupancyPercent
如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。(默认值45%)
-XX:G1MixedGCCountTarget
就是在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次
-XX:G1HeapWastePercent
这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
-XX:G1MixedGCLive ThresholdPercent
他的默认值是85%,意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收
-XX:+PrintGCDetails
打印详细的Gc日志
-XX:+PrintGCTimeStamps
打印每次的gc的时间
-Xloggc:gc.log
将gc日志写入一个磁盘文件
-XX:+PrintHeapAtGC
分别打印GC前后的堆内存的情况
-XX:TraceClassLoading-XX:TraceClassUnloading
追踪类的加载和类的卸载
-XX:+DisableExplicitGC
不允许通过代码来触发GC(System.gc())
oom 当发生oom时保存当前的内存快照
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom
-XX:TraceClassLoading -XX:TraceClassUnloading
追踪磊的加载和类的卸载的情况,他会通过日志打印出来JVM中加载了哪些类卸载了哪些类
调优工具
jstat
jstat -gc PID
查看主要内存的运行情况以及垃圾回收情况
jstat -gccapacity PID
堆内存分析
jstat -gcnew PID
年轻代GC分析,这里的TT和MTT可以看到在年轻代存活的年龄和存活的最大年龄
jstat -gccnewcapacity PID
年轻代内存分析
jstat -gcold PID
老年代GC分析
jstat -gcoldcapacity PID
老年代内存分析
jmap
jmap -heap PID
堆内存中的参数情况以及对内存使用情况
jmap -histo PID
展示对象在内存中的占用情况
jmap -dump:live,format=b,file=dump.hprof PID
堆内存快照
jhat
jhat dump.hprof -port 8080
图形化的方式展示对象在内存中的分布情况
调优经验
优化思路
尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。机娘别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响
JVM自己垃圾回收时也是尽量能不进入老年代就不进入老年代
如果老年代的垃圾回收时间间隔越来越短或者老年代中仍有很大的空间却垃圾回收了,就是碎片问题
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
4核8G的机器的JVM模板
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=10 -XX:PretenureSizeThreshold=2M -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gclog/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom
频繁的发生full gc
MateSpace空间满了导致频繁的发生Full gc
-XX:TraceClassLoading-XX:TraceClassUnloading 追踪类的加载和类的卸载
如果MateSpace中的Class越来越多,-XX:SoftRefLRUPolicyMSPerMB=5000 设置的大一点
system.gc()
内存分配不合理,导致对象频繁进入老年代,进而引发频繁的FullGC
系统承载高并发请求,或者处理数据量过大,导致Young GC很频繁,而且每次Young GC过后存活对象太多,内存分配不合理。Survivor区域过小,导致对象频繁进入老年代,频触发Full GC。
系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年代,必然频繁触发Full GC
存在内存泄漏等问题,就是内存里驻留了大量的对象塞满了老年代,导致稍微有一些对象进入老年代就会引发Full GC
1. jps -l 获取线程PID
2. jstat -gc PID 观察内存使用以及垃圾回收情况,发现每次年轻代垃圾回收之后,任然会有大量的对象进入老年代
3. jmap -histo PID > 1.txt 查看堆内存中有哪些大对象
4. 直接查看不方便可以将内存快照拿出来 jmap -dump:live,format=b,file=dump.prof PID
5. 使用jhat dump.dprof 或者使用profma的在线分析工具 分析对象在内存中的分布情况
遇到大对象:
通过jstat观察发现,通常情况下进入老年代的非常少,但同时隔一段时间就会有大对象进入。通过内存快照发现,原来有一个select * from table的生产一个大对象。调整一个语句和设置一下Eden的大小就好了
通过jstat观察发现,通常情况下进入老年代的非常少,但同时隔一段时间就会有大对象进入。通过内存快照发现,原来有一个select * from table的生产一个大对象。调整一个语句和设置一下Eden的大小就好了
调优案例
通过jstat 和内存的gc日志发现,老年代的垃圾回收间隔越来越短,从一小时一次到40分钟一次,最后20分钟一次,而且老年代中的可用空间大小>minor gc历次进入老年代的大小也会发生sms gc,说明老年代中有垃圾碎片严重。设置两个参数-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 每次CMS标记清理之后就进行碎片整理
Full GC进行深度优化
-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:SurvivorRatio=8 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=10 -XX:PretenureSizeThreshold=2M -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./gclog/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/oom
初始标记的时候开启多线程
初始标记阶段,是会进行Stop the World的,会导致系统停顿,所以这个阶段开启多线程并发之后,可以尽可能优化这个阶段的性能,减少Stop the World的时间。
初始标记阶段,是会进行Stop the World的,会导致系统停顿,所以这个阶段开启多线程并发之后,可以尽可能优化这个阶段的性能,减少Stop the World的时间。
-XX:+CMSParallelInitialMarkEnabled
CMS的重新标记阶段之前,先尽量执行一次Young GC
先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。
先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。
-XX:+CMSScavengeBeforeRemark
大对象导致老年代频繁GC
当观察到每次minor gc之后进入老年代中的对象并不是非常多,但是多一段时间就发有一个非常大的队形进入老年代,导致频繁的Full GC。将大对象进入老年代时进行内存导出并进行分析,发现有一个sql语句是select * from table。导致产生一个大对象,直接进入了老年代。解决方法 :1.修改sql语句2.增加年轻代大小
内存相关
内存分配
大多数情况下,对象会优先分配在Eden区。当Eden区内存不足时,虚拟机会发起一次Minor GC 对象会优先分配在Eden区
为了避免大对象在Eden和Survivor中来回复制,浪费时间。对于超过一定大小的对象,直接分配在老年代 大对象直接进入老年代
长期存活对象进入老年代
如果一个对象在Eden区中,并且在一次Minor GC后仍然存活,也没有进入老年代中。被复制到一个Survivor中,那么它的年龄被记为1,以后每经历过一轮的Minor GC,年龄就增长一岁。达到一定年龄后就会进入老年代
动态年龄判断
如果survivor中年龄一岁+年龄两岁的对象占比超过survivor空间的百分之50,则两岁及以上的所有对象统统进入老年代
动态年龄判断参与计算的对象时Survivor中的所有的对象,不管它是否已经失去了引用。但失去引用的部分并不会进入老年代
空间分配担保机制
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,如果担保失败则会进行一次Full GC;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
链接
https://www.processon.com/view/link/5fb4cb89e401fd3d93e5b190
Minor GC之后,survivor无法放下
Minor GC 之后如果Survivor无法放下,则直接进入老年代
垃圾回收时机
年轻代
当Eden区内存不足时
老年代
老年代中对象的大小设置阈值(比如92%),达到阈值之后触发Full GC
Minor GC之前
老年代中连续空间的大小<新生代所有对象的总空间大小,HandlePromotionFailure没有设置
Minor GC之前,老年代中剩余空间的大小<历次进入老年代对象的大小
Minor GC之后
Minor GC之后,剩余对象的大小>survivor的大小&&剩余对象的大小>老年代剩余空间的大小
MateSpace空间不足
System.gc
内存泄漏
程序在申请内存后,无法释放已申请的内存空间
原因
长生命周期的对象持有生命周期对象的引用
例如将ArrayList设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏
连接未关闭
如数据库连接、网络连接和IO连接等,只有被关闭之后,垃圾回收器才会回收对应的对象
作用域不合理
例如 1.一个变量的定义和作用范围大于其使用范围 2 没有及时把对象设置为null
内部类持有外部类
Java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏
Hash值改变
在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露
内存泄漏的时候使用mat分析 ,完美
内存溢出
程序申请内存时,剩余的内存空间不足
原因
JVM内存运行动图
MateSpace内存空间不足
Class可以回收的条件
这个类加载器被收回
这个类的实例对象全部被收回
OOM发生时机
尝试回收Metaspace中的类之后发现还是没有腾出太多的空间,此时还要继续网Matespace中塞入更多的类,直接就会引发内存溢出的问题。因为此时Me他space区域的内存空间不够了
可能原因
使用默认的MateSpaceSize,空间太小了
使用动态代理,没控制好
栈溢出
StackOverflowError:增大本地变量表,例如不合理的递归
OutOfMemberError:不断建立线程
每个栈内存分配1M,1000个线程就是1G
堆溢出
可能原因
大并发量,导致大量对象存活无法被回收
内存泄漏,导致被申请的空间无法被回收
minor gc 之后 存活对象的大小>老年代剩余连续空间的大小&存活对象的大小>survivor 空间的大小 我发现jdk1.8 并不会轻易的full GC ,它会尝试把一部分存活对象放入老年代,剩余的存活对象,如果survivor存的下就放在survivor中,如果放不下,就会放在Eden中.只有当Eden中放不下了,才会报oom
老年代垃圾回收之后,还是容不下该进入老年代的对象
年轻代中放不下
案例
发送消息到消息中间件,如果失败了就不同的重试,这时候大量的对象驻留在内存中,无法进行释放
进行复杂计算,让后推送到kafka中,结果kafka失败
设置超时时间过长,RPC调用其他服务,其他服务故障,导致处理时间过长,大量内存被占用,导致堆溢出
select * from table导致内溢出
如何查找堆内存溢出的原因
通过gc日志和内存快照分析原因
类的加载机制
类的生命周期
加载
使用的时候加载
验证
检查加载进来的.class文件是否符合JVM规范
准备
分配内存空间,给静态变量分配默认值
解析
把符号引用替换为直接引用
初始化
将静态变量赋值
使用
卸载
类加载器
启动类加载器
加载 JAVA_HOME/lib下的内容
扩展类加载器
加载JAVA_HOME/lib/ext下的内容
应用程序类加载器
加载CLASSPATH下的内容,大致是用户写的内容
自定义类加载器
根据用户需求自己定义,也需要继承ClassLoader类
双亲委派模型
内容
如果一个类加载器接受到一个类加载的请求,它先不会自己去尝试加载,而是去把请求委派给它的父类加载器去加载。当父类加载器找不到类的时候,会抛出ClassNotFoundExcception,然后子类加载器才会自己去尝试加载
实现
首先检查类是否已经被加载
把加载请求委派给父类进行加载
如果父类抛出ClassNotFoundException异常,表示父类加载器无法加载,则当前类加载器调用findClass尝试进行加载
如果能加载,则直接返回类
好处
保证Java类库中的类不受用户类的影响,防止用户自定义一个类库中的同名类,引起问题
类加载方式
1.命令行启动应用时由JVM初始化加载
2.通过Class.forname()方法动态加载
3.通过ClassLoader.loadClass()方法动态加载
类加载时机
遇到new,getStatic,putStatic,invokeStatic这四条指令
new一个对象时
调用一个类的静态方法
直接操作一个类的static属性
使用java.lang.reflect进行反射调用
初始化类时,没有初始化父类,先初始化父类
虚拟机启动时,用户指定的主类(main)
垃圾收集
垃圾在哪儿?
上图可以看到程序计数器、虚拟机栈、本地方法栈都是伴随着线程而生死,这些区域不需要进行 GC。
而方法区/元空间在 1.8 之后就直接放到本地内存了,假设总内存 2G,JVM 被分配内存 100M, 理论上元空间可以分配 2G-100M = 1.9G,空间还是足够的,所以这块区域也不用管。
所以就只剩下堆了,java 对象实例和数组都是在堆上分配的,所以垃圾回收器重点照顾堆。
JVM分代模型
年轻代
老年代
永久代
存放类信息
对象是否存活
引用计数法
给对象添加上一个引用计数器,当对象增加一个引用时就加1,当一个引用失效时就减1,当引用计数为0时,该对象就可以被回收
缺陷:如果循环引用,就可能导致内存泄漏
可达性分析
对每个对象,都分析一下谁引用了它,然后一层一层分析,看最终是否有GC Roots引用了它
GC Roots
当前虚拟机栈中局部变量表在中引用的对象
当前本地方法栈中局部变量表中引用的对象
方法区中静态变量
方法区中的常量
判断一个对象是否能够回收的过程(两步)
1.找到GC Roots不可达的对象,看其是否重写了finalize()方法或者调用了finalize()方法,如果都没有就放入F-Queue中
2.再次进行标记,如果这些对象还没有与GC Roots建立连接,则直接回收
回收对象的引用类型
强引用
垃圾回收器绝对不会回收它,宁愿自己抛出OOM,使得程序异常停止,也不会回收它
软引用
正常情况下垃圾回收器是不会回收软引用的,当垃圾回收之后,内存还是不足,才会回收软引用的对象
软引用特别适合创建缓存,当内存充足的时候不会回收,当内存不足的时候回收
弱引用
当垃圾回收时,弱引用的对象,不管内存是否充足,都会被回收
虚引用
垃圾收集算法
标记-清除
将所有待回收的对象标记出来,然后一起清理
缺陷
1.标记和清理的效率都不高
2.可能产生大量的内存碎片
复制算法
复制算法是将堆内存分为两个相等的区域,每次只使用其中的一块,当其中一块区域满的时候,就会将存活的对象复制到另一个区域,然后将该区域清理
新生代使用的就是复制算法
优点:简单高效,不会存在内存碎片问题
缺点:内存利用率低
标记-整理
和复制算法一样,将所有的存活对象标记出来,然后将所有的对象移动到内存的一端
老年代使用的就是标记-整理算法
缺陷:需要移动大量的对象,效率不高
分代回收算法
根据不同的年代选取不同的垃圾回收算法
年轻代使用复制算法
老年代使用标记-清除算法或者标记-整理算法
分代
Young(年轻代)
Eden(伊利园):新生对象
Survivor(幸存者):垃圾回收后还活着的对象
Tenured(老年代):对象多次回收都没有被清理,会移到老年代
Perm(永久代):存放加载的类别还有方法对象,java8 之后移除了永久代,替换为元空间(Metaspace)
垃圾收集器
Serial收集器
串行单线程垃圾收集器
Serial 收集器是一个单线程收集器,在进行垃圾回收器的时候,必须暂停其他工作线程,也就是发生 STW。在 GC 期间,应用是不可用的。
特点
1、采用复制算法
2、单线程收集器
3、效率会比较慢,但是因为是单线程,所以消耗内存小
ParNew收集器
ParNew 是 Serial 的多线程版本,也是工作在新生代,能与 CMS 配合使用。
在多 CPU 的情况下,由于 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快,也能有效地减少 STW 的时间,提升应用的响应速度。
在多 CPU 的情况下,由于 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快,也能有效地减少 STW 的时间,提升应用的响应速度。
Serial?还是ParNew?
使用Serial还是ParNew 看所处的环境,如果是单核环境下就是用Serial收集器,多核环境下就使用ParNew收集器
如果在单核环境下,使用ParNew收集器,可能导致线程切换,从而导致开销变大
如果在多核环境下使用Serial,又可能导致CPU的浪费
特点
1、采用复制算法
2、多线程收集器
3、效率高,能大大减少 STW 时间。
Serial Old
Serial Old 是工作于老年代的单线程收集器。
作用:
在 Client 模式下与 Serial 回收器配合使用
Server 模式下,则它还有两大用途:一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用,另一种是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
特点:
1、标记-整理算法
2、单线程
3、老年代工作
Stop the world
GC 期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。年轻代和老年代垃圾回收的时候,都会停止程序的执行,来进行垃圾的回收
在 java 程序中引用关系是不断会变化的,那么就会有很多种情况来导致垃圾标识出错。
想想一下如果一个对象 A 当前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他引用指向了 A,那么此刻又不是垃圾了。
那么,如果没有 STW 的话,就要去无限维护这种关系来去采集正确的信息,显然是不可取的。
想想一下如果一个对象 A 当前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他引用指向了 A,那么此刻又不是垃圾了。
那么,如果没有 STW 的话,就要去无限维护这种关系来去采集正确的信息,显然是不可取的。
CMS收集器
CMS 可以说是一款具有"跨时代"意义的垃圾回收器,如果应用很重视服务的响应速度,希望给用户最好的体验,则 CMS 收集器是非常合适的,它是以获取最短回收停顿时间为目标的收集器!
使用的是标记-清理算法
回收的过程
初始标记
- 仅仅是标记GC Roots能够直接关联到的对象,速度很快,需要停顿(STW)
并发标记
从GC Roots开始对堆中所有的对象进行可达性分析,标记所有的存活对象,这个过程耗时最长,不需要STW
在并发标记的过程中可能会产生新的对象,也可能有对象失去引用,所以需要进行最终标记
最终标记
为了修正并发标记过程中,由于程序的运行导致的对象引用的改变,对这部分对象的标记记录进行修正,需要停顿(STW)
并发清理
清理垃圾,不需要停顿
缺陷
吞吐量低
停顿时间是以牺牲吞吐量为代价的,CPU利用率不高
Concurrent Mode Failure
浮动垃圾问题:在并发清理阶段是并发执行的,用户程序还会继续执行,造成一些对象进入老年代,同时又失去了引用,但只能下一次垃圾回收才能回收
如果在CMS垃圾回收阶段,又有对象进入老年代(老大动Minor),而这时老年代中没有足够的空间来容纳这些对象,那么就会报Concurrent Mode Failure异常。此时就会启动Serial Old来进行单线程的垃圾回收
老年代中对象的大小设置阈值(比如92%),不能太大,要预留空间给浮动垃圾
-XX:CMSInitiatingOccupancyFaction
老年代对象占多少时就是垃圾回收
-XX:+UseCMSCompactAtFullCollection
默认打开、标记清理后整理
-XX:CMSFullGCsBeforeCompaction
经过对少次Full GC就会进行一次碎片整理,默认是零
ParNew+CMS的痛点
STW
G1收集器
G1垃圾收集器将堆内存中的对象分为多个大小相等的区域(Region),年轻代和老年代之间不在进行隔离(G1垃圾收集器有逻辑上的年轻代和老年代)
E 代表伊甸区
S 代表 Survivor 区
H 代表的是 Humongous 区
O 代表 Old 区
每个Region不会固定分配给老年代还是年轻代,按需分配
设计思想:用最少的时间,回收最多的对象.停顿时间可控
G1收集器和ParNew+CMS的区别
1.在对象进入老年代时,G1收集器有专门的Region来存放大对象,如果对象过大可以用连续的Region来存放。而ParNew中是超过一定大小的对象直接进入老年代
2.G1收集器中的大对象,在新生代和老年代的垃圾回收中都可能被回收
垃圾收集过程
初始标记
仅标记和GC Roots直接关联到的对象,速度很快(STW)
并发标记
从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
最终标记
最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs 里面,最终标记阶段需要把Remembered Set Logs 的数据合并到Remembered Set中。这阶段需要停顿线程(STW),但是可并行执行。
筛选回收:首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
回收过程并不是一次性完成的。回收过程是停一次回收点,然后恢复系统工作,然后再停回收,直到达到回收要求为止
回收过程并不是一次性完成的。回收过程是停一次回收点,然后恢复系统工作,然后再停回收,直到达到回收要求为止
回收失败时
如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去
此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发一次失败。
一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。
此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发一次失败。
一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。
混合回收触发条件
G1有一个参数,是"-XXInitiatingHeapOccupancyPercent",他的默认值是45%
意思就是说,如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
意思就是说,如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
特点
空间整合,没有碎片
可预测的停顿时间
G1相关参数
-XX:+UseG1Gc
使用G1垃圾收集器
-XX:MaxGCPauseMills
设置最大停顿时间,默认是200ms
-XX:G1HeapRegionSize
设置G1收集器的Region的大小
年轻代先关的参数
-XX:MaxG1NewSizePercent
新生代最大内存的占比
-XX:G1NewSizePercent
新生代所占内存的占比
老年代相关的参数
-XX:InitiatingHeapOccupancyPercent
如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。(默认值45%)
-XX:G1MixedGCCountTarget
就是在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次
-XX:G1HeapWastePercent
这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
-XX:G1MixedGCLive ThresholdPercent
他的默认值是85%,意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收
特点:
并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,可以通过并发的方式让 Java 程序继续执行,进一步缩短 STW 的时间。
分代收集:分代概念在 G1 中依然得以保留,它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次 GC 的旧对象来获得更好的收集效果。
空间整合:G1 从整体上看是基于标记-整理算法实现的,从局部(两个 Region 之间)上看是基于复制算法实现的,G1 运行期间不会产生内存空间碎片。
可预测停顿:G1 比 CMS 厉害在能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
在垃圾收集过程中遇到的几个名词
promotion failed
该问题是在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的。(promotion failed时老年代CMS还没有机会进行回收,又放不下转移到老年代的对象,因此会出现下一个问题concurrent mode failure,需要stop-the-wold 降级为GC-Serail Old)。
这一步主要是,进行年轻代垃圾回收的尝试,但是失败了,然后就会触发老年代的垃圾回收
concurrent mode failure
该问题是在执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的。Serial Old
参考:https://www.jianshu.com/p/ca1b0d4107c5
内存分配与回收策略
堆空间的结构
Eden 区
研究表明,有将近 98%的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配。
当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。
通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。
通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
Survivor 区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,Survivor 又分为 2 个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。
为什么需要 Survivor?
如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实或许第二次,第三次就需要被清除。
为什么需要 From 和 To 两个呢?
这种机制最大的好处就是可以解决内存碎片化,整个过程中,永远有一个 Survivor 区是空的,另一个非空的 Survivor 区是无碎片的。
假设只有一个 Survivor 区。
Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。
那么问题来了,这时候我们怎么清除它们?
在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。
因为 Survivor 有 2 个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,To 区 到 From 区 ,以此反复。
Old 区
老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。
由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以在这里老年代采用的是标记整理算法。
复习参考
https://mp.weixin.qq.com/s/hiw5878tQz0_fbcffaEk1w
自由主题
面试问题
第一个是你们生产环境的系统的JM参数怎么设置的?为什么要这么设置?
还有一个是你在生产环境中的JM优化经验可以聊聊?
另外一个是说说你在生产环境解决过的JVM OOM问题?
0 条评论
下一页