前序:
java开发8年,竟说不清道不明jvm到底身为何物?我都不好意思说自己是从事java开发的。
平时开发很少涉及到JVM底层优化,有关JVM的概念全停留在零零碎碎的理论层面。
今天抽空一探究竟,顺便整理一遍。
1. 内存模型
有图有真相,一图剩千言:
补充说明一点:Java虚拟机栈的生命周期与线程一致
StackOverflowError(栈溢出):线程请求的栈深度大于虚拟机所允许的深度(栈帧)
OutOfMemoryError(内存溢出):创建对象或使用资源时无法申请到足够的堆栈内存
Memory Leak(内存泄露):使用完资源后未释放(比如流未关闭),导致资源占用的内存无法被回收利用,就会引起内存泄露,内存泄漏累积会导致内存溢出
2. 堆栈关联
3. 内存管理
下面通过jdk自带的jvisualvm来观察jvm内存配置情况,并根据实际需要自行调优JVM
3.1 选择Visual GC 插件
visualvm访问地址:https://visualvm.github.io/index.html
然后根据自己安装的JDK版本找到对应版本的插件, 复制插件资源地址URL
3.2 配置Visual GC 插件
打开本地的jdk/bin/jvisualvm.exe
编辑录入复制的插件资源地址URL
3.3 安装Visual GC 插件
3.4 重启jdk/bin/jvisualvm.exe,查看Vsiual GC
可以直观地看到内存区域的划分
3.5 远程JVM调试查看
第一步:登录远程服务器配置jstatd的远程RMI服务
进入java/bin目录下,创建文件jstatd.all.policy:
grant codebase "file:${java.home}/../lib/tools.jar" {
permission java.security.AllPermission;
};
第二步:执行jstat命令:
jstatd -J-Djava.security.policy=jstatd.all.policy -J-Djava.rmi.server.hostname=192.168.2.9 &
(192.168.2.9为服务器ip地址, &表示用守护线程的方式运行)
第三步:打开本地的jdk/bin/jvisualvm.exe配置远程连接
连通后,将看到很多本地的java服务,点击可以查看各个服务的内存区域划分情况
3.6 内存配置
服务器上使用jps, 可以看到多出两个进程:
11523 Jstatd
6863 jar
ps -ef|grep 6863
可以查看JVM内存配置参数:
/usr/java/jdk1.8.0_121/bin/java -server
非堆区配置
// -XX:PermSize设置非堆内存初始值,默认是物理内存的1/64
// -XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4
对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
永久代Perm在JDK7后已废弃,改为Metaspace,从JVM堆内存空间中独立出去,共享本地内存
元空间配置
-XX:MetaspaceSize 初始非堆区元空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize 最大非堆区元空间,默认是没有限制的。
-XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
线程堆栈配置
//-Xss1m // 设置每个线程的堆栈大小,默认1M,适当减小该值能生成更多的线程
堆区配置
-Xms2048m // JVM启动时整个堆内存(包括年轻代,年老代)的初始化大小,(s->starter)
一般Xms与Xmx保持一致,避免GC后重新分配内存
-Xmx2048m // JVM运行时整个堆内存的最大值 (x->max)
-Xmn512m // 新生代的空间大小,剩下的是年老代的空间 (n->new)
-XX:NewRatio=3 // 新生代 :老年代 = 1 :3(即Xmn占Xmx的1/4)(与-Xmn512m配置其一即可)
-XX:SurvivorRatio=8 // 新生代中 Eden :Survivor = 8 :2;
即Eden占新生代空间的 8/10;S0,S1各占 1/10; 因此新生代空间利用率最高可达90%
-XX:+UseConcMarkSweepGC // 使用CMS标记清理回收器
JDK7后推荐使用+UseG1GC,被称为G1类型(或Garbage First)的回收器
-XX:+PrintGCDetails // 打印 GC 信息 // +
启用选项; -
不启用选项
-XX:+HeapDumpOnOutOfMemoryError // 让虚拟机在发生内存溢出时 dump 出当前的内存堆转储快照,以便分析时用
4. 垃圾回收
4.1 垃圾回收分类
Minor GC:
在创建新生对象时,如果Eden区空间不足就会触发一次Young GC 。此时会将 S0 区与Eden区的对象一起进行GC Roots可达性分析,然后将仍被使用或引用的活跃对象复制到 S1 区,并提升活跃对象的分代年龄,同时再将 S0 区域和 Eden 区的对象清空,最后将 S0 区 和 S1区交换,等待下次的Young GC。
Major GC:
当 Survvivor 空间不够用时,需要依赖老年代进行分配担保。
如创建大对象时,如果 Eden 区无法容纳会触发Minor GC,Minor GC后仍无法创建,会尝试直接在老年代创建,老年代空间不足,就会触发Major GC,Major GC后仍无法创建,则报OOM异常
Major GC发生在老年代 ,基本上发生了一次Major GC 就会发生一次 Minor GC。并且Major GC 的速度往往会比 Minor GC 慢 10 倍。
Full GC:
Full GC 时,会导致服务器STW(stop the world), 此时服务对外不可用,响应超时。
尽量避免Full GC: 避免定义过大的对象(数组) 以及 避免将过大对象定义为静态变量
JVM 调优,也就是最大化利用服务器资源开销,减少YGC, 避免FGC
4.2 垃圾回收算法
可达性分析:
4.2.1 复制算法(适用于新生代)
4.2.2 标记-清除算法(适用于老年代)(存在内存碎片,无法存放新建的大对象)
4.2.3 标记-整理算法(适用于老年代)
5. 类的加载
双亲委派模型:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。
双亲委派模型的好处:一是可以避免类的重复加载,二是可以避免java的核心API被篡改
反射机制:
- 隐式加载类(正射),通过 new 关键字创建类的实例
- 显式加载类(反射):
(1) ClassLoader.loadClass:得到的class是已经初始化完成的
(2) forName:得到的class是还没有链接(验证,准备,解析三个过程被称为链接)
6. 线程调度
后记:
感觉东西太多了,简略整理出以上内容。
想深入探究,还是要拜读下《深入理解Java虚拟机》。
另说明本文参考了很多技术大佬的博客,比如:https://blog.csdn.net/qq_41701956/article/details/81664921
https://blog.csdn.net/Javazhoumou/article/details/99298624
非常感谢作者的分享!