0. 前言
版本: JDK7
1. 运行时数据区
1.1 程序计数器
- 较小的内存空间,当前线程所执行的字节码的行号指示器
- 线程执行 Java 方法,记录正在执行的虚拟机字节码指令的地址
- 线程执行 Native 方法,记录为空(Undefined)
- 唯一没有规定 OOM 情况的区域
1.2 虚拟机栈(Java 栈)
-
描述 Java 方法执行的内存模型
-
每个方法执行时创建一个栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息
- 进入方法栈帧入栈、离开方法栈帧出栈
- 局部变量表存储基本数据类型(byte、short、int、long、float、double、char、boolean)、对象引用(reference 类型,指向对象的起始地址的引用指针、指向代表对象的句柄地址、指向其他与对象相关的位置)、returnAddress 类型(指向字节码指令的地址)
- 局部变量表内存大小编译期确定,运行期不改变
-
线程请求栈深度大于虚拟机允许深度,抛出 SOF 异常
-
栈扩展时无法申请足够内存,抛出 OOM 异常
1.3 本地方法栈
- 类似虚拟机栈,服务于 Native 方法
- 对本地方法栈中方法使用的语言、方式和数据结构无强制规定,可由不同虚拟机自由实现
- 类似虚拟机栈可抛出 SOF 和 OOM 异常
1.4 堆
- 虚拟机启动时创建,大多数虚拟机中内存最大的区域
- 几乎所有对象实例在此分配内存
- 垃圾收集器主要管理的区域
- 可以处于物理上的不连续内存空间,只要逻辑上连续即可
- 没有内存空间为对象实例分配内存,抛出 OOM 异常
1.5 方法区
- 存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- HotSpot 虚拟机使用永久代实现该区域,但仍然需要垃圾管理器回收内存,回收条件苛刻
- 可以处于物理上的不连续内存空间,可以选择不实现垃圾回收
- 无法满足内存分配时,抛出 OOM 异常
1.6 运行时常量池
-
存储编译期生成的字面量和符号引用
- 字面量:字符串文本、8种基本数据类型的值、声明为 final 的常量
- 符号引用:类和接口的全限定名、字段名和描述符、方法名和描述符
-
运行期可将新的常量放入该区域
-
无法满足内存分配时,抛出 OOM 异常
1.7 字符串常量池
- 存储不重复字符串
String str = "hello";
字符串常量池存在 "hello" 时返回引用给变量,不存在时创建一个再返回引用给变量String str = new String("hello");
字符串常量池存在 "hello" 时不创建,不存在时创建一个;同时 new 关键字还会在堆中创建对象实例,返回引用给变量
2. HotSpot 虚拟机对象
2.1 对象创建
类加载 -> 分配内存空间 -> 内存空间初始化零值(不包括对象头) -> 设置对象信息(例如对象头) -> 执行 init 方法,按照程序员意愿初始化
2.1.1 内存分配方式
堆的垃圾收集器是否带压缩整理功能决定了内存是否规整,内存是否规整决定了分配方式
-
指针碰撞:如果堆中内存是规整的,已分配和未分配的区域各放一边,中间存在一个指针作为分界点的指示器。为对象分配内存时,指针向未分配区域移动对象内存大小相等的距离
-
空闲列表:如果堆中内存不规整,已分配和未分配区域互相交错,则虚拟机需要维护一个列表,记录哪块内存区域可用。为对象分配内存时,从列表中找出一块可用区域,并更新列表上的记录
2.1.2 内存分配时线程安全问题
虚拟机频繁进行内存分配时,因为堆是线程共享的,所以存在线程安全问题,解决问题方式如下:
-
CAS 配合失败重试,保证操作的原子性
-
本地线程分配缓冲(TLAB):每个线程在堆中预先分配一块区域,各个线程在各自的区域分配内存。只有 TLAB 用完需要分配新 TLAB 时,才需要同步锁定(可通过
-XX:+/-UseTLAB
参数设定)
2.2 对象内存布局
-
对象头:
-
标记字(Mark Word):
- 存储对象运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有锁、偏向线程 ID、偏向时间戳等
- 非固定数据结构,以便在极小空间内存储更多信息
-
类型指针:
- 指向该对象的类元数据的指针,虚拟机可以通过该指针确认对象属于哪个类的实例
-
数组长度:
- 数组对象时,记录数组长度
-
-
实例数据:
- 存储对象真正有效信息
- 存储顺序受虚拟机分配策略和字段在 Java 源码中定义的顺序影响
-
对齐填充:
- 占位符作用,保证对象大小为8字节的整数倍
2.3 对象的访问定位
当需要使用对象时,可以通过栈中的 reference 数据操作堆中的具体对象。reference 数据保存了指向对象的引用,通过引用访问对象的主流方式有使用句柄和直接指针两种
-
使用句柄:堆划分出一块区域作为句柄池,栈中的 reference 存储的是对象句柄地址,句柄中包含了对象的实例数据和类型数据的具体地址
-
直接指针:栈中的 reference 存储的是对象的在堆中的地址,堆中的对象布局放置对象实例数据和类型数据的具体地址
两种方式各有优势:
- 使用句柄:栈中的 reference 存储的对象句柄地址是稳定的,当对象被移动时(垃圾回收时的普遍行为)只用改变句柄中的实例数据指针
- 直接使用:访问速度快,节省了一次指针定位的时间开销,HotSpot 虚拟机使用该方式
3. 数据区内存溢出实战
3.1 堆溢出
不断 new 对象,导致堆无法分配内存给对象抛 OOM 异常
/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* -Xms 堆内存初始值
* -Xmx 堆内存最大值
* -XX:+HeapDumpOnOutOfMemoryError 内存溢出时 dump 出当前内存堆转储快照,便于后续分析
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
在线工具分析内存堆转储快照:PerfMa
3.2 栈溢出
递归不断调用方法新增栈帧,栈无法分配内存给栈帧抛 SOF 异常
/**
* VM Args:-Xss256k
*
* -Xss 栈内存大小
*/
public class StackSOF1 {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
StackSOF1 oom = new StackSOF1();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
不断新增线程获取栈内存,导致进程中的内存无法分配内存给栈内存抛 OOM 异常
/**
* 谨慎尝试,随时死机
* VM Args:-Xss50M
*
* -Xss 栈内存大小
*/
public class StackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
StackOOM oom = new StackOOM();
oom.stackLeakByThread();
}
}
3.3 方法区溢出
通过 Cglib 不断创建代理类,方法区无法分配内存给类信息抛出 OOM 异常
/**
* VM Args: -XX:PermSize=2M -XX:MaxPermSize=2M
*
* -XX:PermSize 持久化内存初始值
* -XX:MaxPermSize 持久化内存最大值
*/
public class MethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {
}
}
3.4 直接内存溢出
unsafe.allocateMemory() 申请内存,直接内存无法分配内存抛出 OOM 异常
/**
* VM Args:-XX:MaxDirectMemorySize=200M
*
* -XX:MaxDirectMemorySize 直接内存最大值
*/
public class DirectMemoryOOM {
private static final int _100MB = 1024 * 1024 * 100;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_100MB);
}
}
}
学自《深入理解Java虚拟机》(第二版)