前言
当前文章只代表个人初学理解,若有问题,欢迎指出,蟹蟹~
一、内存模型
区域划分
- 堆: 存放实例对象;其中又被划分成:新生代、老年代,默认比例1:2,新生代中又区分Eden区、Surivivor区(survivor From、Survivor to),默认比例8:1:1。堆也是主要进行垃圾回收的区域。
- 方法区(元数据区): 存放虚拟机加载的:类型信息,域(Field)信息,方法(Method)信息,常量,静态变量,即时编译器编译后的代码缓存。
- 程序计数器: 记录线程执行的字节码行号。
- 虚拟机栈: 保存了每次方法调用的信息。每一次执行方法都会往虚拟机栈中压入一个栈帧(每个栈帧中具有:局部变量表、操作数栈、动态链接、方法出口)。
- 本地方法栈: 存放native方法。
总结
Java类加载机制是JVM在运行时动态加载类的核心机制,其核心流程分为加载、验证、准备、解析、初始化五个阶段。以下是各环节的详细功能说明:
二、类加载机制
1️⃣ 加载(Loading)
- 功能:将类的
.class文件二进制数据读入内存,并生成对应的java.lang.Class对象。 - 核心步骤:
- 通过类的全限定名(如
java.lang.String)获取字节流(如从JAR包、网络、动态代理生成等)。 - 将字节流转换为方法区的运行时数据结构。
- 在堆内存中创建
Class对象,作为方法区数据的访问入口。
- 通过类的全限定名(如
- 类加载器:
- Bootstrap ClassLoader:加载JRE核心库(如
rt.jar)。 - Extension ClassLoader:加载
jre/lib/ext目录下的扩展类。 - Application ClassLoader:加载用户类路径(ClassPath)的类。
- 自定义类加载器:实现特殊加载需求(如热部署、模块化)。
- Bootstrap ClassLoader:加载JRE核心库(如
2️⃣ 验证(Verification)
- 功能:确保字节流符合JVM规范,防止恶意代码破坏虚拟机安全。
- 验证内容:
- 文件格式验证:检查魔数(
0xCAFEBABE)、版本号等。 - 元数据验证:检查语义(如是否有父类、是否继承final类)。
- 字节码验证:确保方法逻辑合法(如类型转换、跳转指令)。
- 符号引用验证:检查引用的类、方法、字段是否存在(发生在解析阶段)。
- 文件格式验证:检查魔数(
3️⃣ 准备(Preparation)
- 功能:为类的静态变量分配内存并设置初始值。
- 关键点:
- 仅分配类变量(
static变量),实例变量在对象实例化时分配。 - 初始值为零值(如
int初始化为0,boolean为false)。 - 若变量是
static final常量(如public static final int x = 123),直接赋值为定义值。
- 仅分配类变量(
4️⃣ 解析(Resolution)
- 功能:将常量池中的符号引用替换为直接引用。
- 符号引用:用一组符号描述引用的目标(如
java.lang.Object)。 - 直接引用:指向目标的指针、偏移量或句柄。
- 解析目标:
- 类/接口解析(如
com.example.MyClass)。 - 字段解析(如
MyClass.myField)。 - 方法解析(如
MyClass.myMethod())。
- 类/接口解析(如
5️⃣ 初始化(Initialization)
- 功能:执行类构造器
<clinit>()方法,完成静态变量赋值和静态代码块逻辑。 - 关键规则:
- JVM保证子类初始化前,父类已初始化。
<clinit>()由编译器自动生成,合并所有静态变量赋值和静态代码块。- 多线程环境下,JVM会加锁确保仅初始化一次。
类加载的触发时机
- 主动引用(触发初始化):
new对象、访问静态变量/方法(非final常量)、反射调用类。- 初始化子类时,父类未初始化会触发父类初始化。
- 被动引用(不触发初始化):
- 通过子类引用父类的静态字段。
- 定义类的数组(如
MyClass[] arr = new MyClass[10])。 - 访问
static final常量(值在编译期确定)。
双亲委派模型(Parent Delegation)
- 机制:类加载请求优先委派给父加载器处理,若父类无法完成,子加载器才尝试加载。
- 目的:
- 避免重复加载,确保核心类(如
java.lang.Object)唯一性。 - 防止用户自定义类替换核心类(安全沙箱)。
- 避免重复加载,确保核心类(如
类卸载(Unloading)
- 条件:类的
Class对象无引用,且对应的类加载器被回收。 - 场景:由JVM的垃圾回收机制自动完成,常见于动态生成的类(如JSP、OSGi)。
示例流程:
public class MyClass {
public static int x = 10; // 准备阶段x=0,初始化阶段x=10
static {
System.out.println("Initialized!"); // 初始化阶段执行
}
}
- 加载:找到
MyClass.class并创建Class对象。 - 验证:检查字节码合法性。
- 准备:为
x分配内存并赋初始值0。 - 解析:处理
System.out的符号引用。 - 初始化:执行
<clinit>(),将x设为10,打印“Initialized!”。
通过这一机制,JVM实现了类的动态加载、安全隔离和高效内存管理。
三、垃圾回收
以下是Java垃圾回收(Garbage Collection, GC)机制的详细解析,涵盖核心原理、算法及实践要点:
垃圾回收的本质
目标:自动回收不再使用的对象内存,避免内存泄漏,减少手动内存管理的复杂性。
关键概念
-
堆内存结构(以HotSpot VM为例):
- 新生代(Young Generation)
- Eden区:对象初次分配区域
- Survivor区(From/To):存活对象过渡区
- 老年代(Old Generation):长期存活对象
- 元空间(Metaspace):类元信息(替代永久代)
- 新生代(Young Generation)
-
对象存活判定:
- 引用计数法(Java未采用):循环引用问题
- 可达性分析(Java核心算法):
- 从GC Roots(栈局部变量、静态变量、JNI引用等)出发,不可达对象标记为垃圾
经典垃圾回收算法
| 算法 | 过程 | 优点 | 缺点 | 应用场景 |
|---|---|---|---|---|
| 标记-清除 | 1. 标记存活对象 2. 清除未标记对象 | 简单 | 内存碎片 | CMS老年代回收 |
| 复制算法 | 将存活对象复制到新内存区 | 无碎片,高效 | 内存利用率50% | 新生代(Eden→Survivor) |
| 标记-整理 | 标记后压缩存活对象到内存一端 | 无碎片 | 移动对象开销大 | Serial Old、G1 |
| 分代收集 | 新生代用复制,老年代用标记-清除/整理 | 针对不同生命周期优化 | 需配合不同算法实现 | 所有现代JVM |
垃圾收集器对比
| 收集器 | 工作模式 | 特点 | 适用场景 |
|---|---|---|---|
| Serial | 单线程 | 简单高效,全程STW | 客户端小内存应用 |
| Parallel | 多线程 | 吞吐量优先(JDK8默认) | 后台计算型应用 |
| CMS | 并发 | 低停顿,分四阶段:初始标记→并发标记→重新标记→并发清除 | 响应优先的Web应用 |
| G1 | 分区+并发 | 将堆划分为Region,可预测停顿(JDK9+默认) | 大内存混合负载 |
| ZGC | 并发+染色指针 | 亚毫秒级停顿(<10ms),支持TB级堆 | 超低延迟金融/电信系统 |
| Shenandoah | 并发 | 与ZGC竞争,低延迟且无需额外硬件支持 | 通用低延迟场景 |
内存分配与回收策略
- 对象优先在Eden分配
- 新生代内存不足时触发 Minor GC
- 大对象直接进老年代
- 通过
-XX:PretenureSizeThreshold设定阈值(默认0,由收集器决定)
- 通过
- 长期存活对象晋升老年代
- 年龄计数器(
-XX:MaxTenuringThreshold,默认15)
- 年龄计数器(
- 动态年龄判定
- Survivor区中相同年龄对象总大小超过Survivor一半时,≥该年龄的对象直接晋升
GC触发条件
| GC类型 | 触发条件 |
|---|---|
| Minor GC | Eden区满时 |
| Full GC | 1. 老年代空间不足 2. 方法区(元空间)不足 3. 调用 System.gc()等 |
调优关键指标
- 吞吐量:用户代码运行时间 / (用户代码时间 + GC时间)
(通常要求 > 95%) - 停顿时间:单次GC导致应用暂停的时长
(CMS/G1目标:100-200ms;ZGC目标:<10ms) - 内存占用:堆空间大小与系统物理内存的平衡
常用JVM参数示例
// 基础配置
-Xms4g -Xmx4g // 堆初始/最大内存
-XX:NewRatio=2 // 新生代:老年代=1:2
-XX:SurvivorRatio=8 // Eden:Survivor=8:1:1
// 收集器选择
-XX:+UseG1GC // 启用G1
-XX:+UseConcMarkSweepGC // 启用CMS
// GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
GC优化实践
- 避免过早晋升老年代
- 增加新生代大小(
-Xmn) - 增大Survivor区(调整
-XX:SurvivorRatio)
- 增加新生代大小(
- 降低Full GC频率
- 监控老年代使用率(
jstat -gcutil) - 避免大量短生命周期大对象
- 监控老年代使用率(
- 选择合适收集器
- 小堆(<4G):Parallel GC
- 中等堆(4-16G):G1 GC
- 超大堆(>16G):ZGC/Shenandoah
监控工具推荐
- 命令行:
jstat、jmap、jcmd - 图形工具:VisualVM、JConsole、GCViewer
- APM工具:Prometheus+Grafana(配合JMX exporter)
示例分析:
// 内存泄漏典型场景:静态集合持有对象引用
public class MemoryLeak {
static List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
while(true) {
list.add(new byte[1024 * 1024]); // 不断向老年代填充对象
}
}
}
- 现象:频繁Full GC,最终
OutOfMemoryError - 解决方案:检查长生命周期集合的清理逻辑
通过合理配置垃圾回收策略,可以在吞吐量、延迟、内存占用之间取得最佳平衡,这是Java高性能应用调优的核心技能之一。