JVM内存模型
内存结构
JVM内存结构是怎样的?
答案:
JVM内存
├── 线程共享
│ ├── 堆(Heap)
│ │ ├── 新生代(Young Generation)
│ │ │ ├── Eden区
│ │ │ ├── Survivor0区(From)
│ │ │ └── Survivor1区(To)
│ │ └── 老年代(Old Generation)
│ └── 方法区(Method Area)/ 元空间(Metaspace)
│ ├── 类信息
│ ├── 常量池
│ ├── 静态变量
│ └── JIT编译后的代码
└── 线程私有
├── 程序计数器(Program Counter)
├── 虚拟机栈(VM Stack)
│ └── 栈帧(Stack Frame)
│ ├── 局部变量表
│ ├── 操作数栈
│ ├── 动态链接
│ └── 方法出口
└── 本地方法栈(Native Method Stack)各个区域的作用?
答案:
1. 程序计数器(Program Counter)
- 记录当前线程执行的字节码行号
- 线程私有
- 唯一不会OOM的区域
2. 虚拟机栈(VM Stack)
- 存储方法调用的栈帧
- 每个方法对应一个栈帧
- 线程私有
- 可能抛出StackOverflowError(栈深度超限)或OOM(无法分配内存)
栈帧结构:
java
public int add(int a, int b) {
int c = a + b;
return c;
}
// 栈帧包含:
// 1. 局部变量表:a, b, c
// 2. 操作数栈:计算a+b
// 3. 动态链接:指向运行时常量池的方法引用
// 4. 方法出口:返回地址3. 本地方法栈(Native Method Stack)
- 为Native方法服务
- 线程私有
4. 堆(Heap)
- 存储对象实例和数组
- 线程共享
- GC的主要区域
- 可能抛出OOM
堆内存分代:
新生代(Young):老年代(Old) = 1:2(默认)
Eden:Survivor0:Survivor1 = 8:1:1(默认)5. 方法区(Method Area)/ 元空间(Metaspace)
- JDK7及之前:永久代(PermGen),在堆中
- JDK8及之后:元空间(Metaspace),在本地内存
- 存储类信息、常量、静态变量、JIT代码
- 可能抛出OOM
为什么要分代?
答案: 根据对象存活时间的不同,采用不同的GC策略,提高GC效率。
分代假说:
- 弱分代假说:大部分对象朝生夕死
- 强分代假说:熬过多次GC的对象难以消亡
优势:
- 新生代:对象存活率低,使用复制算法
- 老年代:对象存活率高,使用标记-清除或标记-整理算法
对象创建
对象的创建过程?
答案:
1. 类加载检查
↓
2. 分配内存
├── 指针碰撞(内存规整)
└── 空闲列表(内存不规整)
↓
3. 初始化零值
↓
4. 设置对象头
├── Mark Word(哈希码、GC年龄、锁信息)
└── 类型指针
↓
5. 执行<init>方法
├── 执行父类构造方法
├── 初始化实例变量
└── 执行构造方法代码示例:
java
User user = new User("Tom", 20);
// 1. 检查User类是否已加载
// 2. 在堆中分配内存
// 3. 将内存初始化为零值(name=null, age=0)
// 4. 设置对象头信息
// 5. 执行User的<init>方法
// - 调用Object的构造方法
// - 初始化name="Tom", age=20
// - 执行User的构造方法体对象的内存布局?
答案:
对象内存布局
├── 对象头(Header)
│ ├── Mark Word(8字节)
│ │ ├── 哈希码(HashCode)
│ │ ├── GC分代年龄
│ │ ├── 锁状态标志
│ │ └── 线程持有的锁
│ └── 类型指针(4/8字节)
│ └── 指向类元数据
├── 实例数据(Instance Data)
│ └── 对象的字段数据
└── 对齐填充(Padding)
└── 保证对象大小是8字节的倍数示例:
java
public class User {
private int age; // 4字节
private String name; // 4字节(引用)
}
// 对象大小:
// 对象头:12字节(Mark Word 8 + 类型指针 4)
// 实例数据:8字节(age 4 + name引用 4)
// 对齐填充:4字节(保证总大小是8的倍数)
// 总计:24字节对象的访问方式?
答案:
1. 句柄访问
栈 → 句柄池 → 堆中对象
↓
方法区类型数据- 优点:对象移动时只需修改句柄,栈中引用不变
- 缺点:多一次指针定位
2. 直接指针(HotSpot使用)
栈 → 堆中对象 → 方法区类型数据- 优点:速度快,少一次指针定位
- 缺点:对象移动时需要修改栈中引用
类加载
类加载的过程?
答案:
1. 加载(Loading)
- 通过类的全限定名获取二进制字节流
- 将字节流转换为方法区的运行时数据结构
- 在堆中生成Class对象
↓
2. 验证(Verification)
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
↓
3. 准备(Preparation)
- 为类变量分配内存
- 设置初始值(零值)
↓
4. 解析(Resolution)
- 将符号引用转换为直接引用
↓
5. 初始化(Initialization)
- 执行类构造器<clinit>()
- 初始化类变量和静态代码块示例:
java
public class Test {
private static int a = 1; // 准备阶段:a=0,初始化阶段:a=1
private static final int b = 2; // 准备阶段:b=2(常量直接赋值)
static {
System.out.println("静态代码块"); // 初始化阶段执行
}
}类加载器有哪些?
答案:
类加载器
├── 启动类加载器(Bootstrap ClassLoader)
│ └── 加载<JAVA_HOME>/lib下的核心类库
├── 扩展类加载器(Extension ClassLoader)
│ └── 加载<JAVA_HOME>/lib/ext下的扩展类库
├── 应用程序类加载器(Application ClassLoader)
│ └── 加载classpath下的应用类
└── 自定义类加载器(Custom ClassLoader)
└── 用户自定义的类加载器什么是双亲委派模型?
答案: 类加载器收到加载请求时,先委派给父加载器加载,父加载器无法加载时才自己加载。
流程:
1. 应用程序类加载器收到请求
↓
2. 委派给扩展类加载器
↓
3. 委派给启动类加载器
↓
4. 启动类加载器尝试加载
- 成功:返回Class对象
- 失败:返回给扩展类加载器
↓
5. 扩展类加载器尝试加载
- 成功:返回Class对象
- 失败:返回给应用程序类加载器
↓
6. 应用程序类加载器尝试加载
- 成功:返回Class对象
- 失败:抛出ClassNotFoundException优点:
- 避免类的重复加载
- 保护核心类库不被篡改
示例:
java
// 自定义String类
package java.lang;
public class String {
// 自定义实现
}
// 加载时会委派给启动类加载器
// 启动类加载器加载JDK的String类
// 自定义的String类不会被加载如何打破双亲委派?
- 重写loadClass()方法
- 使用线程上下文类加载器
- OSGI模块化
内存分配
对象优先在Eden区分配?
答案:
java
// -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
public class Test {
public static void main(String[] args) {
byte[] b1 = new byte[2 * 1024 * 1024]; // 2MB,分配在Eden
byte[] b2 = new byte[2 * 1024 * 1024]; // 2MB,分配在Eden
byte[] b3 = new byte[2 * 1024 * 1024]; // 2MB,分配在Eden
byte[] b4 = new byte[4 * 1024 * 1024]; // 4MB,Eden不足,触发Minor GC
}
}大对象直接进入老年代?
答案: 大对象(需要大量连续内存的对象)直接分配在老年代,避免在Eden和Survivor之间复制。
java
// -XX:PretenureSizeThreshold=3145728(3MB)
byte[] b = new byte[4 * 1024 * 1024]; // 4MB,直接进入老年代长期存活的对象进入老年代?
答案: 对象在Survivor区每熬过一次Minor GC,年龄+1,当年龄达到阈值(默认15)时,晋升到老年代。
java
// -XX:MaxTenuringThreshold=15(默认)对象年龄:
新创建:年龄0,在Eden区
第1次GC:年龄1,进入Survivor
第2次GC:年龄2,在Survivor
...
第15次GC:年龄15,进入老年代动态对象年龄判定?
答案: 如果Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象直接进入老年代。
java
// Survivor区大小:1MB
// 年龄1的对象:300KB
// 年龄2的对象:300KB
// 年龄3的对象:300KB
// 年龄1+2的对象总和:600KB > 500KB(Survivor的一半)
// 年龄2及以上的对象直接进入老年代练习题
- 堆和栈的区别?
- 方法区和永久代、元空间的关系?
- 为什么JDK8要用元空间替代永久代?
- 如何判断一个对象是否可以被回收?