堆空间的划分
Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实列对象。这样划分的目的是为了使JVM能够更好的管理堆内存中的对象。堆的内存划分如图:
Java堆的内存划分如图所示,分别为年轻代、Old Memory(老年代)、Perm(永久代)。其中在Jdk1.8中,永久代被移除,使用MetaSpace代替。其中新生代被细分为 Eden 和 两个Survivor 区域,这两个Survivor区域分别被命名为 from 和 to ,以示区分。
- 新生代(1/3堆空间) 老年代(2/3堆空间)
- Eden :from :to = 8 :1 :1
- JVM 每次只会使用Eden 和 其中一块Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor 区域是空闲的,因此新生代实际可用内存空间为 9/10 的新生代空间。
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
GC堆
java中的堆是GC垃圾回收的主要区域。前篇已经分析过垃圾收集需要完成两件事:检测出垃圾和回收垃圾,讲解了如何判断垃圾回收,和垃圾回收算法以及常用垃圾收集器。本部分针对垃圾收集过程进行主要说明。
新生代几乎是所有Java对象出生的地方,即Java对象申请的内存空间以及存放都是在这个地方。java中的大部分对象通常不需要长久存活,具有朝生夕灭的性质。当一个对象被判定为“死亡”的时候,GC就有责任来回收掉这部分对象的内存空间。新生代是GC收集垃圾的频繁区域。GC分为两种:Minor GC 和 FullGC 。MinorGC 是针对新生代中的垃圾收集动作,所采用的是复制算法。当老年代内存空间不足时,将发生FulGC,采用标记清除/整理算法。Full GC指的是清理整个堆空间,包括年轻代和永久代。垃圾回收结果图:
GC过程:
①对象在Eden和一个FromSurvivor区域出生后,当Eden区域满了,或者新创建的对象大小 > Eden所剩空间,将进行一次MinorGC。②如果对象还存活着,并且能被另一块ToSurvivor区域容纳,则将使用复制算法将这些仍存活的对象复制到另一块ToSurvivor区域中,然后清理所使用过得Eden和Survivor区域,并且将这些对象的年龄设置为1.③后续Eden区继续生成对象,再次发生Minor gc的时候,会将存活的对象复制到From区,并将Eden区和To区清空回收,并将改对象的年龄+1.④部分对象会在From区域和To区域中复制来复制去,每熬过一次MinorGC,就将对象的年龄+1。当对象的年龄达到某个值时(默认15),最终如果还存活,就存入老年代。
注意:
- 对于一些较大的对象,(即需要分配一块比较大的连续的内存空间)则直接进入到老年代。
- FullGc 发生的次数不会有MinorGc那么频繁,并且做一次FullGc要比进行一次MinorGc的时间更长。
- MinorGc拷贝到老年代空间不足和大对象老年代空间也不足 触发FullGC
- Perm永久代空间不足会触发Full GC,可以让CMS清理永久代的空间。
发现虚拟机频繁full GC时应该怎么办:可以用命令查看触发GC的原因是什么 jstat –gccause 进程id
什么是空间分配担保策略?
JVM在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果大于,则此次Minor GC是安全的如果小于,则虚拟机会查看HandlePromotionFailure设置项的值是否允许担保失败。如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
动态年龄判断
根据对象年龄有另外一个策略也会让对象进入老年代,不用等待15次GC之后进入老年代,他的大致规则就是,假如当前放对象到Survivor区域,一批对象的总大小大于这块Survivor内存的50%,那么大于这批对象年龄的对象,就可以直接进入老年代了。
GC时为什么要停顿所有Java线程?
设置STW机制,因为GC先进行可达性分析。可达性分析是判断GC Root对象到其他对象是否可达,假如分析过程中对象的引用关系在不断变化,分析结果的准确性就无法得到保证。
JVM参数设置
-Xms | 堆初始值 如:-Xms256m |
-Xmx | 堆最大可用值 如:-Xms512m |
-Xmn | 新生代堆最大可用值, 通常为Xmx1/3 |
-Xss | JDK1.5+ 每个线程堆栈大小为1M |
-XX:NewRatio | 配置新生代与老年代占比 1:2 如:-XX:NewRatio =2 则 新生代占用整个堆空间1/3,老年代2/3 |
-XX:SurvivorRatio | 新生代中eden空间和from/to空间的比例 默认值为8. 即Eden占用新生代8/10 from/to 1/10 |
-XX:PermSize | 永久代的初始化大小 |
-XX:MaxPermSize | 永久代的最大值 |
-XX:+PrintGCDetails | 打印GC信息 |
-XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在发生内存溢出时,Dump出当前的内存堆转存储快照,以便分析用。 |
使用示例:-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
当前堆最大内存 20M,初始化堆内存 20M,新生代最大可用内存 1M,Eden 区域和 form、to 区域的比例是 2:1:1,打印 GC 日志,使用串行回收
内存溢出与内存泄漏区别
内存溢出:申请空间超出系统能够提供的空间大小 内存泄露:内存泄露是指程序中间动态分配了内存,但在程序结束时没有释放这部分内存,从而造成那部分内存不可用的情况,最终导致内存溢出
JVM性能调优
调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐,JVM也不列外,JVM性能调优的目的:减少Full GC的次数,提高JVM性能。
1、性能定义
要查找和评估器性能瓶颈,首先要知道性能定义,对于jvm调优来说,我们需要知道以下三个定义属性,依作为评估基础:这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得。
- 吞吐量(性能):重要指标之一,是指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。
- 延迟(停顿时间):其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。
- 内存占用:垃圾收集器流畅运行所需要 的内存数量。
2、性能调优原则
在调优过程中,我们应该谨记以下3个原则,以便帮助我们更轻松的完成垃圾收集的调优,从而达到应用程序的性能要求。
① MinorGC回收原则: 每次minor GC 都要尽可能多的收集垃圾对象。以减少应用程序发生Full GC的频率。
②GC内存最大化原则:处理吞吐量和延迟问题时候,垃圾处理器能使用的内存越大,垃圾收集的效果越好,应用程序也会越来越流畅。
③ GC调优3选2原则: 在性能属性里面,吞吐量、延迟、内存占用,我们只能选择其中两个进行调优,不可三者兼得。
JVM性能调优方式:系统估算法和GC日志分析法
一、系统估算法以实际案列进行说明:亿级流量电商系统JVM参数设置优化
以标准的4核8G机器为例说明,首先系统预留4G,其他4G按如下规则分配 :
- 堆内存:3g
- 新生代:1.5g
- 新生代Eden区:1228m
- 新生代Survivor区:153m
- 方法区:256m
- 虚拟机栈:1m/thread
设置参数:-Xms3072m -Xmx3072m -Xmn1536m -Xss=1m -XX:PermSize=256m -XX:MaxPermSize=256m -XX:HandlePromotionFailure -XX:SurvivorRatio=8
1、估算系统每秒占用内存数量
在优化JVM之前,要先估算要系统每秒占用的内存数量,如有个亿级流量电商系统,日活用户200万,付费转化率10%,那么每日下单量在20w左右,这些单正常在三四个小时内产生,平均每秒几十单。在大促活动中这些单会在几分钟内产生,假如每秒处理2400单,部署在三台服务器上,那么每台订单服务每秒大概会有800个请求,然后粗略的估算下每个请求占用多少内存,计算出每秒要花费多少内存。
假设是每秒800个请求,每个订单对象锁定1k,首先下单还设计其他对象,如库存、优惠券、积分等,现将请求对象放大10倍。然后,下单过程还涉及其他查询等操作在放大10倍,最后确定每个请求需要分配100k的空间,那1秒需要分配大约80m的内存。1秒后变为垃圾对象(1秒内方法执行完)
2、计算下多长时间触发一次Minor GC
按照之前的估算1秒需要分配大约80m的内存的话,Eden区的空间是1228m那平均每15秒就要执行一次Minor GC。
3、检查下Survivor区是否足够
按照上面的模型,每15秒就要执行一次Minor GC,GC执行期间并不能回收掉所有的新生代中的对象,那每次GC执行期间还会剩下大约80m无法回收的对象会进入Survivor区,但是别忘记JVM有动态年龄判断机制,80M大于Survivor的50%,直接进入老年代。老年代分配1536m,大概需要4.8分钟填满,此时就会发生FullGc。很明显FullGC频率就高,JVM性能调优的目的就是为了减少FullGC频率。
解决:从上面分析来看,这样设置下来Survivor的空间明显小了一点,所以将新生代设置2048m,才能避免触发动态年龄判断,这样就可以避免频繁FullGc。
二、GC日志分析法
想要进行JVM性能调优,首先要了解现有JVM运行情况,我们可以通过观察应用的GC日志,特别是Full GC 日志,对现有系统情况进行了解。
GC日志指令: -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:<filename>
GC日志是收集调优所需信息的最好途径,即便是在生产环境,也可以开启GC日志来定位问题,开启GC日志对性能的影响极小,却可以提供丰富数据。
必须得有FullGC 日志,如果没有的话,可以采用监控工具强制调用一次,或者采用以下命令,亦可以触发
jmap -histo:live pid
在稳定阶段触发了FullGC我们一般会拿到如下信息:
从以上gc日志中,我们大概可以分析到,在发生fullGC之时,整个应用的堆占用以及GC时间,当然了,为了更加精确,应该多收集几次,获取一个平均值。或者是采用耗时最长的一次FullGC来进行估算。
在上图中,fullGC之后,老年代空间占用在93168kb(约93MB),我们以此定为老年代空间的活跃数据。
其他堆空间的分配,基于以下规则来进行。
基于以上规则和上图中的FullGC信息,我们现在可以规划的该应用堆空间为:
java 堆空间: 373Mb (=老年代空间93168kb*4)
新生代空间:140Mb(=老年代空间93168kb*1.5)
永久代空间:5Mb(=永久代空间3135kb*1.5)
老年代空间: 233Mb=堆空间-新生代看空间=373Mb-140Mb
对应的应用启动参数应该为:
java -Xms373m -Xmx373m -Xmn140m -XX:PermSize=5m -XX:MaxPermSize=5m
提升吞吐量的性能调优的目标就是就是尽可能避免或者很少发生FullGC ,尽量在MinorGC 阶段回收更多的对象,避免对象提升过快到老年代。
文章参考:
https://www.toutiao.com/a6659169929876472331/
https://www.toutiao.com/a6758453803709628942/