Java面试实战系列【JVM篇】- JVM内存结构与运行时数据区详解(共享区域)
文章目录
- 四、线程共享区域详解
- 4.1 Java堆(Heap)
- 4.1.1 堆内存概述
- 4.1.2 堆内存分代结构
- 4.1.3 新生代详解
- 4.1.4 老年代详解
- 4.1.5 对象分配流程详解
- 4.1.6 TLAB(Thread Local Allocation Buffer)详解
- 4.1.7 堆内存参数设置
- 4.1.8 堆内存溢出分析
- 4.2 方法区(Method Area)
- 4.2.1 方法区的定义和作用
- 4.2.2 方法区存储内容详解
- 4.2.3 运行时常量池深度解析
- 4.2.4 JDK版本演进:永久代到元空间
- 4.2.5 元空间详解
- 4.2.6 方法区参数配置
- 4.2.7 方法区垃圾回收
四、线程共享区域详解
线程共享区域是JVM内存结构中最复杂也是最重要的部分,包括Java堆和方法区。这两个区域被所有线程共享,是垃圾回收器工作的主要目标,也是大多数内存溢出异常的发生地。深入理解这些区域的工作原理对于Java程序的性能优化和故障排查至关重要。
4.1 Java堆(Heap)
4.1.1 堆内存概述
Java堆是JVM管理的内存中最大的一块区域,也是垃圾收集器管理的主要区域。在虚拟机启动时创建,几乎所有的对象实例以及数组都在这里分配内存。
堆内存的基本特征:
- 线程共享:所有线程都可以访问堆内存中的对象
- 动态分配:对象的创建和销毁都是动态进行的
- 垃圾回收目标:是垃圾收集器工作的主要区域
- 可调整大小:可以通过JVM参数调整堆的大小
- 物理不连续:可以处于物理上不连续的内存空间中,只要逻辑上连续即可
4.1.2 堆内存分代结构
分代假说理论基础
堆内存采用分代设计是基于以下观察得出的假说:
-
弱分代假说:绝大多数对象都是朝生夕灭的
具体含义:
- 新创建的对象很快就会变成垃圾(不再被引用)
- 大部分对象的生命周期都很短暂
- 在程序运行过程中,年轻对象比老对象更容易死亡
-
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
具体含义:
- 存活时间越长的对象,未来继续存活的概率越大
- 老对象之间相互引用的概率远大于老对象引用新对象的概率
- 老对象很少会引用新创建的对象
基于这些假说,将堆分为不同的代,采用不同的回收策略:
默认空间分配比例:
- 新生代 : 老年代 = 1 : 2 (可通过-XX:NewRatio调整)
- Eden : Survivor0 : Survivor1 = 8 : 1 : 1 (可通过-XX:SurvivorRatio调整)
分代收集的优势:
- 针对性回收:新生代采用复制算法,老年代采用标记-清除或标记-整理算法
- 效率提升:大部分垃圾回收只需要处理新生代
- 减少扫描:避免扫描整个堆空间
- 优化分配:新对象优先在Eden区分配,分配速度快
4.1.3 新生代详解
Eden区(伊甸园区)
Eden区是新对象的诞生地,几乎所有新创建的对象都首先在这里分配内存:
public class EdenAllocationExample {public static void main(String[] args) {// 这些对象都会在Eden区分配String str1 = new String("Hello");StringBuilder sb = new StringBuilder();List<Integer> list = new ArrayList<>();// 大量小对象的分配for (int i = 0; i < 1000; i++) {Object obj = new Object(); // 在Eden区快速分配}}
}
Eden区的特点:
- 快速分配:使用指针碰撞(Bump Pointer)技术,分配速度极快
- 无碎片:连续分配,不存在内存碎片问题
- 定期清理:当Eden区满时触发Minor GC
- TLAB优化:每个线程在Eden区都有自己的本地分配缓冲区
Survivor区(幸存者区)
Survivor区由两个大小相等的区域组成:From Survivor和To Survivor,它们的作用是存放经过一次Minor GC后仍然存活的对象:
Survivor区的工作机制:
- 交替使用:两个Survivor区在每次GC时角色互换
- 复制算法:采用复制算法,确保其中一个Survivor区始终为空
- 年龄计数:每经历一次Minor GC,对象年龄增加1
- 晋升条件:年龄达到阈值或Survivor区空间不足时晋升到老年代
对象年龄与晋升机制:
-
初始年龄:对象在Eden区创建时,年龄为0
-
年龄增长:每次Minor GC后,如果对象仍然存活,年龄+1
-
年龄上限:在HotSpot虚拟机中,对象年龄最大为15(4位二进制表示)
-
存储位置:对象年龄信息存储在对象头的Mark Word中
完整的年龄增长流程
第一次GC前:
Eden区:对象A(年龄=0)
Survivor0:空
Survivor1:空第一次Minor GC后:
Eden区:清空
Survivor0:对象A(年龄=1)
Survivor1:空第二次Minor GC后:
Eden区:清空
Survivor0:空
Survivor1:对象A(年龄=2)第三次Minor GC后:
Eden区:清空
Survivor0:对象A(年龄=3)
Survivor1:空... 如此循环,年龄逐步增长
动态年龄判定:
除了固定的年龄阈值外,还有动态年龄判定机制:
- 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
4.1.4 老年代详解
老年代的作用与特征
老年代主要存储长期存活的对象,具有以下特征:
- 生命周期长:对象已经经过多次GC仍然存活
- 空间较大:通常占堆内存的2/3
- GC频率低:Major GC或Full GC频率远低于Minor GC
- 回收耗时:由于空间大、对象多,回收时间较长
垃圾回收:
- 老年代满时触发Major GC或Full GC
- 回收频率低,但耗时长
对象进入老年代的条件:
空间分配担保机制:
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间:
情况一:老年代空间充足(担保成功)
条件:老年代最大可用连续空间 > 新生代所有对象总空间
处理方式:
- 直接进行Minor GC
- 确保安全性:即使新生代所有对象都存活并晋升,老年代也能容纳
- 最理想情况:这是最安全且高效的情况
情况二:空间不足但历史担保成功(冒险Minor GC)
条件:
- 老年代可用空间 < 新生代所有对象总空间
- 老年代可用空间 > 历史平均晋升大小
处理方式:
- 基于经验判断:根据历史GC数据进行风险评估
- 尝试Minor GC:大概率能够成功
- 准备后备方案:如果失败则进行Full GC
情况三:担保失败(直接Full GC)
条件:
- 老年代可用空间 < 新生代所有对象总空间
- 老年代可用空间 < 历史平均晋升大小
处理方式:
- 跳过Minor GC:直接进行Full GC
- 回收整个堆:包括新生代和老年代
- 确保安全性:避免晋升失败的风险
4.1.5 对象分配流程详解
完整的对象分配决策树:
4.1.6 TLAB(Thread Local Allocation Buffer)详解
TLAB的概念与原理
TLAB(Thread Local Allocation Buffer)是Eden区内每个线程私有的分配缓冲区,专门用于解决多线程并发分配对象时的同步问题。每个线程都有自己独立的TLAB空间,在其中分配对象时无需加锁同步。
TLAB的工作机制:
分配流程:
public class TLABWorkflow {// 模拟TLAB分配流程public Object allocateObject(int objectSize) {Thread currentThread = Thread.currentThread();TLAB tlab = currentThread.getTLAB();// 尝试在TLAB中分配if (tlab.hasSpace(objectSize)) {return tlab.allocate(objectSize); // 无锁分配} else {// TLAB空间不足,申请新的TLABreturn allocateInNewTLAB(objectSize);}}private Object allocateInNewTLAB(int objectSize) {// 1. 废弃当前TLAB的剩余空间// 2. 从Eden区申请新的TLAB(需要同步)// 3. 在新TLAB中分配对象return newTLAB.allocate(objectSize);}
}
具体工作流程:
- 初始分配尝试:
- 当线程需要创建新对象时,首先检查当前线程的TLAB是否有足够空间
- 如果TLAB中有足够空间,直接在TLAB中进行无锁分配,这是最快的分配方式
- 分配过程使用简单的指针碰撞技术,只需将TLAB的顶部指针向前移动
- TLAB空间不足处理:
- 当TLAB剩余空间不足以分配当前对象时,不会立即放弃TLAB
- 系统会废弃当前TLAB的剩余空间(这部分空间会被浪费)
- 向Eden区申请一个新的TLAB空间(这个过程需要同步,因为要访问共享的Eden区)
- 在新分配的TLAB中完成对象分配
- 大对象特殊处理:
- 如果要分配的对象大小超过TLAB的最大容量,则绕过TLAB机制
- 直接在Eden区的公共区域进行分配(需要同步)
- 这样避免了为大对象分配过大的TLAB而造成内存浪费
- TLAB生命周期管理:
- TLAB随线程创建而创建,随线程销毁而销毁
- 当发生Minor GC时,所有TLAB都会被重新初始化
- 线程在GC后会获得新的TLAB空间
TLAB大小计算:TLAB的大小计算是一个动态过程,需要考虑多个因素以平衡性能和内存利用率:
// TLAB大小计算逻辑(简化版)
public class TLABSizing {public int calculateTLABSize() {int edenSize = getEdenSize();int threadCount = getActiveThreadCount();int allocationRate = getAllocationRate();// 基础大小:Eden区大小 / 线程数int baseSize = edenSize / threadCount;// 根据分配速率调整int adjustedSize = baseSize * allocationRate / 100;// 限制在最小值和最大值之间return Math.max(MIN_TLAB_SIZE, Math.min(MAX_TLAB_SIZE, adjustedSize));}
}
基础大小计算:
- 起始点是Eden区总大小除以当前活跃线程数
- 这确保每个线程都能获得相对公平的分配空间
- 公式:基础大小 = Eden区大小 / 活跃线程数
分配速率调整:
- 系统会监控线程的对象分配速率
- 分配频繁的线程会获得更大的TLAB
- 分配较少的线程会获得相对较小的TLAB
- 调整公式:调整后大小 = 基础大小 × (分配速率 / 100)
边界限制:
- 系统设置了TLAB的最小值和最大值边界
- 最小值确保TLAB有基本的可用性,避免过于频繁的TLAB更新
- 最大值防止单个线程占用过多Eden区空间,影响其他线程
- 最终大小 = max(最小值, min(最大值, 调整后大小))
动态调整机制:
- TLAB大小不是固定的,会根据运行时情况动态调整
- 如果线程的TLAB经常不够用,下次分配时会增大TLAB
- 如果线程的TLAB浪费率较高,下次会适当减小TLAB
- 系统维护每个线程的分配统计信息用于大小调整
TLAB的优势与限制:
优势:
- 无锁分配:避免了多线程分配对象时的同步开销
- 减少竞争:降低了Eden区分配指针的竞争
- 提高性能:显著提高了对象分配的性能
- 局部性优化:同一线程创建的对象在内存中相邻,提高缓存命中率
限制:
- 空间浪费:TLAB废弃时的剩余空间会被浪费
- 大对象限制:超过TLAB大小的对象无法在TLAB中分配
- 线程数影响:线程过多时,每个TLAB变小,效果降低
TLAB相关参数配置:
# TLAB基本参数
-XX:+UseTLAB # 启用TLAB(默认开启)
-XX:TLABSize=256k # 设置TLAB初始大小
-XX:+ResizeTLAB # 允许动态调整TLAB大小
-XX:TLABWasteTargetPercent=1 # TLAB浪费空间的目标百分比# TLAB调试参数
-XX:+PrintTLAB # 打印TLAB分配信息
-XX:+TraceTLAB # 跟踪TLAB操作
4.1.7 堆内存参数设置
参数 | 作用 | 示例 |
---|---|---|
-Xms | 初始堆大小 | -Xms512m |
-Xmx | 最大堆大小 | -Xmx2g |
-Xmn | 新生代大小 | -Xmn256m |
-XX:NewRatio | 老年代/新生代比值 | -XX:NewRatio=2 |
-XX:SurvivorRatio | Eden/Survivor比值 | -XX:SurvivorRatio=8 |
-XX:+UseTLAB | 启用TLAB | 默认开启 |
4.1.8 堆内存溢出分析
常见原因:
- 内存泄漏:对象无法被回收
- 内存溢出:对象确实需要这么多内存
- 堆大小设置不合理
堆内存溢出的排查步骤:
内存分析工具使用示例:
# 1. 配置JVM参数生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/heapdump.hprof# 2. 使用jmap手动生成堆转储
jmap -dump:format=b,file=heap.hprof <pid># 3. 使用MAT分析堆转储文件的关键步骤
# - 打开堆转储文件
# - 查看Overview页面,关注内存使用最多的类
# - 运行Leak Suspects Report查找可能的内存泄漏
# - 分析Dominator Tree找出占用内存最多的对象
# - 查看GC Roots到泄漏对象的路径
4.2 方法区(Method Area)
4.2.1 方法区的定义和作用
方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然被称为"方法区",但它存储的不仅仅是方法代码,更准确地说,它是类级别元数据的存储区域。
方法区的核心作用:
- 类元数据存储:保存类的结构信息,如类名、访问修饰符、字段信息、方法信息等
- 常量池管理:运行时常量池是方法区的一部分,存储编译期生成的字面量和符号引用
- 静态变量存储:类的静态变量(类变量)在类加载时分配内存
- 方法字节码存储:存储方法的字节码指令
- JIT编译代码:即时编译器编译后的本地机器码
方法区与类加载的关系:
方法区的内容主要在类加载过程中填充:
public class MethodAreaExample {// 静态变量存储在方法区private static int staticVar = 100;private static final String CONSTANT = "Hello World";// 实例变量存储在堆中private int instanceVar = 200;// 方法字节码存储在方法区public static void staticMethod() {System.out.println("静态方法的字节码存储在方法区");}public void instanceMethod() {System.out.println("实例方法的字节码也存储在方法区");}
}
当这个类被加载时,JVM会在方法区创建:
- 类
MethodAreaExample
的元数据信息 - 静态变量
staticVar
和CONSTANT
的存储空间 - 方法
staticMethod
和instanceMethod
的字节码 - 相关的常量池项
4.2.2 方法区存储内容详解
类型信息(Type Information)
方法区为每个加载的类型(类、接口、枚举、注解)存储以下信息:
// 示例类,展示方法区需要存储的各种信息
public class CompleteExample extends BaseClass implements InterfaceA, InterfaceB {// 字段信息public static final String PUBLIC_CONSTANT = "公共常量";private static int privateStaticField = 10;protected int protectedField;private final int finalField = 20;// 构造方法信息public CompleteExample() {this.protectedField = 30;}public CompleteExample(int value) {this.protectedField = value;}// 方法信息public static void publicStaticMethod() { }private void privateMethod() { }protected final void protectedFinalMethod() { }// 内部类信息static class StaticNestedClass { }class InnerClass { }
}
对于上述类,方法区存储的类型信息包括:
1. 基本类型信息:
- 类的全限定名:com.example.CompleteExample
- 直接父类的全限定名:com.example.BaseClass
- 类的访问标志:public
- 实现的接口列表:[InterfaceA, InterfaceB]
- 类的类型:class(不是interface、enum或annotation)
2. 字段信息表:
字段表结构:
- 字段名称:PUBLIC_CONSTANT
- 字段类型:Ljava/lang/String;
- 访问标志:public static final
- 字段属性:ConstantValue指向常量池中的字符串- 字段名称:privateStaticField
- 字段类型:I (int)
- 访问标志:private static
- 初始值:10
3. 方法信息表:
当一个类被加载到JVM时,类的每个方法都会在方法区中创建对应的方法信息表,存储该方法的完整元数据。
- 方法名称(methodName)
就是方法的简单名称,比如:
public void sayHello() { } // methodName: "sayHello"
public static void main(String[] args) { } // methodName: "main"
- 方法描述符(methodDescriptor)
这是最重要但最容易混淆的部分,它用特殊格式描述方法的参数类型和返回类型:
格式:(参数类型列表)返回类型
类型对应表:
V
= voidI
= intJ
= longF
= floatD
= doubleZ
= booleanC
= charB
= byteS
= shortL类名;
= 对象类型(如Ljava/lang/String;
表示String)[
= 数组(如[I
表示int数组,[Ljava/lang/String;
表示String数组)
具体示例:
public void method1() { }
// 方法描述符: "()V" - 无参数,返回voidpublic int method2(String str) { }
// 方法描述符: "(Ljava/lang/String;)I" - 接受String参数,返回intpublic String method3(int a, double b, boolean c) { }
// 方法描述符: "(IDZ)Ljava/lang/String;" - 接受int、double、boolean参数,返回Stringpublic void method4(int[] arr, String[] strs) { }
// 方法描述符: "([I[Ljava/lang/String;)V" - 接受int数组和String数组,返回voidpublic Object method5(List<String> list, Map<String, Integer> map) { }
// 方法描述符: "(Ljava/util/List;Ljava/util/Map;)Ljava/lang/Object;"
// 注意:泛型信息在方法描述符中会被擦除
- 访问标志(accessFlags)
使用位标志组合表示方法的访问权限和特性:
常见标志位:
ACC_PUBLIC = 0x0001 // public方法
ACC_PRIVATE = 0x0002 // private方法
ACC_PROTECTED = 0x0004 // protected方法
ACC_STATIC = 0x0008 // static方法
ACC_FINAL = 0x0010 // final方法
ACC_SYNCHRONIZED= 0x0020 // synchronized方法
ACC_NATIVE = 0x0100 // native方法
ACC_ABSTRACT = 0x0400 // abstract方法
示例:
public static void main(String[] args) { }
// accessFlags: ACC_PUBLIC | ACC_STATIC = 0x0001 | 0x0008 = 0x0009private synchronized void doSomething() { }
// accessFlags: ACC_PRIVATE | ACC_SYNCHRONIZED = 0x0002 | 0x0020 = 0x0022public final native int nativeMethod();
// accessFlags: ACC_PUBLIC | ACC_FINAL | ACC_NATIVE = 0x0001 | 0x0010 | 0x0100 = 0x0111
- 方法字节码(bytecode)
存储方法编译后的JVM字节码指令序列:
简单示例:
public int add(int a, int b) {return a + b;
}// 对应的字节码:
// 0: iload_1 // 加载参数a到操作数栈
// 1: iload_2 // 加载参数b到操作数栈
// 2: iadd // 执行整数加法
// 3: ireturn // 返回整数结果// 在方法信息表中存储为:[iload_1, iload_2, iadd, ireturn]
- 异常表(ExceptionTable)
记录方法中try-catch块的信息:
public void handleException() {try { // start_pc: 0riskyOperation(); } catch (IOException e) { // handler_pc: 10, catch_type: IOExceptionhandleIO(e);} catch (Exception e) { // handler_pc: 20, catch_type: Exception handleGeneral(e);} // end_pc: 30
}// 异常表结构:
// [
// {start_pc: 0, end_pc: 8, handler_pc: 10, catch_type: "java/io/IOException"},
// {start_pc: 0, end_pc: 8, handler_pc: 20, catch_type: "java/lang/Exception"}
// ]
- 属性表(AttributeInfo)
存储方法的额外信息:
行号表(LineNumberTable):
public void example() { // 源码行号: 15int x = 10; // 源码行号: 16 System.out.println(x); // 源码行号: 17
}// 行号表:
// [
// {start_pc: 0, line_number: 15}, // 方法开始对应源码第15行
// {start_pc: 2, line_number: 16}, // pc=2的指令对应源码第16行
// {start_pc: 5, line_number: 17} // pc=5的指令对应源码第17行
// ]
局部变量表(LocalVariableTable):
public void calculate(int param) {String local = "test";for (int i = 0; i < 10; i++) {// ...}
}// 局部变量表:
// [
// {name: "this", descriptor: "Lcom/example/MyClass;", index: 0, start_pc: 0, length: 25},
// {name: "param", descriptor: "I", index: 1, start_pc: 0, length: 25},
// {name: "local", descriptor: "Ljava/lang/String;", index: 2, start_pc: 3, length: 22},
// {name: "i", descriptor: "I", index: 3, start_pc: 8, length: 15}
// ]
完整的方法信息表示例
public class Example {public static int multiply(int a, int b) {try {return a * b;} catch (ArithmeticException e) {return 0;}}
}// 对应的方法信息表:
// {
// methodName: "multiply",
// methodDescriptor: "(II)I", // 两个int参数,返回int
// accessFlags: 0x0009, // ACC_PUBLIC | ACC_STATIC
// bytecode: [
// iload_0, // 加载参数a
// iload_1, // 加载参数b
// imul, // 整数乘法
// ireturn, // 返回结果
// // ... 异常处理相关字节码
// ],
// exceptionTable: [
// {start_pc: 0, end_pc: 3, handler_pc: 4, catch_type: "java/lang/ArithmeticException"}
// ],
// attributes: [
// LineNumberTable: [...],
// LocalVariableTable: [...]
// ]
// }
常量池详细结构
运行时常量池是方法区的重要组成部分,包含各种类型的常量:
符号引用的解析过程:
public class SymbolicReferenceExample {public void demonstrateResolution() {// 编译时:生成符号引用 java/lang/System.out:Ljava/io/PrintStream;// 运行时:解析为直接引用,指向System.out字段的内存地址System.out.println("Hello");// 编译时:生成符号引用 java/lang/String.length:()I// 运行时:解析为直接引用,指向String.length方法的入口地址String str = "test";int len = str.length();}
}
4.2.3 运行时常量池深度解析
运行时常量池的动态性
与Class文件中的常量池相比,运行时常量池具有动态性,可以在运行期间添加新的常量:
public class RuntimeConstantPoolExample {public static void demonstrateDynamicNature() {// String.intern()方法可以向运行时常量池添加字符串常量String str1 = new StringBuilder("计算机").append("软件").toString();System.out.println(str1.intern() == str1); // JDK7+中为trueString str2 = new StringBuilder("ja").append("va").toString();System.out.println(str2.intern() == str2); // false,因为"java"已在常量池中// 反射也可能向常量池添加新内容Class<?> clazz = String.class;Method[] methods = clazz.getDeclaredMethods(); // 可能添加方法相关常量}
}
常量池中不同类型常量的存储:
- CONSTANT_Utf8_info:存储字符串字面量
public class Utf8ConstantExample {// 以下字符串都会作为CONSTANT_Utf8_info存储在常量池中private String field1 = "字符串字面量1";private String field2 = "字符串字面量2";public void method() {String local = "局部字符串字面量";}
}
- CONSTANT_Class_info:存储类或接口的符号引用
public class ClassConstantExample {// 这些类引用会生成CONSTANT_Class_info常量private Object obj; // java/lang/Objectprivate String str; // java/lang/Stringprivate List<Integer> list; // java/util/List, java/lang/Integer
}
- CONSTANT_FieldRef_info:存储字段的符号引用
public class FieldRefExample {private static int staticField = 100;public void accessFields() {// 访问静态字段生成CONSTANT_FieldRef_infoint value = FieldRefExample.staticField;// 访问System.out也生成字段引用常量System.out.println(value);}
}
常量池的内存管理
public class ConstantPoolMemoryManagement {/*** 演示常量池内存使用的代码* VM参数:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m*/public static void main(String[] args) {List<String> list = new ArrayList<>();int i = 0;try {while (true) {// 不断向常量池添加新的字符串常量list.add(String.valueOf(i++).intern());}} catch (OutOfMemoryError e) {System.out.println("常量池内存溢出,添加了 " + i + " 个常量");throw e;}}
}
4.2.4 JDK版本演进:永久代到元空间
永久代时期(JDK 7及之前)
在JDK 7及之前,方法区的实现称为永久代(Permanent Generation),它是堆内存的一部分:
永久代的问题:
- 大小限制:永久代有固定的大小限制,容易发生OutOfMemoryError
- GC效率低:永久代的垃圾回收效率低,影响应用性能
- 调优困难:很难确定永久代的合适大小
/*** 永久代内存溢出示例(JDK 7及之前)* VM参数:-XX:PermSize=10M -XX:MaxPermSize=10M*/
public class PermGenOOMExample {public static void main(String[] args) {try {while (true) {// 使用CGLib动态生成类,消耗永久代空间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();}} catch (OutOfMemoryError e) {System.out.println("PermGen space OutOfMemoryError");throw e;}}static class OOMObject {}
}
元空间时期(JDK 8开始)
JDK 8完全移除了永久代,引入了元空间(Metaspace):
元空间的优势:
- 动态调整:元空间大小可以动态调整,默认无上限
- 减少OOM:大大减少了因方法区空间不足导致的OutOfMemoryError
- 简化GC:元空间的垃圾回收更加高效
- 统一管理:与堆内存分离,便于独立管理和调优
/*** 元空间使用示例(JDK 8+)* VM参数:-XX:MetaspaceSize=21m -XX:MaxMetaspaceSize=21m*/
public class MetaspaceExample {public static void main(String[] args) {try {while (true) {// 使用CGLib动态生成类,消耗元空间Enhancer enhancer = new Enhancer();enhancer.setSuperclass(TestClass.class);enhancer.setUseCache(false);enhancer.setCallback(NoOp.INSTANCE);enhancer.create();}} catch (OutOfMemoryError e) {System.out.println("Metaspace OutOfMemoryError");throw e;}}static class TestClass {}
}
版本迁移的影响:
特性 | 永久代(JDK 7-) | 元空间(JDK 8+) |
---|---|---|
位置 | 堆内存的一部分 | 直接内存(Native Memory) |
大小限制 | 固定大小,容易OOM | 默认无限制,受系统内存限制 |
垃圾回收 | 随堆一起进行Full GC | 独立的垃圾回收机制 |
参数设置 | -XX:PermSize -XX:MaxPermSize | -XX:MetaspaceSize -XX:MaxMetaspaceSize |
字符串常量池 | 在永久代中(JDK6-) 在堆中(JDK7) | 在堆中 |
类卸载 | 困难,条件苛刻 | 更容易,条件相对宽松 |
4.2.5 元空间详解
元空间的内存结构
元空间主要包含两个部分:
- Klass Metaspace:存储类的元数据信息
- NoKlass Metaspace:存储除类元数据之外的其他元数据
元空间的分配机制
元空间使用类似堆的分配策略,但更加简单:
// 概念性代码,展示元空间分配逻辑
public class MetaspaceAllocation {// 每个类加载器都有自己的元空间分配器private MetaspaceAllocator allocator;public MetaData allocateMetaData(int size) {// 1. 尝试从当前chunk分配if (currentChunk.hasSpace(size)) {return currentChunk.allocate(size);}// 2. 当前chunk不足,申请新的chunkChunk newChunk = allocateNewChunk(size);currentChunk = newChunk;return newChunk.allocate(size);}private Chunk allocateNewChunk(int minimumSize) {// 根据需求大小确定chunk大小int chunkSize = calculateChunkSize(minimumSize);return virtualSpaceManager.allocateChunk(chunkSize);}
}
元空间的垃圾回收
元空间的垃圾回收与堆的垃圾回收是独立的:
public class MetaspaceGCExample {/*** 演示元空间垃圾回收* VM参数:-XX:+TraceClassLoading -XX:+TraceClassUnloading*/public static void main(String[] args) throws Exception {// 创建自定义类加载器for (int i = 0; i < 1000; i++) {createClassLoader();// 触发Full GC,可能回收元空间if (i % 100 == 0) {System.gc();Thread.sleep(100);}}}private static void createClassLoader() throws Exception {// 创建URLClassLoader加载动态生成的类URLClassLoader loader = new URLClassLoader(new URL[]{});// 使用ASM动态生成类ClassWriter cw = new ClassWriter(0);cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "DynamicClass" + System.currentTimeMillis(), null, "java/lang/Object", null);byte[] classData = cw.toByteArray();// 通过反射调用defineClass方法Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);defineClass.setAccessible(true);Class<?> clazz = (Class<?>) defineClass.invoke(loader, "DynamicClass", classData, 0, classData.length);// 类加载器和相关的元数据会在GC时被回收}
}
元空间的监控
public class MetaspaceMonitoring {public static void printMetaspaceUsage() {// 获取元空间使用情况List<MemoryPoolMXBean> memoryPools = ManagementFactory.getMemoryPoolMXBeans();for (MemoryPoolMXBean pool : memoryPools) {if (pool.getName().contains("Metaspace")) {MemoryUsage usage = pool.getUsage();System.out.println("=== " + pool.getName() + " ===");System.out.println("已使用: " + usage.getUsed() / 1024 / 1024 + " MB");System.out.println("已提交: " + usage.getCommitted() / 1024 / 1024 + " MB");if (usage.getMax() > 0) {System.out.println("最大值: " + usage.getMax() / 1024 / 1024 + " MB");System.out.println("使用率: " + String.format("%.2f%%", (double) usage.getUsed() / usage.getMax() * 100));} else {System.out.println("最大值: 无限制");}System.out.println();}}}public static void setMetaspaceWarning() {// 设置元空间使用率预警List<MemoryPoolMXBean> pools = ManagementFactory.getMemoryPoolMXBeans();for (MemoryPoolMXBean pool : pools) {if (pool.getName().contains("Metaspace") && pool.isUsageThresholdSupported()) {// 设置80%使用率预警long threshold = (long) (pool.getUsage().getMax() * 0.8);pool.setUsageThreshold(threshold);// 注册监听器MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();NotificationEmitter emitter = (NotificationEmitter) memoryBean;emitter.addNotificationListener((notification, handback) -> {System.out.println("元空间使用率告警: " + notification.getMessage());}, null, null);}}}
}
4.2.6 方法区参数配置
JDK版本对应的参数
# JDK 7及之前(永久代配置)
-XX:PermSize=128m # 永久代初始大小
-XX:MaxPermSize=256m # 永久代最大大小# JDK 8及之后(元空间配置)
-XX:MetaspaceSize=128m # 元空间初始大小(触发GC的阈值)
-XX:MaxMetaspaceSize=256m # 元空间最大大小
-XX:CompressedClassSpaceSize=32m # 压缩类空间大小(默认1G)# 元空间相关的调试参数
-XX:+TraceClassLoading # 跟踪类加载
-XX:+TraceClassUnloading # 跟踪类卸载
-XX:+PrintGCDetails # 打印GC详细信息(包括元空间GC)
不同应用场景的参数调优:
- 微服务应用(类较少,启动快):
# 小型微服务,减少元空间初始大小
-XX:MetaspaceSize=64m
-XX:MaxMetaspaceSize=128m
-XX:CompressedClassSpaceSize=16m
- 企业级应用(类多,框架复杂):
# 大型企业应用,增加元空间大小
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:CompressedClassSpaceSize=64m
- 动态类生成应用(使用CGLib、ASM等):
# 大量动态生成类的应用
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=1024m
-XX:+CMSClassUnloadingEnabled # 启用类卸载(如果使用CMS)
参数调优实践:
public class MetaspacetuningExample {/*** 元空间调优监控代码*/public static void monitorAndTune() {MemoryPoolMXBean metaspacePool = null;// 找到Metaspace内存池for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {if ("Metaspace".equals(pool.getName())) {metaspacePool = pool;break;}}if (metaspacePool != null) {MemoryUsage usage = metaspacePool.getUsage();long used = usage.getUsed();long committed = usage.getCommitted();long max = usage.getMax();System.out.println("=== 元空间使用情况 ===");System.out.println("已使用: " + used / 1024 / 1024 + " MB");System.out.println("已提交: " + committed / 1024 / 1024 + " MB");if (max > 0) {System.out.println("最大值: " + max / 1024 / 1024 + " MB");double usageRate = (double) used / max;// 调优建议if (usageRate > 0.9) {System.out.println("警告: 元空间使用率过高 (" + String.format("%.1f%%", usageRate * 100) + ")");System.out.println("建议: 增加-XX:MaxMetaspaceSize参数");} else if (usageRate < 0.3 && committed > used * 2) {System.out.println("建议: 可以适当减小-XX:MetaspaceSize参数");}}// 监控GC频率long gcCount = getMetaspaceGCCount();if (gcCount > 0) {System.out.println("元空间GC次数: " + gcCount);if (gcCount > 10) { // 假设的阈值System.out.println("建议: 元空间GC频繁,考虑增加MetaspaceSize");}}}}private static long getMetaspaceGCCount() {long count = 0;for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {// CMS和G1等收集器会处理元空间if (gc.getName().contains("ConcurrentMarkSweep") || gc.getName().contains("G1")) {count += gc.getCollectionCount();}}return count;}
}
4.2.7 方法区垃圾回收
方法区垃圾回收的特点
方法区的垃圾回收主要针对两类数据:
- 废弃的常量:不再被任何地方引用的常量
- 无用的类:满足特定条件的类可以被卸载
常量回收机制
public class ConstantGCExample {public static void main(String[] args) {// 字符串常量的回收示例String str1 = new StringBuilder("Hello").append("World").toString();str1.intern(); // 将"HelloWorld"加入常量池str1 = null; // 断开引用// 如果没有其他引用指向"HelloWorld",它可能在GC时被回收System.gc();// 这时intern()可能返回不同的对象引用String str2 = new StringBuilder("Hello").append("World").toString();System.out.println(str2.intern() == str2); // 结果可能是true或false}
}
类卸载机制
类的卸载条件非常严格,需要同时满足三个条件:
- 该类所有的实例都已被回收
- 加载该类的ClassLoader已被回收
- 该类对应的java.lang.Class对象没有被引用
public class ClassUnloadingExample {/*** 演示类卸载的条件* VM参数:-XX:+TraceClassUnloading -verbose:class*/public static void main(String[] args) throws Exception {// 创建自定义类加载器URLClassLoader customLoader = new URLClassLoader(new URL[]{new File("custom_classes/").toURI().toURL()});// 加载一个类Class<?> clazz = customLoader.loadClass("com.example.CustomClass");Object instance = clazz.newInstance();System.out.println("类已加载: " + clazz.getName());// 第一步:销毁所有实例instance = null;// 第二步:销毁Class对象的引用clazz = null;// 第三步:销毁类加载器customLoader.close();customLoader = null;// 强制垃圾回收System.gc();Thread.sleep(1000);System.out.println("尝试触发类卸载");// 再次强制垃圾回收,观察类是否被卸载System.gc();}
}
Web应用中的类卸载问题
在Web应用服务器中,类卸载是一个常见问题:
public class WebAppClassLeakExample {// 这是一个导致类无法卸载的典型案例private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();public void doSomething() {// 在ThreadLocal中存储当前类的实例threadLocal.set(this);// 问题:如果忘记清理ThreadLocal,该类永远无法被卸载// 正确的做法应该在finally块中清理:// try {// // 业务逻辑// } finally {// threadLocal.remove();// }}// 另一个常见问题:静态引用private static List<Object> staticList = new ArrayList<>();public void addToStaticList(Object obj) {staticList.add(obj); // 这会阻止对象和类的回收}
}
方法区垃圾回收的监控
public class MethodAreaGCMonitoring {public static void monitorMethodAreaGC() {// 监控方法区GC的代码List<GarbageCollectorMXBean> gcBeans = ManagementFactory.getGarbageCollectorMXBeans();for (GarbageCollectorMXBean gcBean : gcBeans) {String[] poolNames = gcBean.getMemoryPoolNames();for (String poolName : poolNames) {if (poolName.contains("Metaspace") || poolName.contains("Perm")) {System.out.println("GC收集器: " + gcBean.getName());System.out.println("管理的内存池: " + poolName);System.out.println("GC次数: " + gcBean.getCollectionCount());System.out.println("GC总时间: " + gcBean.getCollectionTime() + "ms");System.out.println("---");}}}}public static void simulateMethodAreaGC() {/*** 模拟方法区垃圾回收* VM参数:-XX:+TraceClassUnloading -XX:MetaspaceSize=10m*/List<ClassLoader> loaders = new ArrayList<>();try {for (int i = 0; i < 1000; i++) {// 创建大量的类加载器和类,消耗元空间URLClassLoader loader = new URLClassLoader(new URL[]{});generateDynamicClass(loader, "DynamicClass" + i);loaders.add(loader);if (i % 100 == 0) {// 定期清理一些类加载器,触发类卸载for (int j = 0; j < 50 && !loaders.isEmpty(); j++) {ClassLoader oldLoader = loaders.remove(0);if (oldLoader instanceof URLClassLoader) {((URLClassLoader) oldLoader).close();}}// 强制GC,观察类卸载情况System.gc();System.out.println("第 " + i + " 轮,剩余类加载器: " + loaders.size());}}} catch (Exception e) {e.printStackTrace();}}private static void generateDynamicClass(ClassLoader loader, String className) {// 使用ASM生成动态类的代码(简化版)try {byte[] classData = generateClassBytes(className);Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);defineClass.setAccessible(true);defineClass.invoke(loader, className, classData, 0, classData.length);} catch (Exception e) {e.printStackTrace();}}private static byte[] generateClassBytes(String className) {// 实际应用中会使用ASM、CGLib等工具生成字节码// 这里返回一个简单的类字节码return new byte[1024]; // 占位符}
}