学习笔记01——《深入理解Java虚拟机(第三版)》第二章
概述
理解JVM内存管理的核心设计思想,掌握内存区域的划分原理、对象生命周期与内存溢出(OOM)的根本原因及排查方法。第二章主要是围绕Java虚拟机的运行时数据区展开,详细介绍了Java虚拟机在运行Java程序时,如何分配和管理内存空间。
一、内存区域划分总览
JVM内存分为线程私有和线程共享区域:
- 线程私有: 程序技术器、虚拟机栈、本地方法栈
- 线程共享:堆、方法区
二、运行时数据区详解
1. 程序计数器(Program Counter Register)
-
作用:记录当前线程执行的字节码指令地址(分支、循环、跳转等依赖此区域),是一块很小的内存空间。如果正在执行的是本地方法,则程序技术器的值为空。
-
特性:
-
线程私有,生命周期与线程绑定。
-
唯一无OOM的区域(无垃圾回收,无内存溢出)。
-
2. Java虚拟机栈(Java Virtual Machine Stack)
-
核心功能:线程私有,用来存储栈帧(Frame),每个方法调用对应一个栈帧的入栈与出栈(方法开始-进栈,方法结束-出栈)。
-
栈帧结构:
-
局部变量表:存放方法参数和局部变量(基本类型、对象引用)。
-
操作数栈:执行字节码指令的工作区(如加减乘除、方法调用)。
-
动态链接:指向运行时常量池的方法引用。
-
方法返回地址:方法正常退出或异常退出的地址,方法返回的是void类型,则不存储返回值。
-
-
异常场景:
-
StackOverflowError:线程请求栈深度超过虚拟机限制(如无限递归)。
-
OutOfMemoryError:虚拟机栈动态扩展时无法申请足够内存(如大量线程并发)。
-
🔍 参数调优:
-Xss1m # 设置线程栈大小为1MB(默认值依赖操作系统,Linux通常为1MB)
3. 本地方法栈(Native Method Stack)
-
功能:为Native方法(如C/C++实现的方法)服务,可以直接访问底层操作系统资源。
-
特点:本地方法栈也是线程私有的,其生命周期与线程的生命周期同步。本地方法栈的实现方式可以由虚拟机实现者自行决定,有些虚拟机直接将本地方法栈和虚拟机栈合二为一。
-
异常:与Java虚拟机栈类似,可能抛出StackOverflowError和OOM。
4. Java堆(Java Heap)
-
核心角色:所有对象实例和数组的存储区域,GC主战场,同时也是Java虚拟机管理的内存区域中最大的一块,是被所有线程共享的内存区域。
-
分代设计:
-
新生代(Young Generation):Eden区 + 2个Survivor区(默认比例8:1:1),Minor GC触发条件:Eden区满。
-
老年代(Old Generation):长期存活对象晋升至此,Major GC/Full GC触发条件:老年代空间不足。
-
-
内存分配策略:
对象优先分配在Eden区;
大对象直接进入老年代(避免复制开销);
动态对象年龄判定,Survivor区中相同年领对象总大小超过50%,晋升到老年代。
-
异常场景:
-
OutOfMemoryError: Java heap space:堆内存不足(内存泄漏或堆容量不足)。
-
🔍 参数调优:
-Xms4g -Xmx4g # 初始堆=最大堆(避免动态扩容引发性能波动)
-XX:NewRatio=2 # 老年代与新生代比例(2表示老年代:新生代=2:1)
5. 方法区(Method Area)
-
核心功能:是所有线程共享的内存区域,用来存储已被虚拟机加载的类元信息(类名、字段、方法)、运行时常量池、静态变量、JIT编译后的代码。
-
演进历史:
-
JDK7及之前:永久代(PermGen),易引发OOM。
-
JDK8+:元空间(Metaspace),使用本地内存,动态扩展。
-
-
异常场景:
-
OutOfMemoryError: Metaspace:类加载过多(如动态生成类、反射滥用)。
-
🔍 参数调优:
-XX:MetaspaceSize=256m # 初始元空间大小
-XX:MaxMetaspaceSize=512m # 最大元空间大小(默认无限制,依赖系统内存)
6. 运行时常量池(Runtime Constant Pool)
-
功能:存放编译期生成的字面量与符号引用(如字符串常量)。
-
与字符串常量池关系:JDK7+将字符串常量池移至堆中,避免永久代OOM。
7. 直接内存(Direct Memory)
-
定义:通过
ByteBuffer.allocateDirect()
分配的堆外内存,不受JVM堆限制。 -
异常场景:
-
OutOfMemoryError:直接内存超过
-XX:MaxDirectMemorySize
限制。
-
三、对象生命周期与内存溢出实战
1. 对象创建流程
-
类加载检查:检查类是否已被加载、解析和初始化。
-
分配内存:
-
指针碰撞(堆内存规整时使用,如Serial、ParNew)。
-
空闲列表(堆内存不规整时使用,如CMS)。
-
-
初始化零值:对象字段赋默认值(如int=0,boolean=false)。
-
设置对象头:存储对象哈希码、GC分代年龄、锁状态等元数据。
-
执行
<init>
方法:构造函数初始化(Java代码层面)。
2. 内存溢出(OOM)场景与排查
OOM类型 | 原因分析 | 排查工具 |
---|---|---|
Java heap space | 对象数量超过堆容量或内存泄漏 | MAT、JProfile分析堆转储文件 |
Metaspace | 动态生成类过多(如CGLib代理) | JVM参数限制,检查类加载器 |
Unable to create thread | 线程数过多,栈总内存超过系统限制 | jstack 分析线程栈,减少线程数 |
Direct buffer memory | 直接内存分配过多(NIO使用不当) | -XX:MaxDirectMemorySize 调整 |
🔧 实战案例:模拟堆内存溢出
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 持续分配1MB数组
}
}
}
输出:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
排查步骤:
-
添加JVM参数:
-Xmx20m -XX:+HeapDumpOnOutOfMemoryError
(生成堆转储文件)。 -
使用MAT(Memory Analyzer Tool)分析
java_pid<pid>.hprof
文件,查找大对象或GC Roots引用链。
四、本章核心总结
-
内存区域划分:线程私有(栈、程序计数器)与线程共享(堆、方法区)。
-
对象生命周期:从分配到回收,依赖GC算法与内存区域特性。
-
OOM本质:内存区域容量不足或对象无法回收(内存泄漏)。
-
调优核心:根据应用类型(高吞吐、低延迟)选择GC算法,合理设置内存参数。
五、高频面试题
1.JVM哪些区域是线程共享的?哪些是线程私有的?
共享:堆、方法区。
私有:虚拟机栈、本地方法栈、程序计数器
2.如何判断一个对象是否可以回收?可达性分析中的GC Roots有哪些
1)可达性分析(Reachability Analysis):如果一个对象无法通过任何引用链连接到 GC Roots,则判定为不可达,可以被回收;GC Roots 是垃圾回收的起点,包括虚拟机栈中引用的对象(局部变量、方法参数等)、方法区中静态变量引用的对象、方法区中常量引用的对象(如字符串常量池)、JNI(Native方法)引用的对象。
// 示例:对象不可达的情况
public class Example {
public static void main(String[] args) {
Object obj = new Object(); // obj 是 GC Root(栈中局部变量)
obj = null; // obj 断开引用,对象变为不可达
// 此时 new Object() 可以被回收
}
}
2)引用类型的影响:
强引用(Strong Reference):默认引用类型,只要存在强引用,对象不会被回收。
软引用(Soft Reference):内存不足时可能被回收(适合缓存)。
弱引用(Weak Reference):无论内存是否充足,GC 运行时都可能被回收。
虚引用(Phantom Reference):无法通过虚引用访问对象,仅用于跟踪对象被回收的状态。
// 弱引用示例:对象可能被快速回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc(); // 触发 GC
if (weakRef.get() == null) {
System.out.println("对象已被回收");
}
3.元空间(Metaspace)与永久代(PermGen)的区别是什么
- 内存位置:元空间在本地内存中分配;永久代在堆内存中分配。
- 内存管理:元空间动态扩展,按需分配,减少内存溢出风险,自动调整大小,仅需设置上限(可选);永久代固定大小,容易导致
OutOfMemoryError: PermGen space,
需要手动调优PermSize。
自动扩容和垃圾回收:
特性 永久代 元空间 扩容机制 固定大小,需手动调整 动态扩展(默认无上限) 垃圾回收 Full GC 时触发,效率低 由元空间自身管理,与堆 GC 解耦 内存释放 难以释放已加载的类元数据 类加载器死亡时,元数据可被回收 - 配置参数:
永久代 元空间 -XX:PermSize
-XX:MetaspaceSize
(初始大小)-XX:MaxPermSize
-XX:MaxMetaspaceSize
(最大大小)(Java 8 已移除) -XX:MinMetaspaceFreeRatio
等 - 元空间的优点:1)避免内存溢出:本地内存通常比堆内存大得多,显著减少了
OutOfMemoryError
风险。2)性能提升:元空间的内存分配和回收效率更高,减少 Full GC 频率;3)简化调优:开发者不再需要关注PermSize
的设置,除非需要限制元空间的最大大小。 - 总结
维度 | 永久代(PermGen) | 元空间(Metaspace) |
---|---|---|
内存位置 | 堆内存 | 本地内存 |
内存管理 | 固定大小,易溢出 | 动态扩展,按需分配 |
垃圾回收 | 依赖 Full GC,效率低 | 独立回收,高效 |
调优复杂度 | 需手动设置 PermSize | 默认无需调优 |
适用版本 | Java 7 及之前 | Java 8 及之后 |
元空间的设计解决了永久代的痛点,使得 JVM 在处理类元数据时更灵活、高效,尤其是在需要动态生成大量类(如 Spring、Hibernate 等框架)的场景下表现更优。
4.StackOverflowError和OutOfMemoryError在栈内存中的区别?
首先我们来分析下,这两个错误触发的条件:
错误类型 | 触发条件 |
---|---|
StackOverflowError | 单个线程的调用栈深度超过栈容量(例如无限递归、过深的嵌套方法调用)。 |
OutOfMemoryError | 创建线程时,线程的栈内存总需求超过 JVM 可分配的栈内存总量(例如创建大量线程)。 |
在来看下栈内存:
-
栈内存(Stack Memory):
-
每个线程独占一个栈,用于存储方法调用的栈帧(局部变量、操作数栈、方法返回地址等)。
-
栈大小由
-Xss
参数指定(如-Xss1m
表示每个线程栈大小为 1MB)。 -
栈内存不足的两种场景:
-
单线程栈溢出:单个线程的调用链过深(如递归未终止)。
-
多线程栈总内存耗尽:创建大量线程,每个线程的栈内存总和超过 JVM 可分配的总内存。
-
-
具体区别有:
维度 | StackOverflowError | OutOfMemoryError (栈内存场景) |
---|---|---|
错误根源 | 单个线程的调用栈过深 | 线程数量过多,总栈内存不足 |
错误类型 | 单线程内部错误 | 多线程资源耗尽错误 |
触发代码示例 | 无限递归:void foo() { foo(); } | 循环创建线程:while(true) new Thread(...).start(); |
错误信息 | java.lang.StackOverflowError | java.lang.OutOfMemoryError: unable to create native thread |
解决方案 | 修复代码逻辑(如终止递归条件) | 减少线程数、调整 -Xss 或增加系统内存 |
是否可恢复 | 不可恢复(需代码修复) | 不可恢复(需调整资源或代码逻辑) |
通过代码辅助理解:
StackOverflowError
public class StackOverflowExample {
public static void main(String[] args) {
recursiveCall(); // 无限递归
}
static void recursiveCall() {
recursiveCall(); // 触发 StackOverflowError
}
}
OutOfMemoryError(栈内存耗尽)
public class ThreadOOMExample {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
try {
Thread.sleep(Long.MAX_VALUE); // 保持线程不退出
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
最后我们可以通过参数调优来避免该错误发生
错误类型 | 调优参数 | 说明 |
---|---|---|
StackOverflowError | -Xss (如 -Xss2m ) | 增大单个线程的栈大小(需权衡内存开销)。 |
OutOfMemoryError | -Xss (减小单个线程栈大小)减少线程数 | 降低单个线程的栈内存占用,或限制线程总数。 |
总结
场景 | StackOverflowError | OutOfMemoryError (栈内存) |
---|---|---|
根本原因 | 单线程调用链过深 | 多线程耗尽总栈内存 |
修复方向 | 代码逻辑优化 | 资源分配优化(线程数、栈大小) |
JVM参数调整 | 增大 -Xss | 减小 -Xss 或增加物理内存 |
-
StackOverflowError
是单线程的栈深度问题,直接由代码逻辑引发。 -
OutOfMemoryError
(栈内存场景)是多线程资源竞争问题,需调整线程数量或栈内存配置。