服务器与大带宽专家 · 持牌IDC/CDN/ISP服务商
简米科技官网JIANMI TECH
资讯 2026-11-06 简米科技 3,505 字 8 分钟阅读

JVM底层原理到底是什么?垃圾回收如何实现

导读JVM底层原理的核心答案:Java虚拟机本质是一个虚构的计算机,通过类加载机制、运行时数据区、字节码执行引擎和垃圾回收器四套核心组件,将Java字节码翻译为机器可执行的指令并自动管理内存,JVM类加载机制:你的代码怎么变成活的写好的Java文件经过javac编译,生成class字节码文件,这个文件躺在磁盘上,本……

JVM底层原理的核心答案:Java虚拟机本质是一个虚构的计算机,通过类加载机制、运行时数据区、字节码执行引擎和垃圾回收器四套核心组件,将Java字节码翻译为机器可执行的指令并自动管理内存。

JVM类加载机制:你的代码怎么变成活的

写好的Java文件经过javac编译,生成class字节码文件,这个文件躺在磁盘上,本身只是冷冰冰的二进制序列,JVM的第一步工作,就是把这份字节码“叫醒”。

类加载的三次握手

类加载器不是直接读文件就完事,JVM遵循双亲委派模型:一个类加载请求先交给父加载器,层层向上,直到顶层的Bootstrap ClassLoader,只有父加载器回报“我加载不了”,子加载器才接手处理。

这种机制解决了两个痛点:一是防止核心API被篡改,比如你自己写的java.lang.String永远不会覆盖JDK自带的版本;二是避免类被重复加载,同一个类由不同加载器加载会生成不同的Class对象,双亲委派保证了全局唯一性。

整个加载过程分三个阶段:

  • 加载:通过全限定名获取二进制字节流,把静态存储结构转化为方法区的运行时数据结构,在堆中生成Class对象。
  • 链接:验证字节码合法性,为静态变量分配内存并赋予零值,将符号引用替换为直接引用。
  • 初始化:执行clinit方法,给静态变量赋真实值,执行静态代码块。

类加载触发时机需要记住:new对象、访问静态字段、反射调用、初始化子类、JVM启动时的主类,其他情况都是被动引用,不会触发初始化。

JVM内存模型详解:数据区到底在做什么

JVM把内存划分成几个物理区,各司其职,搞清楚这些区域的职责,是排查内存溢出和性能瓶颈的基础。

线程私有区域

每个线程诞生时,JVM都会悄悄给它分配两块私有地盘:

  • 程序计数器:记录当前线程正在执行的字节码指令地址,分支、循环、跳转、异常恢复都靠它记住位置,唯一不会触发OutOfMemoryError的区域。
  • 虚拟机栈:每个方法调用创建一个栈帧,包含局部变量表、操作数栈、动态链接、方法返回地址,栈深度超过JVM设定值会抛StackOverflowError,动态扩展失败则抛OutOfMemoryError。

线程共享区域

  • 堆:JVM管理的内存中最大的一块,对象实例和数组都在这里分配内存,堆内部进一步划分为新生代(Eden区+两个Survivor区)和老年代,比例默认是1:2,这个比例可以通过参数调整。
  • 方法区:存储已被加载的类信息、常量、静态变量、JIT编译后的代码缓存,JDK 8之后用元空间替代了永久代,区别在于元空间使用本地内存,不再受JVM堆大小限制。

栈上分配与逃逸分析

多数情况下,JVM通过逃逸分析发现某个对象只在方法内部使用,就把对象的内存分配在栈帧上而非堆中,方法结束栈帧弹出,对象直接被销毁,不需要垃圾回收参与,这解释了为什么短生命周期的小对象频繁创建销毁,性能没有想象中那么差。

JVM垃圾回收器怎么选:从Serial到ZGC

垃圾回收是JVM另一个重要的底层机制,对象不再被引用时,就需要被回收释放内存空间,不同场景下的回收器选择,直接影响应用的响应速度和吞吐量。

分代回收的核心思路

JVM假设大部分对象“朝生夕灭”,所以把堆分成新生代和老年代,新生代用复制算法,存活对象复制到Survivor区,其余全部清空,对象每熬过一次Minor GC,年龄加1,达到15岁(可配置)就晋升老年代。

老年代对象存活率高,用标记-清除或标记-整理算法,JDK 8默认的Parallel Scavenge+Parallel Old,注重吞吐量;CMS(Concurrent Mark Sweep)注重低停顿,适合互联网后端服务。

主流的垃圾回收器对比

回收器 适用场景 特点
Serial 单核小内存 简单高效,需要STW停顿
Parallel 多核计算密集型 吞吐量优先
CMS 低延迟服务 并发标记清除,碎片较多
G1 大堆多核 可预测停顿时间,JDK 9+默认
ZGC 超大堆 停顿时间不超过10ms,JDK 15+逐步成熟

G1把堆划分为多个Region,通过追踪每个Region回收价值和成本,维护优先级列表,每次回收最有价值的Region集合,从根本上解决了CMS碎片问题,线上环境JDK 11及以上,多数场景直接使用G1即可。

触发Full GC的常见场景

  • 老年代空间不足
  • 元空间不足(类加载过多)
  • Concurrent Mode Failure(CMS回收速度跟不上对象分配速度)
  • 大对象直接进入老年代且超过剩余空间

Full GC停顿时间通常是Minor GC的数倍,排查性能问题先看GC日志,用-Xlog:gc(JDK 9+)或者-verbose:gc(JDK 8及以下)打印回收详情。

JVM参数调优与线上故障排查实操

参数调优不是玄学,而是基于观察、假设、验证的循环过程,核心逻辑遵循“观察-调整-验证-复盘”的四段式。

必知必会的操作路径

先获取当前JVM运行时信息,再针对性调整参数。

jps -l   # 列出Java进程PID
jinfo -flags <pid>   # 查看当前生效的JVM参数
jstat -gcutil <pid> 1000   # 每秒输出GC情况
jmap -heap <pid>   # 查看堆内存分配和占用
jstack <pid>   # 输出线程快照,排查死锁和阻塞

生产环境如果出现CPU飙升,先top -Hp <pid>定位线程,再用jstack把线程号转成十六进制,在dump文件里搜索对应nid,这个流程能快速锁定是GC线程疯狂工作还是业务代码死循环。

堆内存设置的两个原则

  • 初始值等于最大值:-Xms和-Xmx设为相同值,避免运行期动态扩容收缩带来的性能损耗。
  • 预留系统资源:不要把机器内存全部分给JVM,操作系统和外部进程需要余量,容器环境还要考虑堆外内存(元空间、线程栈、DirectByteBuffer)占用。

典型风险场景应对

内存溢出(OOM),启用HeapDumpOnOutOfMemoryError参数,让JVM在抛出异常时自动导出堆快照。用MAT或VisualVM分析快照,查看支配树找大对象,查看引用链找泄漏源头。

频繁Full GC,用jstat观察GC频率和耗时,再用jmap导出堆,重点关注老年代占用率是否持续增长、是否出现大对象直接晋升,常见原因包括:代码里缓存了不淘汰的数据、SQL查询一次性加载过多数据、连接池设置过大。

元空间溢出,多发生在动态生成代理类或热部署场景,排查方式是检查是否有重复加载的类,设置-XX:MaxMetaspaceSize上限,频繁加载类时考虑使用类加载器隔离。

参数调优交给JVM默认值

JVM内部有一套自适应调节机制,比如-XX:+UseAdaptiveSizePolicy会动态调整堆各区比例,盲目手动调整大概率适得其反,实践经验是:单机部署、非核心应用,直接使用默认参数,只有压测或线上出现明确瓶颈时才介入调优。

JVM底层原理常见问题解答

问题:JVM和JDK有什么区别?

JDK是Java开发工具包,包含JRE(Java运行时环境)、编译器javac、调试器等开发工具,JRE只包含运行Java程序所需的JVM和核心类库,一句话总结:JDK负责把代码编译成字节码,JVM负责把字节码跑起来。

问题:JVM参数调优需要关注哪些核心指标?

三个指标最重要:GC暂停时间(每次垃圾回收让应用停多久)、吞吐量(应用线程运行时间占比)、内存占用(堆使用量),三者互相制约,调优是找平衡点而非极端优化,先用jstat观察当前状态,停顿时间在100ms以内且吞吐量不低于95%,就不需要额外调整。

问题:线上发生JVM内存溢出,排查步骤是什么?

加-XX:+HeapDumpOnOutOfMemoryError参数复现问题,拿到堆转储文件后用MAT分析,先看直方图找出占总内存最高的对象类型,再看GC根引用链确认对象被谁持有,最后回到业务代码修复,堆内存溢出多数原因是集合类无限增长、数据库结果集一次性加载过多、第三方SDK缓存未清理。

分享本文
本文为 简米科技官网 原创,已由运维技术专家审核。转载请注明来源:原文链接
售前咨询 服务热线 售后 邮箱