类加载子系统
类加载器整个工作流程
当一个类加载器收到类加载请求的时候
1、双亲委派检查:首先检查自己是否已经加载过此类
2、向上委托:如果没有加载,则请求委托给父类加载器
3、递归传递:父类加载器接到请求后,同样先检查是否已加载,然后委托给自己的父类加载器
4、到底返回:知道Bootstrap ClassLoader
5、向下尝试:如果父类加载器无法加载该类(找不到对应类文件),则子类加载器再尝试自己加载
类加载的触发时机
1、创建类的实例
2、访问类的静态变量(除final修饰的常量除外)
3、调用类的静态方法
4、使用java.lang.reflect包的方法对类惊醒反射调用
5、初始化一个类的子类
6、虚拟机启动时被标明为启动类的类
这个过程确保了类在需要的时候才被加载到JVM中,实现了按需加载,节省了内存资源
Class文件进入到类加载子系统
1、加载(loading)阶段
- 通过类的全限定名获取该类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法去运行时的数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个了的各种数据的访问入口
2、验证(Verification)阶段
-
1、文件格式验证:验证字节流是否符合Class文件格式规范
元数据验证:对字节码描述的信息进行语义分析
-
字节码验证:通过数据流和控制流分析,确保程序语义是合法的
-
符号引用验证:确保解析动作能正常执行
3、准备(Preparation)阶段
为类变量分配内存并设置类变量初始值(零值),这些变量所使用的内存都在方法去进行分配
4、解析(Resolution)阶段
将常量池内的符号引用替换为直接引用的过程
1、类或接口的解析
2、字段解析
3、类方法解析
4、接口方法解析
5、方法类型解析
6、方法句柄解析
7、调用点限定符解析
5、初始化(Initialization)阶段
执行类构造器<clinit>方法的过程,这是类加载过程的最后一步
1、由编译器自动收集类中的所有类变量赋值动作和静态语句块(static{})中语句合并产生的
2、与实例构造器<init>不同,不需要显式调用父类构造器
3、接口也有<clinit>方法,但不需要限制性父接口的<clinit>方法
运行时数据区
程序计数器
作用
1、记录当前线程执行的字节码指令地址
2、存储正在执行的虚拟机字节码指令的地址
3、存储正在执行的虚拟机字节码指令的地址
特点
1、线程私有:每个线程都有独立的程序计数器,各线程之间的程序计数器互不影响
2、不会发生内存溢出:程序计数器是唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域(OOM)
3、执行字节码指令:对于java方法,程序计数器记录正在执行的JVM字节码指令地址:对于Native方法,此计数器值为undefined
意义
1、线程切换支持:当CPU切换到其他线程的时候,原线程的程序计数器保存了上次执行的位置,当线程重新获得CPU时间片的时候,可以从原来的位置继续执行
2、异常处理支持:当程序发生异常的时候,可以通过程序计数器定位到具体的出错位置
3、方法调用跟踪:在方法调用过程中,程序计数器帮助维护执行流程
注意事项
1、内存占用小:程序计数器只存储一个指针,占用内存很小
2、声明周期同步:程序计数器的声明周期和线程项目,线程创建的时候创建,线程销毁的时候销毁
3、对开发者透明:程序计数器对开发者是完全透明的,无需手动管理
4、程序计数器虽然功能简单,但是他是多线程环境下保证程序正确执行的关键组件之一,确保了每个线程都能在正确的指令位置执行
java虚拟机栈
概述与基本结构
java虚拟机栈是线程私有的内存区域,每个线程都有自己的虚拟机栈,声明周期与线程相同。每当创建一个新线程,就会创建一个虚拟机栈;
虚拟机栈以栈帧为基本单位,每次调用方法都会创建一个新的栈帧并压入栈顶,方法执行完毕之后会被弹出
java
public class VMStackExample {
public static void main(String[] args) {
method1(); // 创建main方法的栈帧,然后调用method1
}
public static void method1() {
int a = 10; // 局部变量存储在栈帧的局部变量表中
method2(); // 创建method1的栈帧,然后调用method2
}
public static void method2() {
String str = "hello"; // 局部变量存储在栈帧中
// method2执行完毕,其栈帧被弹出
}
// method1执行完毕,其栈帧被弹出
// main方法执行完毕,其栈帧被弹出,线程结束
}
关于栈帧的创建时机详细如下所示
java
public class VMStackExample {
public static void main(String[] args) {
method1(); // 创建main方法的栈帧,然后调用method1
}
public static void method1() {
int a = 10; // 局部变量存储在栈帧的局部变量表中
method2(); // 创建method1的栈帧,然后调用method2
}
public static void method2() {
String str = "hello"; // 局部变量存储在栈帧中
// method2执行完毕,其栈帧被弹出
}
// method1执行完毕,其栈帧被弹出
// main方法执行完毕,其栈帧被弹出,线程结束
}
栈帧的组成
1、局部变量表
- 存储方法参数和方法内部定义的局部变量
- 每个槽位(Slot)可以存储32位数据类型(boolean、byte、char、short、int、float、reference、returnAddress)
- double和long需要两个连续槽位
java
public class LocalVariableTableExample {
public void exampleMethod(int param, String strParam) { // param和strParam存储在局部变量表
int localVar = 100; // localVar存储在局部变量表
Object obj = new Object(); // obj引用存储在局部变量表
double d = 3.14; // d需要两个槽位存储
// 局部变量表结构示意:
// slot 0: this (如果是实例方法)
// slot 1: param (int)
// slot 2: strParam (reference)
// slot 3: localVar (int)
// slot 4: obj (reference)
// slot 5-6: d (double, 占两个槽位)
}
}
2、操作数栈
- 用于计算的临时存储区域
- 执行字节码指令的时候,操作数从局部变量表或常量池取出压入操作数栈,计算完成之后结果压回栈
java
public class OperandStackExample {
public int calculate(int a, int b) {
int result = a + b; // 字节码执行过程:
// iload_1 (加载a到操作数栈)
// iload_2 (加载b到操作数栈)
// iadd (相加,结果压入操作数栈)
// istore_3 (将结果存储到result)
return result;
}
}
3、动态链接
- 执行运行时常量池中该栈帧所属方法的引用
- 支持方法调用过程中的动态链接
4、方法返回地址
- 记录方法正常退出或异常退出后应该会到的位置
- 用于恢复上层方法的执行状态
栈的异常情况
1、StackOverflowError
当线程请求的栈深度大于虚拟机允许的最大深度的时候抛出,常见于无限递归导致的
java
public class StackOverflowExample {
public static void recursiveMethod() {
recursiveMethod(); // 无限递归导致栈溢出
}
public static void main(String[] args) {
try {
recursiveMethod();
} catch (StackOverflowError e) {
System.out.println("栈溢出错误: " + e.getMessage());
}
}
}
2. OutOfMemoryError
当虚拟机栈可以动态扩展且扩展时无法申请到足够的内存时抛出:
java
/**
* 实用的栈内存溢出模拟 - 通过线程创建
* 这是最接近真实环境中栈导致OutOfMemoryError的情况
*/
public class PracticalStackOOM {
public static void main(String[] args) {
// 创建一个线程安全的计数器
var createdThreads = new java.util.concurrent.atomic.AtomicInteger(0);
try {
while (true) {
Thread thread = new Thread(() -> {
// 线程执行一个长期运行的任务,保持栈不被释放
try {
Thread.sleep(Long.MAX_VALUE); // 永远睡眠
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
thread.setName("OOM-Thread-" + createdThreads.incrementAndGet());
thread.start();
if (createdThreads.get() % 100 == 0) {
System.out.println("已创建 " + createdThreads.get() + " 个线程");
// 输出当前内存使用情况
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
System.out.println("已使用内存: " + usedMemory / (1024 * 1024) + " MB");
}
}
} catch (OutOfMemoryError e) {
System.out.println("\n=== 捕获到内存溢出错误 ===");
System.out.println("错误类型: " + e.getClass().getSimpleName());
System.out.println("错误信息: " + e.getMessage());
System.out.println("总共创建了 " + createdThreads.get() + " 个线程");
// 输出当前线程数量
ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
while (rootGroup.getParent() != null) {
rootGroup = rootGroup.getParent();
}
int activeCount = rootGroup.activeCount();
System.out.println("当前活跃线程数: " + activeCount);
}
}
}
栈的相关JVM参数
-Xss:设置每个线程的栈大小
默认大小因平台而异,典型值为512KB到1MB
栈帧的生命周期
java
public class StackFrameLifecycle {
public static void main(String[] args) {
System.out.println("Main方法开始");
methodA(); // 创建methodA的栈帧
System.out.println("Main方法结束");
}
public static void methodA() {
System.out.println("MethodA开始");
methodB(); // 创建methodB的栈帧
System.out.println("MethodA结束");
}
public static void methodB() {
System.out.println("MethodB执行中");
// methodB执行完毕,其栈帧被弹出
}
// methodA执行完毕,其栈帧被弹出
// main方法执行完毕,其栈帧被弹出
}
执行顺序和栈的状态变化:
main方法执行 → 栈:[main]
调用methodA → 栈:[main, methodA]
调用methodB → 栈:[main, methodA, methodB]
methodB执行完毕 → 栈:[main, methodA]
methodA执行完毕 → 栈:[main]
main执行完毕 → 栈:[]
本地方法栈
概述
本地方法栈与Java虚拟机栈类似,但专门服务于Native方法(本地方法)。它也为虚拟机使用到的Native方法服务,与虚拟机栈一样,本地方法栈也会在线程创建时同步创建。
什么是本地方法?
本地方法是在Java中声明但在外部实现的方法,使用native关键字标识:
本地方法(Native Method)是使用非Java语言编写的方法,通常用C、C++或其他本地语言实现
java
public class NativeMethodExample {
// 本地方法声明 - 只有方法签名,没有方法体
public native void nativeMethod();
// 有返回值的本地方法
public native int nativeAdd(int a, int b);
// 返回字符串的本地方法
public native String getNativeString();
// 静态本地方法
public static native void staticNativeMethod();
static {
// 加载包含本地方法实现的动态库
System.loadLibrary("nativeImpl"); // 会加载nativeImpl.dll (Windows) 或 libnativeImpl.so (Linux/Mac)
}
public static void main(String[] args) {
NativeMethodExample example = new NativeMethodExample();
// 调用本地方法
example.nativeMethod();
int result = example.nativeAdd(10, 20);
String str = example.getNativeString();
System.out.println("Native add result: " + result);
System.out.println("Native string: " + str);
}
}
本地方法栈的作用
java
public class NativeMethodStackExample {
// 声明一个本地方法
public native void nativeMethod();
// JVM内部调用本地方法时会使用本地方法栈
static {
// 加载包含本地方法实现的本地库
System.loadLibrary("nativeLib");
}
public static void main(String[] args) {
NativeMethodStackExample example = new NativeMethodStackExample();
// 调用本地方法时,会在本地方法栈中创建相应的栈帧
example.nativeMethod();
}
}
本地方法栈的工作原理
当执行一个Native方法时:
栈帧创建:在本地方法栈中创建新的栈帧
参数传递:将Java参数转换为本地代码可接受的格式
本地执行:执行本地代码逻辑
结果返回:将结果返回给Java代码
栈帧清理:方法执行完成后清理栈帧
本地方法栈与java虚拟机栈的区别
|------|--------------------|-------------------------------------|
| 特性 | java虚拟机栈 | 本地方法栈 |
| 服务对象 | java方法 | Native方法 |
| 数据结构 | 栈帧 | 栈帧 |
| 存储结构 | 局部变量表、操作数栈 | 本地方法的参数和局部变量 |
| 异常 | StackOverflowError | StackOverflowError、OutOfMemoryError |
本地方法栈的异常情况
1. StackOverflowError
当本地方法调用链过深或本地方法栈容量不足时:
java
// 伪代码示例,展示本地方法递归调用
/*
* 在C/C++实现的本地方法中:
* JNIEXPORT void JNICALL Java_MyClass_recursiveNativeCall(JNIEnv *env, jobject obj) {
* // 无限递归调用
* Java_MyClass_recursiveNativeCall(env, obj);
* }
*/
2. OutOfMemoryError
当无法申请到足够内存来扩展本地方法栈时:
java
// 模拟大量线程同时调用本地方法
public class NativeStackOOM {
public static void main(String[] args) {
for (int i = 0; ; i++) {
new Thread(() -> {
// 持续调用本地方法
while (!Thread.currentThread().isInterrupted()) {
System.nanoTime(); // System.nanoTime()是native方法
}
}, "NativeOOMThread-" + i).start();
}
}
}
JNI(Java Native Interface)与本地方法栈
当Java程序通过JNI调用本地代码时,本地方法栈起着关键作用:
java
public class JNIExample {
// 声明本地方法
public native int nativeAdd(int a, int b);
public native String nativeGetString();
static {
System.loadLibrary("myNativeLib"); // 加载本地库
}
public static void main(String[] args) {
JNIExample example = new JNIExample();
// 调用本地方法时:
// 1. 在本地方法栈创建栈帧
// 2. 传递参数到本地代码
// 3. 执行本地代码
// 4. 返回结果到Java代码
int result = example.nativeAdd(10, 20);
String str = example.nativeGetString();
System.out.println("Result: " + result + ", String: " + str);
}
}
本地方法栈的重要性
Java与本地代码的桥梁:使Java能够调用C/C++等本地代码
性能提升:某些性能敏感的操作可通过本地代码实现
系统调用:访问操作系统特定功能
遗留代码集成:重用现有的C/C++代码库
本地方法栈是Java与Native代码交互的重要基础设施,虽然大部分Java开发人员很少直接接触,但它在JVM的底层实现中扮演着不可或缺的角色。
堆
概述
java堆是JVM管理中的内存最大的一块,所有线程共享此区域,主要用于存储对象实例和数组
java
public class HeapExample {
public static void main(String[] args) {
// 所有new出来的对象都存储在堆中
String str = new String("Hello"); // String对象存储在堆中
int[] array = new int[100]; // 数组存储在堆中
Person person = new Person(); // Person对象存储在堆中
List<String> list = new ArrayList<>(); // ArrayList对象存储在堆中
}
}
class Person {
private String name;
private int age;
// Person对象本身存储在堆中,成员变量也在堆中
public Person() {
this.name = new String("Default Name"); // name引用的对象也在堆中
}
}
堆的内存结构
java堆通常被划分为以下几个区域
1、新生代
- Eden区:新创建的对象首先被分配在这里
- Survivor区:包括From和To两个区域,用于存放经过一次垃圾回收后仍然存活的对象
2、老年代
- 存放经过多次垃圾回收后仍然存活的对象
- 打对象可能直接分配到老年代
3、元空间/永久代
- java8之前成为永久代,java8之后成为元空间
- 存储类信息、常量、静态变量等
java
public class HeapStructureExample {
public static void main(String[] args) {
// 新对象通常分配在Eden区
for (int i = 0; i < 1000; i++) {
new Object(); // 这些对象首先在Eden区创建
}
// 经过几次GC后仍然存活的对象会进入老年代
Object[] oldObjects = new Object[100];
for (int i = 0; i < 100; i++) {
oldObjects[i] = new Object(); // 这些对象可能最终进入老年代
}
// 大对象可能直接分配到老年代
byte[] largeArray = new byte[1024 * 1024]; // 1MB的大数组
}
}
堆的分配策略
1、指针碰撞
适用于内存规整的情况(如新生代的Eden区),使用指针移动的方法分配内存
2、空闲列表
适用于内存不规整的情况(如老年代),JVM维护一个空闲内存块列表
java
public class HeapAllocationExample {
// 大对象直接进入老年代的示例
private static final byte[] LARGE_OBJECT = new byte[1024 * 1024]; // 1MB
public static void main(String[] args) {
// 小对象通常在Eden区分配
String smallString = "Small String";
// 短生命周期对象
for (int i = 0; i < 1000; i++) {
Object temp = new Object(); // 快速创建和销毁
// 这些对象很快变成垃圾,会被Minor GC回收
}
// 长生命周期对象
Object[] longLivingObjects = new Object[100];
for (int i = 0; i < 100; i++) {
longLivingObjects[i] = new LongLivedObject();
// 这些对象会逐渐晋升到老年代
}
}
}
class LongLivedObject {
private byte[] data = new byte[1024]; // 1KB数据
private String name;
public LongLivedObject() {
this.name = "LongLivedObject-" + System.currentTimeMillis();
}
}
堆的垃圾收集
堆是垃圾收集器工作的主要区域,不同的垃圾收集器有不同的算法
java
public class GCExample {
public static void main(String[] args) {
// 创建大量临时对象,触发Minor GC
for (int i = 0; i < 100000; i++) {
createTempObject();
}
// 创建一些长期存活的对象
java.util.List<Object> longLivedList = new java.util.ArrayList<>();
for (int i = 0; i < 1000; i++) {
longLivedList.add(new LongLivedObject());
}
// 显式触发垃圾收集
System.gc(); // 建议JVM进行垃圾收集
}
private static void createTempObject() {
// 这些对象生命周期很短,很快成为垃圾
Object temp = new Object();
String str = "Temporary String";
int[] arr = new int[10];
}
}
堆的JVM参数设置
java
# 设置堆的初始大小和最大大小
-Xms2g # 初始堆大小2GB
-Xmx4g # 最大堆大小4GB
# 设置新生代大小
-Xmn1g # 新生代大小1GB
# 设置新生代和老年代比例
-XX:NewRatio=2 # 老年代:新生代 = 2:1
# 设置Eden区和Survivor区比例
-XX:SurvivorRatio=8 # Eden:Survivor1:Survivor2 = 8:1:1
堆内存溢出示例
java
import java.util.ArrayList;
import java.util.List;
/**
* 堆内存溢出示例
* JVM参数: -Xms10m -Xmx10m
*/
public class HeapOOMExample {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
try {
// 持续分配大对象直到堆内存耗尽
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配1MB
}
} catch (OutOfMemoryError e) {
System.out.println("堆内存溢出: " + e.getMessage());
e.printStackTrace();
}
}
}
堆的监控和诊断
java
public class HeapMonitoring {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
// 获取堆内存信息
long maxMemory = runtime.maxMemory(); // 堆最大内存
long totalMemory = runtime.totalMemory(); // 当前分配的堆内存
long freeMemory = runtime.freeMemory(); // 空闲堆内存
long usedMemory = totalMemory - freeMemory; // 已使用堆内存
System.out.println("最大堆内存: " + maxMemory / (1024 * 1024) + " MB");
System.out.println("当前分配内存: " + totalMemory / (1024 * 1024) + " MB");
System.out.println("已使用内存: " + usedMemory / (1024 * 1024) + " MB");
System.out.println("空闲内存: " + freeMemory / (1024 * 1024) + " MB");
// 创建对象观察内存变化
for (int i = 0; i < 1000; i++) {
new byte[1024 * 10]; // 10KB的对象
}
System.out.println("\n分配对象后:");
usedMemory = runtime.totalMemory() - runtime.freeMemory();
System.out.println("已使用内存: " + usedMemory / (1024 * 1024) + " MB");
}
}
方法区
概述
方法区是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区在JDK8之后使用的是本地存储,在JDK7及以前使用的是java的堆内存难以维护且容易OOM,虽然在JDK8之后使用的是本地内存,但是也可能存在OOM的情况,例如动态在内存中加载大数据容易导致本地内存占用超过内存限制导致内存泄漏
java
public class MethodAreaExample {
// 静态变量存储在方法区
public static int staticVar = 100;
// 静态常量存储在方法区的运行时常量池
public static final String CONSTANT = "Constant Value";
// 类信息、方法信息等也存储在方法区
public void instanceMethod() {
System.out.println("Instance method");
}
public static void staticMethod() {
System.out.println("Static method");
}
public static void main(String[] args) {
// 类加载时,类的元数据存储在方法区
MethodAreaExample example = new MethodAreaExample();
System.out.println("Static var: " + staticVar);
System.out.println("Constant: " + CONSTANT);
}
}
方法区的演变
java
// JDK 7及以前的内存布局
public class PermGenInHeap {
// 永久代是堆的一部分,用于存储类元数据
public static final String PERM_GEN_DATA = "Stored in PermGen within Heap";
public static int staticVariable = 100;
public static void main(String[] args) {
System.out.println("In JDK 7-, PermGen is part of the Heap");
// 在JDK 7及以前,内存布局大致如下:
// ┌─────────────────────────────────┐
// │ Heap │
// │ ┌─────────────────────────┐ │
// │ │ Young Gen │ │
// │ │ ┌───────────────────┐ │ │
// │ │ │ Eden │ │ │
// │ │ │ Survivor │ │ │
// │ │ └───────────────────┘ │ │
// │ └─────────────────────────┘ │
// │ ┌─────────────────────────┐ │
// │ │ Old Gen │ │
// │ └─────────────────────────┘ │
// │ ┌─────────────────────────┐ │
// │ │ Perm Gen │ │ ← 永久代在堆中
// │ └─────────────────────────┘ │
// └─────────────────────────────────┘
}
}
java
// JDK 7及以前的永久代参数设置
/*
* -Xms2g # 堆初始大小
* -Xmx2g # 堆最大大小
* -XX:PermSize=128m # 永久代初始大小(堆内)
* -XX:MaxPermSize=256m # 永久代最大大小(堆内)
*/
public class PermGenParameters {
public static void main(String[] args) {
System.out.println("PermGen parameters in JDK 7-:");
System.out.println("-XX:PermSize sets initial size within heap");
System.out.println("-XX:MaxPermSize sets maximum size within heap");
}
}
从JDK 8开始,永久代被完全移除,取而代之的是元空间(Metaspace):
java
// JDK 8及以后的内存布局
public class MetaspaceInsteadOfPermGen {
// 元空间使用本地内存,不在Java堆中
public static final String METASPACE_DATA = "Metadata stored in Metaspace (Native Memory)";
public static int heapVariable = 200; // 普通变量仍在堆中
public static void main(String[] args) {
System.out.println("In JDK 8+, PermGen is removed, Metaspace uses native memory");
// JDK 8+的内存布局:
// ┌─────────────────────────────────┐
// │ Heap │
// │ ┌─────────────────────────┐ │
// │ │ Young Gen │ │
// │ │ ┌───────────────────┐ │ │
// │ │ │ Eden │ │ │
// │ │ │ Survivor │ │ │
// │ │ └───────────────────┘ │ │
// │ └─────────────────────────┘ │
// │ ┌─────────────────────────┐ │
// │ │ Old Gen │ │
// │ └─────────────────────────┘ │
// └─────────────────────────────────┘
// ↑
// │
// ┌─────────────────────────────────┐
// │ Metaspace (Native) │ ← 元空间在本地内存中
// │ ┌─────────────────────────┐ │
// │ │ Class Metadata │ │
// │ │ (Class Definitions) │ │
// │ └─────────────────────────┘ │
// └─────────────────────────────────┘
}
}
字符串常量池的迁移
java
public class StringPoolMigration {
public static void demonstrateMigration() {
// 一个重要的变化是字符串常量池的迁移
// JDK 6: 字符串常量池在永久代
// JDK 7: 字符串常量池从永久代迁移到堆中
// JDK 8+: 字符串常量池在堆中(继承JDK 7的变化)
String literal = "Hello"; // 字符串字面量
String interned = new String("Hello").intern(); // 调用intern()
System.out.println("literal == interned: " + (literal == interned));
System.out.println("String pool moved from PermGen to Heap in JDK 7");
System.out.println("This reduced pressure on PermGen space");
}
public static void main(String[] args) {
demonstrateMigration();
}
}
永久代内存分配的具体变化
java
// 演示永久代中存储的不同类型数据及其变化
public class PermGenContentChanges {
// 静态变量 - JDK 7及以前在永久代,JDK 8+在堆中
public static String staticString = "Moved from PermGen to Heap";
// 静态常量的符号引用 - 仍在元空间(类的元数据)
public static final String CONSTANT_REF = "Symbol reference in Metaspace";
// 类的元数据 - 从永久代移到元空间
public void method() {
System.out.println("Method metadata moved from PermGen to Metaspace");
}
public static void main(String[] args) {
System.out.println("Content migration summary:");
System.out.println("1. Class metadata: PermGen → Metaspace");
System.out.println("2. String constants: PermGen → Heap (JDK 7)");
System.out.println("3. Static variables: PermGen → Heap (JDK 8)");
System.out.println("4. Method metadata: PermGen → Metaspace");
}
}
内存溢出行为的变化
java
public class OOMBehaviorChanges {
public static void demonstrateOOMChanges() {
System.out.println("OOM behavior changes:");
System.out.println("JDK 7及以前:");
System.out.println("- java.lang.OutOfMemoryError: PermGen space");
System.out.println("- 永久代空间不足导致");
System.out.println("\nJDK 8+:");
System.out.println("- java.lang.OutOfMemoryError: Metaspace");
System.out.println("- 本地内存不足导致");
}
public static void main(String[] args) {
demonstrateOOMChanges();
}
}
JVM参数变化
java
public class ParameterChanges {
public static void showParameterChanges() {
System.out.println("JVM参数变化:");
System.out.println("JDK 7及以前:");
System.out.println("-XX:PermSize=128m");
System.out.println("-XX:MaxPermSize=256m");
System.out.println("\nJDK 8+:");
System.out.println("-XX:MetaspaceSize=256m");
System.out.println("-XX:MaxMetaspaceSize=512m (可选,默认无限制)");
}
public static void main(String[] args) {
showParameterChanges();
}
}
总结
永久代的内存位置变化经历了以下演进:
JDK 7及以前:永久代是Java堆的一部分
JDK 8:永久代被完全移除,引入元空间使用本地内存
字符串常量池:从永久代迁移到Java堆(JDK 7完成)
静态变量:从永久代迁移到Java堆(JDK 8完成)
这个变化解决了永久代的诸多限制,提供了更灵活的内存管理方式。
方法区存储的内容
- 类型信息
java
public class TypeInfoInMethodArea {
// 类的全限定名
// Class文件格式版本
// 访问修饰符
// 常量池信息
// 字段信息
// 方法信息
// 接口信息
// 类变量(静态变量)
public static String className = "TypeInfoInMethodArea";
private int instanceField;
public static int staticField = 42;
public void instanceMethod() {}
public static void staticMethod() {}
}
- 运行时常量池
java
public class RuntimeConstantPoolExample {
public static void main(String[] args) {
// 字面量存储在运行时常量池
String str1 = "Hello"; // "Hello"存储在常量池
String str2 = "Hello"; // 引用相同的常量池项
System.out.println(str1 == str2); // true
// 数值常量也存储在常量池
int number = 100; // 100可能存储在常量池
double pi = 3.14159; // 3.14159可能存储在常量池
// 字符串连接的结果
String combined = "Hello" + "World"; // "HelloWorld"存储在常量池
}
}
- 静态变量
java
public class StaticVariablesInMethodArea {
// 以下静态变量都存储在方法区
public static int staticInt = 10;
public static String staticString = "Static String";
public static Object staticObject = new Object();
public static final int FINAL_CONSTANT = 100;
public static final String FINAL_STRING = "Final String";
// 静态代码块也在方法区执行
static {
System.out.println("Static block executed, stored in Method Area");
staticInt = 20;
}
}
方法区的垃圾回收
方法区(JDK 8+的元空间)的垃圾回收主要回收两部分内容:
废弃的常量
不再使用的类型(类卸载)
java
public class MethodAreaGCOverview {
// 静态变量存储在方法区
public static String staticConstant = "Will be GC if class is unloaded";
public static void main(String[] args) {
System.out.println("Method Area GC can reclaim:");
System.out.println("1. Unused constants");
System.out.println("2. Unloaded classes");
// 显示当前方法区使用情况
Runtime runtime = Runtime.getRuntime();
System.out.println("Heap memory used: " +
(runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024) + " MB");
}
}
类卸载条件
要使一个类被卸载,必须满足以下条件:
java
public class ClassUnloadingConditions {
public static void main(String[] args) {
System.out.println("Class unloading conditions:");
System.out.println("1. All instances of the class have been reclaimed");
System.out.println("2. The ClassLoader that loaded the class has been reclaimed");
System.out.println("3. No references to the Class object exist anywhere");
// 示例:动态加载类并尝试卸载
try {
// 使用自定义类加载器加载类
CustomClassLoader loader = new CustomClassLoader();
// 加载类
Class<?> loadedClass = loader.loadClass("java.lang.Object");
System.out.println("Loaded class: " + loadedClass.getName());
// 移除对类的引用
loadedClass = null;
loader = null;
// 请求垃圾回收
System.gc();
Thread.sleep(100); // 给GC时间
System.out.println("After GC attempt");
} catch (Exception e) {
e.printStackTrace();
}
}
static class CustomClassLoader extends ClassLoader {
public Class<?> loadClassFromBytes(byte[] classBytes) {
return defineClass(null, classBytes, 0, classBytes.length);
}
}
}
废弃常量的回收
java
public class UnreferencedConstantsGC {
public static void main(String[] args) {
// 创建大量字符串常量
String constant1 = "Constant1".intern();
String constant2 = "Constant2".intern();
String constant3 = "Constant3".intern();
System.out.println("Before removing references");
// 移除对常量的引用
constant1 = null;
constant2 = null;
constant3 = null;
// 此时常量池中的字符串常量可能被回收
System.gc();
System.out.println("After GC attempt - unreferenced constants may be collected");
}
}
触发方法区垃圾回收
java
public class TriggerMethodAreaGC {
public static void main(String[] args) {
// 在JDK 8+中,元空间的垃圾回收通常伴随整个堆的GC
System.out.println("Method Area GC typically occurs with full GC");
// 强制进行完整的垃圾回收
System.gc(); // 建议JVM执行GC,包括方法区
// 或者使用更积极的方式
Runtime.getRuntime().gc(); // 同样是建议GC
System.out.println("GC requested - includes Method Area/Metaspace");
}
}
自定义类加载器和类卸载示例
java
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
public class CustomClassLoaderGCDemo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
// 创建自定义类加载器实例
DynamicClassLoader loader = new DynamicClassLoader();
try {
// 加载一个类(这里用Object类作为示例)
Class<?> clazz = loader.loadClass("java.lang.String");
System.out.println("Loaded class: " + clazz.getName() +
" with loader: " + loader);
// 不保留对类或类加载器的引用
// 在循环结束后,这些对象可以被GC回收
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
System.out.println("Created multiple ClassLoaders, now attempting GC");
// 尝试触发GC,可能回收不再使用的类
System.gc();
try {
Thread.sleep(1000); // 等待GC完成
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("GC completed - unused classes may be unloaded");
}
static class DynamicClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先检查是否已加载该类
Class<?> clazz = findLoadedClass(name);
if (clazz == null) {
// 委托给父类加载器
clazz = super.loadClass(name, resolve);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
}
方法区垃圾回收的JVM参数
java
public class MethodAreaGCParameters {
public static void main(String[] args) {
System.out.println("JVM parameters affecting Method Area GC:");
System.out.println("For JDK 8+ (Metaspace):");
System.out.println("-XX:MetaspaceSize=256m # Initial metaspace size");
System.out.println("-XX:MaxMetaspaceSize=512m # Max metaspace size");
System.out.println("-XX:MinMetaspaceFreeRatio=40 # Min free ratio");
System.out.println("-XX:MaxMetaspaceFreeRatio=70 # Max free ratio");
System.out.println("\nGeneral GC parameters that affect Method Area:");
System.out.println("-XX:+UseG1GC # G1GC can collect metaspace");
System.out.println("-XX:+CMSClassUnloadingEnabled # Enable class unloading in CMS");
System.out.println("-XX:+ExplicitGCInvokesConcurrent # Concurrent GC for System.gc()");
}
}
监控方法区垃圾回收
java
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
public class MonitorMethodAreaGC {
public static void main(String[] args) {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
System.out.println("Monitoring Method Area/Metaspace GC:");
// 显示堆内存信息
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
System.out.println("Heap init: " + heapUsage.getInit() / (1024 * 1024) + " MB");
System.out.println("Heap used: " + heapUsage.getUsed() / (1024 * 1024) + " MB");
System.out.println("Heap committed: " + heapUsage.getCommitted() / (1024 * 1024) + " MB");
System.out.println("Heap max: " + heapUsage.getMax() / (1024 * 1024) + " MB");
// 注意:标准API不直接提供Metaspace的监控
// 需要使用特定的JVM工具如jstat, jconsole等
System.out.println("\nPerforming GC...");
System.gc();
// 再次查看内存使用
heapUsage = memoryBean.getHeapMemoryUsage();
System.out.println("After GC - Heap used: " + heapUsage.getUsed() / (1024 * 1024) + " MB");
}
}
实际的类卸载示例
java
public class ClassUnloadingDemo {
public static void main(String[] args) {
System.out.println("Demonstrating conditions for class unloading:");
// 条件1:所有实例都被回收
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = null;
obj2 = null;
// 条件2:类加载器被回收
// 条件3:没有对Class对象的引用
Class<?> classRef = null;
try {
Class<?> tempClass = Class.forName("java.lang.Object");
classRef = tempClass; // 引用类对象
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
classRef = null; // 移除对Class对象的引用
System.out.println("Removed all references, ready for class unloading");
// 执行GC,可能触发类卸载
System.gc();
System.out.println("GC executed - eligible classes may be unloaded");
}
}
方法区垃圾回收的特点
回收频率较低:相比堆内存,方法区GC频率较低
回收条件严格:特别是类卸载需要满足严格的条件
JDK版本差异:JDK 8前后实现方式不同,影响GC行为
性能影响:方法区GC可能影响应用性能,特别是在频繁动态类加载场景
方法区的垃圾回收虽然不如堆内存GC那样频繁,但对于长期运行的应用程序,特别是使用大量动态代理、反射或热部署功能的应用,方法区GC仍然很重要。