JVM内存模型及调优
最近参与项目已经推上线进入初期试运行阶段,各个业务服务目前显示处于稳定状态,此时考虑为后期业务范围扩展、数据量增长等情况可能会导致的各种服务性能问题做些准备。故,开始整合相关资料与线上前辈文章,学习研究JVM调优相关理论与经验问题,后期在具体的调优实践过程中会持续修正本文以及更新新的调优实战总结,文中不免疏漏之处,望读者予以批评指正,不胜感激!
1. JVM内存模型
1.1 JVM内存结构
JVM内存模型结构图
1.2 JVM内存数据区域简介
1)栈 Stack
Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、
对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
2)堆 Heap
JVM 所管理的内存中最大的一块,主要是存放对象实例和数组。
内部分为年轻代(Young)、年老代(Old); 年轻代又进一步分为伊甸区(Eden)、存活区(S0/S1)。
内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
3)方法区(Method Area)-- 永久代(PermGen)-- 元空间(MetaSpace)
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
jdk7以前,方法区的实现是永久代,jdk8开始方法区的实现使用元空间取代了永久代;
元空间和永久代最大的区别是:元空间不在虚拟机设置的内存中,而是使用本地内存
4)本地方法栈 Native Method Stack
本地方法栈则为虚拟机使用到的 Native 方法服务
5)程序计数器 Program Counter Register
如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
2. GC算法及GC过程
JVM GC是:JVM的垃圾回收算法,现在的JVM基本采用分代收集,Young区收集频繁,Old区收集较少,Perm(永久代)基本不回收;JVM进行GC时大部分是对新生代的回收,少量的全局回收。
GC按照作用的区域分为:
Minor GC:作用于新生代
Major GC:作用于老年代
Full GC : 整个新生代、老生代、元空间的全局范围的GC
2.1 GC算法
1)标记–清理
2)标记–整理
3)标记–复制
2.2 GC过程
2.2.1 Minor GC
1)Minor GC过程
Survivor区,一块叫From,一块叫To,对象存在Eden和From块。
当进行GC时,Eden存活的对象全移到To块,而From中,存活的对象按年龄值确定去向,
当达到一定值(年龄阈值,通过-XX:MaxTenuringThreshold可设置,默认=15)的对象会移到年老代中,
没有达到值的复制到To区,然后直接清空Eden和From。
之后,From和To交换角色,新的From即为原来的To块,新的To块即为原来的From块,且新的Form块中对象年龄加1.
2)触发条件
Eden区满时
2.2.2 Major GC
Major GC指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),
Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。
使用“标记-整理”或“标记-清理”算法进行回收
2.2.3 Full GC
Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。
1)Full GC过程
使用“标记-整理”或“标记-清理”算法进行回收
2)触发条件
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,
则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
3. JVM优化
调优的最终目的都是为了令应用程序使用最小的硬件消耗来承载更大的吞吐。
使用较小的内存占用来获得较高的吞吐量或者较低的延迟。
JVM内存调优主要的目的是减少GC的频率和Full GC的次数
3.1 JVM优化目标
(1)优化问题
1. heap 内存(老年代)持续上涨达到设置的最大内存值;
2. Full GC 次数频繁;
3. GC 停顿时间过长(超过1秒);
4. 应用出现OutOfMemory 等内存异常;
5. 应用中有使用本地缓存且占用大量内存空间;
6. 系统吞吐量与响应性能不高或下降。
(2)量化目标参考实例
1. Heap 内存使用率 <= 70%;
2. Old generation内存使用率<= 70%;
3. avgpause <= 1秒;
4. Full gc 次数0 或 avg pause interval >= 24小时 ;
注意:不同应用的JVM调优量化目标是不一样的。
3.2 JVM调优参数
JVM常用参数及相关说明
3.3 JVM命令工具
(1)jps(JVM process Status)可以查看虚拟机启动的所有进程,jps -v可查看启动参数
jps
jps -v
(2)jstat(JVM Statistics Monitoring Tool)监视虚拟机信息
jstat -gcutil pid 500 10
每500毫秒打印一次Java堆状况(各个区的容量、使用容量、gc时间等信息),打印10次
(3)jmap(Memory Map for Java)查看堆内存信息
jmap -histo pid
可以打印出当前堆中所有每个类的实例数量和内存占用
jmap -dump
可以转储堆内存快照到指定文件,
比如执行 jmap -dump:format=b,file=/data/dumpfile_jmap.hprof 14620
(4)jconsole、jvisualvm分析内存信息(各个区如Eden、Survivor、Old等内存变化情况)
3.4 JVM优化步骤流程
JVM调优的一般步骤为:
第1步:分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
使用命令 :
jps -v
jmap -dump:format=b,file=fileName.hprof pid
第2步:确定JVM调优量化目标;
第3步:确定JVM调优参数(根据历史JVM参数来调整);
第4步:调优一台服务器,对比观察调优前后的差异;
第5步:不断的分析和调整,直到找到合适的JVM参数配置;
第6步:找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
4. 参考资料
[1] Java虚拟机(JVM)你只要看这一篇就够了!.
[2] JVM性能调优的6大步骤,及关键调优参数详解.
[3] JVM调优参数简介、调优目标及调优经验.