研究JVM内存模型与运行机制,核心是把“内存区域如何划分、对象如何创建与回收、字节码如何执行”三条线串起来,再用工具和故障案例反复验证。 只背八股文,遇到线上OOM、CPU飙高、线程卡死时仍然会无从下手。
Java虚拟机内存模型和JVM调优有什么区别
很多人把“JVM内存模型”和“JVM内存区域”混着说,面试和排查故障时容易跑偏,先把两个概念拆开,后面学调优才不会乱。
内存区域回答“东西放在哪”
JVM运行时数据区是空间问题,线程私有区包括程序计数器、虚拟机栈、本地方法栈;线程共享区主要是堆和方法区,JDK 8以后方法区的实现叫元空间,堆里放对象实例,元空间放类元数据、常量池等。
| 区域 | 归属 | 常见异常 | 常用参数或观察方式 |
|---|---|---|---|
| 程序计数器 | 线程私有 | 基本不抛OOM | 一般不用调 |
| 虚拟机栈 | 线程私有 | StackOverflowError、OOM | -Xss |
| 本地方法栈 | 线程私有 | 栈溢出 | 跟本地调用相关 |
| 堆 | 线程共享 | Java heap space | -Xms、-Xmx、-Xmn |
| 元空间 | 线程共享 | Metaspace | -XX:MetaspaceSize、-XX:MaxMetaspaceSize |
据Oracle官方文档,堆是垃圾收集器管理的主要区域,但对象不一定都在堆上分配,逃逸分析开启后,部分对象可能在栈上分配或标量替换,new出来的对象一定在堆里”这句话,在优化场景下并不严谨。
JMM回答“线程之间怎么看见”
Java内存模型JMM不是内存区域图,它约束的是多线程读写共享变量时的原子性、可见性、有序性。volatile解决可见性和一定程度的有序性,synchronized同时保证原子性和可见性,final保证构造完成后的安全发布。
JMM的核心是happens-before规则,比如同一个锁的解锁先于后续加锁,volatile写先于后续读,线程启动先于线程内操作,写业务代码时,订单状态、库存标记这类共享字段,如果只靠普通变量跨线程传递,就可能出现一个线程改了值,另一个线程迟迟看不到。
用命令把“图”落到进程上
- 先执行
jps -l找到Java进程PID。 - 执行
jcmd <pid> VM.flags查看实际生效参数。 - 执行
jcmd <pid> GC.heap_info看堆和元空间概况。 - 执行
jstat -gcutil <pid> 1000连续观察GC变化。 - 执行
jmap -histo:live <pid>看存活对象分布。
这些命令比盯着内存模型图背区域更有效,图是地图,命令是定位器。
线上服务Java虚拟机内存溢出怎么排查
OOM不是“加内存”三个字就能解决,先保现场,再分类型,最后复盘参数和代码。
先抓现场再重启
- 在启动参数里加
-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=/data/dump。 - 出现OOM后先执行
jmap -dump:format=b,file=/data/dump/heap.hprof <pid>,如果进程还能响应。 - 执行
jstack <pid> > thread.txt保存线程栈。 - 执行
jstat -gcutil <pid> 1000 20看老年代、元空间和GC耗时。 - 用MAT或VisualVM打开hprof,先看支配树,再看GC Roots到大对象的引用链。
堆内和堆外要分开看
常见OOM信息不只是Java heap space。Metaspace多半和动态生成类、热部署、反射膨胀有关;Direct buffer memory要查Netty、NIO和直接内存参数;unable to create native thread要结合线程数和系统ulimit -u看,堆内问题看对象引用链,堆外问题看分配器和本地内存。
参数配置别只改-Xmx
生产环境里,-Xms与-Xmx设为一致能减少堆伸缩带来的抖动,元空间要设上限,避免无限增长拖垮机器,GC日志建议打开,JDK 9以上可用-Xlog:gc:file=gc.log:time,uptime,level,tags,老版本JDK 8常用-XX:+PrintGCDetails一类参数,版本差异要提前确认。
复盘时别只看“内存够不够”,要回答三个问题:对象为什么被长期持有、缓存有没有上限、线程池队列是否无限长,多数情况下,OOM是代码里的生命周期问题,不是堆参数太小。
Java虚拟机运行机制和垃圾回收有什么关系
运行机制讲的是类怎么加载、字节码怎么执行、方法怎么调用;垃圾回收讲的是对象什么时候算死、内存怎么回收,两者通过对象生命周期连在一起。
从类加载到字节码执行
类加载通常经历加载、验证、准备、解析、初始化,双亲委派模型让基础类优先由上层加载器加载,避免核心类被随意替换,加载完成后,字节码进入执行引擎,解释器先保证启动和兼容性,JIT把热点代码编译成本地机器码,分层编译会根据运行热度在C1、C2之间协作。
可以用javap -c -v Demo.class看字节码,用-XX:+PrintCompilation观察方法编译情况,栈帧里保存局部变量表、操作数栈、动态链接和返回地址,一次方法调用,就是栈帧入栈和出栈的过程。
GC如何介入对象生命周期
对象创建后,垃圾收集器通过可达性分析判断它是否还活着,分代假说认为多数对象朝生夕灭,所以新生代回收频繁,老年代回收相对少,G1按Region管理堆,ZGC追求低停顿,不同收集器适合不同场景,行业共识认为,选择收集器要看停顿目标、堆规模和吞吐要求,不能只看“新不新”。
举个场景:订单服务突发流量,短生命周期的请求对象大量进入新生代,若新生代过小,对象过早晋升到老年代,老年代很快被填满,Full GC次数上升,此时先看GC日志和对象晋升速率,再决定调新生代比例还是查内存泄漏。
Java虚拟机内存模型图怎么看
先分线程私有和共享
看图时先问三个问题:这块区域归谁所有、存什么、满了报什么错,程序计数器、虚拟机栈、本地方法栈是线程私有;堆和元空间是线程共享,方法区是规范概念,永久代是JDK 8以前HotSpot的实现方式,元空间是后来的实现方式,把“方法区等于永久代”挂在嘴边,容易在JDK 8以后的项目里判断失误。
三个常见误区
- 把JMM图当成JVM内存区域图,前者讲并发可见性,后者讲运行时空间。
- 看到StackOverflowError就加堆内存,栈溢出应优先查递归深度和
-Xss。 - 看到元空间增长就怪堆太小,元空间和堆是两套参数。
把图跑起来
打开jconsole或jvisualvm连接本地进程,观察堆、线程、类加载数量,再用jcmd <pid> VM.system_properties和jcmd <pid> VM.classloader_stats核对运行状态,图上的每一块,都能在工具里找到对应指标。
学习路径和培训价格怎么判断
北京Java虚拟机调优培训价格一般多少,公开课、就业班和专题课差异很大,关键看有没有真实故障演练,只讲PPT的课程,不如自己用jmap、jstack、jstat做几轮实验。
三阶段学习路径
- 基础阶段:搞清运行时数据区、类加载、对象创建、GC Roots和常见收集器。
- 实验阶段:本地写一个内存泄漏Demo,用
-Xmx128m限制堆,触发OOM后抓dump并用MAT分析。 - 生产阶段:读GC日志、看线程栈、做压测对比,记录参数变更前后的停顿和吞吐变化。
业内专家指出,JVM能力最终体现在故障定位效率上,而不是背出多少参数,学习时每学一个区域,就配一条命令和一个故障场景,记忆会更牢。
Java虚拟机内存模型与运行机制Q&A
问:Java内存模型和JVM内存区域是一回事吗?
不是,JVM内存区域描述运行时空间划分,JMM描述多线程读写共享变量时的可见性、有序性和原子性规则,排查OOM看内存区域,排查并发可见性问题看JMM。
问:为什么一个线程改了字段,另一个线程有时看不到?
普通共享变量没有跨线程可见性保证,线程可能从工作内存或寄存器读取旧值,可以用volatile建立写读的happens-before关系,或用锁保证同一时刻的可见性和互斥。
问:线上CPU飙高和GC有关系吗?怎么快速定位?
有关系,频繁Full GC、JIT编译线程、业务死循环都可能导致CPU升高,先执行top -Hp <pid>找高CPU线程,把线程ID转十六进制,再执行jstack <pid> | grep -A 20 <nid>对照线程栈;同时用jstat -gcutil <pid> 1000看GC是否异常,定位到具体栈帧后,再判断是GC线程、业务线程还是编译线程,这就是JMM用happens-before约束可见性的原因。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/771313.html


