一、概述
理解JVM内存管理的核心设计思想,掌握内存区域的划分原理、对象生命周期与内存溢出(OOM)的根本原因及排查方法。第二章主要是围绕Java虚拟机的运行时数据区展开,详细介绍了Java虚拟机在运行Java程序时,如何分配和管理内存空间。
二、运行时数据区详解
1. 程序计数器(Program Counter Register)
作用:记录当前线程执行的字节码指令地址(分支、循环、跳转等依赖此区域),是一块很小的内存空间。
特性:
线程私有,生命周期与线程绑定。
唯一无OOM的区域(无垃圾回收,无内存溢出)。
2. Java虚拟机栈(Java Virtual Machine Stack)
核心功能:存储栈帧(Frame),每个方法调用对应一个栈帧的入栈与出栈。
栈帧结构:
局部变量表:存放方法参数和局部变量(基本类型、对象引用)。
操作数栈:执行字节码指令的工作区(如加减乘除、方法调用)。
动态链接:指向运行时常量池的方法引用。
方法返回地址:方法正常退出或异常退出的地址。
异常场景:
StackOverflowError:线程请求栈深度超过虚拟机限制(如无限递归)。
OutOfMemoryError:虚拟机栈动态扩展时无法申请足够内存(如大量线程并发)。
🔍 参数调优:
-Xss1m # 设置线程栈大小为1MB(默认值依赖操作系统,Linux通常为1MB)
3. 本地方法栈(Native Method Stack)
功能:为Native方法(如C/C++实现的方法)服务。
异常:与Java虚拟机栈类似,可能抛出StackOverflowError和OOM。
4. Java堆(Java Heap)
核心角色:所有对象实例和数组的存储区域,GC主战场。
分代设计:
新生代(Young Generation):Eden区 + 2个Survivor区(默认比例8:1:1)。
老年代(Old Generation):长期存活对象晋升至此。
异常场景:
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算法,合理设置内存参数。
五、高频面试题
JVM哪些区域是线程共享的?哪些是线程私有的?
共享:堆、方法区。
私有:虚拟机栈、本地方法栈、程序计数器。
如何判断一个对象是否可以回收?可达性分析中的GC Roots有哪些?
GC Roots包括:虚拟机栈中的局部变量、静态变量、JNI引用等。
元空间(Metaspace)与永久代(PermGen)的区别是什么?
元空间使用本地内存,动态扩展;永久代受JVM堆大小限制。
StackOverflowError和OutOfMemoryError在栈内存中的区别?
StackOverflowError:栈深度超过限制(代码问题)。
OutOfMemoryError:栈扩展时内存不足(系统资源问题)。