楼层: 首页/ 软件技术/ Java 基础/ JVM 入门:Java 程序住在哪里
09

JVM 入门:Java 程序住在哪里

JVM Memory · Garbage Collection · Class Loading

面试必问、线上排障必备。你不用手写 JVM,但要知道对象住哪、垃圾谁收、类怎么加载这三件事。理解了它们,遇到"内存溢出 OutOfMemoryError"才不会两眼一抹黑。

运行时内存区域:对象都住哪个房间

区域存什么大白话 & 常见错误
堆 Heapnew 出来的对象所有线程共享。OutOfMemoryError: Java heap space 就是这儿爆了——对象太多没回收掉。
栈 Stack方法内局部变量、调用栈每个线程一个。方法调用太深(无限递归)会 StackOverflowError。
方法区 / 元空间 Metaspace类信息、常量池JDK 8 后用本地内存,OutOfMemoryError: Metaspace 常见于动态生成类太多。
程序计数器当前执行到哪行字节码最小的一块,几乎不会出问题。

垃圾回收 GC:Java 的"清洁工"

C++ 里你 new 了对象必须自己 delete,忘了就内存泄漏。Java 不用——GC(Garbage Collector)自动把"没人引用的对象"回收掉。你只管 new,JVM 负责扫垃圾。

G1 → JDK 9 起的默认收集器
面向大堆、可预测停顿时间,服务器首选。
人话:把堆分成一块一块,每次只收一小片,停顿短,现代应用基本都用它。
ZGC / Shenandoah → 超低延迟收集器
GC 停顿控制在亚毫秒级,适合大堆、低延迟交易。
什么时候上:你的服务对延迟敏感(比如高频交易、实时推荐),普通 G1 的 GC 停顿影响业务时再考虑。

常用 JVM 启动参数(入门够用版)

# 指定堆初始大小和最大值(生产环境强烈建议设成一样,避免来回伸缩)
java -Xms512m -Xmx512m -jar myapp.jar

# 打印 GC 日志,排查"偶发卡顿"时用
java -Xlog:gc* -jar myapp.jar

# 选 GC(JDK 21 默认就是 G1,显式写出来也无妨)
java -XX:+UseG1GC -jar myapp.jar

GC 日志怎么读:一条典型 G1 日志的字段含义

# JDK 9+ 统一日志格式,加 -Xlog:gc* 就会打印:
[0.123s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause)
        20M->5M(100M) 3.456ms

# 逐字段翻译:
# [0.123s]          JVM 启动后 0.123 秒
# GC(0)             第 0 次 GC
# Pause Young       这次是 Young GC(回收新生代)
# 20M->5M(100M)     GC 前堆 20M,GC 后 5M,堆总大小 100M
# 3.456ms           这次停顿 3.4 毫秒

# 排查"偶发卡顿":找耗时最长的那行 Pause,看是不是 Mixed GC / Full GC
# 如果频繁出现 Full GC,说明堆不够大或者内存泄漏

生产环境 JVM 参数调优示例(入门够用版)

# 一个 4G 堆的 Spring Boot 应用启动参数参考
java \
  -Xms4g -Xmx4g \                    # 堆初始和最大设成一样,避免来回伸缩
  -XX:+UseG1GC \                      # JDK 9+ 默认就是 G1,显式写出来
  -XX:MaxGCPauseMillis=200 \         # 期望 GC 停顿不超过 200ms
  -XX:+HeapDumpOnOutOfMemoryError \   # OOM 时自动 dump 堆快照
  -XX:HeapDumpPath=/var/log/dump/ \  # dump 文件存哪
  -Xlog:gc*:file=/var/log/gc.log:time,tags \  # GC 日志写文件
  -jar myapp.jar

# 排查 OOM 时:拿到 heap dump 用 Eclipse MAT 打开,看哪个对象占了大头
# 别上来就加 -Xmx:先搞清楚为什么内存涨上去,加堆只是续命

三种基础回收算法:G1/ZGC 都是它们的拼盘

面试官爱问"GC 算法有哪些",其实底层就三板斧。新生代老年代用不同组合,G1 也只是把这三板斧分区分片地用。

算法人话优缺点
标记-清除
Mark-Sweep
先扫一遍标记哪些对象活着,再把没标记的清掉。简单;但清完会产生碎片,东一块西一块,大对象放不下。
复制
Copying
把活着的对象整体复制到另一半空区域,再把原区域一把清空。没有碎片、快;但要浪费一半空间。新生代(对象朝生夕死)用它最划算。
标记-整理
Mark-Compact
标记后,把活对象往一端挪,再把边界外的清掉。无碎片、不浪费空间;但要挪对象,慢。老年代(活得久、存活率高)用它。

论分代收集 = 三板斧的拼盘

新生代对象死得快,用复制算法(只留少量存活对象复制走);老年代对象活得久、占空间大,用标记-整理(别浪费空间)。G1 把堆切成很多小块,每块按需选用这三板斧,所以能做到停顿可预测。知道"新生代复制、老年代整理"这一句,面试就够了。

常见 OutOfMemoryError:报错信息对号入座

报错信息哪里爆了 & 大概原因
Java heap space堆爆了:对象太多没回收,或一次性加载大集合。
GC overhead limit exceededGC 花了 98% 时间却只回收一点点,基本等同于"快撑爆了"。
Metaspace元空间爆了:动态生成类(CGLIB、反射)太多。
unable to create new native thread线程开太多,操作系统扛不住——八成是没用到线程池。

论类加载机制:三个阶段

加载(Loading):把 .class 字节码读进内存。链接(Linking):验证格式、给静态变量赋默认值、把符号引用换成直接引用。初始化(Initialization):执行静态代码块、给静态变量赋真正的值。

面试常问"双亲委派模型":类加载请求先递给父加载器,父加载器找不到才自己加载——目的是防止核心类被篡改。工作中知道"类是按需加载、有层级委派"这个概念即可。

三层类加载器(从上到下):启动类加载器(加载 java.lang.* 等核心类)→ 平台类加载器(加载扩展模块)→ 应用类加载器(加载你自己写的 classpath 下的类)。加载 java.lang.String 时,永远先让最顶层去加载,就算你自己写了一个同名 String,也不会被用到——这就是双亲委派的安全意义。

章末面试 · JVM(5 题)

1.(概念题)JVM 运行时数据区有哪些?对象实例存在哪?

查看答案

答案:堆(Heap,所有线程共享,new 的对象在这)、栈(Stack,每个线程一个,存局部变量和调用栈)、方法区/元空间(类信息、常量池)、程序计数器。对象实例在堆里。

2.(概念题)GC 怎么判断对象可以回收?

查看答案

答案:可达性分析:从 GC Roots(栈里的局部变量、静态变量、常量)出发,顺着引用链走,走不到的对象就是垃圾。不是引用计数法(解决不了循环引用)。

3.(概念题)类加载过程分哪几步?什么是双亲委派?

查看答案

答案:加载 → 验证 → 准备 → 解析 → 初始化。双亲委派:类加载请求先递给父加载器,父加载器找不到才自己加载,防止核心类(java.lang.String)被篡改。

4.(排错题)报 OutOfMemoryError: Java heap space,先做什么?

查看答案

答案:不是立刻加 -Xmx。先开 -XX:+HeapDumpOnOutOfMemoryError 拿到 dump,用 MAT 分析谁占内存最大。可能是一次性加载了大集合,或静态 Map 只加不清导致泄漏。

5.(选择题)JDK 9+ 默认 GC 是哪个?A. Parallel B. CMS C. G1 D. ZGC

查看答案

答案:C。G1 从 JDK 9 起是默认,面向大堆、可预测停顿。CMS 在 JDK 14 被移除。ZGC 是超低延迟选项。