目录
一、简介
二、程序计数器
三、虚拟机栈
栈帧结构:
特点:
四、本地方法栈
特点:
五、堆
堆结构:
特点:
对象分配过程:
六、方法区
方法区结构:
特点:
运行时常量池
七、StringTable
(一)StringTable 核心概念
(二)核心特性与机制
1. 字符串唯一性(Intern机制)
2. 延迟加载
3. 不可变性
(三)内存位置演变
(四)字符串创建流程
(五)性能优化建议
(六)示例:StringTable 与 GC 交互
(七)总结对比表
(八)面试题
八、直接内存
(一) 基本概念
(二)与堆内存的对比
(三)核心优势
(四)内存分配与回收
(五)潜在问题
(六)最佳实践
(七)典型应用场景
(八)总结
(九)分配和回收原理
九、JVM内存整体结构图
一、简介
Java虚拟机(JVM)在执行Java程序时会将内存划分为不同的区域,每个区域有特定的用途和生命周期。JVM内存结构主要分为线程私有区域(程序计数器、虚拟机栈、本地方法栈)和线程共享区域(堆、方法区)。下面将详细解析每个部分的结构和功能。
二、程序计数器
程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
作用:记住下一条jvm指令的执行地址
特点:
线程私有:每个线程都有独立的程序计数器
唯一不会OOM的区域:没有内存溢出问题
三、虚拟机栈
虚拟机栈(Java Virtual Machine Stacks) 是描述Java方法执行的内存模型,每个方法执行时都会创建一个栈帧。
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
1. 垃圾回收是否涉及栈内存?
不涉及
2. 栈内存分配越大越好吗?
No
3. 方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈帧结构:
特点:
线程私有:生命周期与线程相同
FILO结构:方法调用对应栈帧的入栈出栈
可能抛出异常:
StackOverflowError:栈深度超过限制
OutOfMemoryError:栈扩展失败
局部变量表:
-
存储编译期可知的各种基本数据类型
-
对象引用(reference类型)
-
returnAddress类型(指向字节码指令地址)
操作数栈:
-
用于存储计算过程的中间结果
-
工作区,方法执行过程中数据写入和提取
四、本地方法栈
本地方法栈(Native Method Stack) 与虚拟机栈作用相似,区别在于它为Native方法服务。
特点:
线程私有区域
存储Native方法调用的状态
在HotSpot JVM中与虚拟机栈合并
同样会抛出StackOverflowError和OutOfMemoryError
五、堆
堆(Heap) 是JVM管理的最大一块内存区域,被所有线程共享,在虚拟机启动时创建。通过 new 关键字,创建对象都会使用堆内存
堆结构:
特点:
线程共享:所有线程访问同一堆空间
GC主要区域:垃圾收集器管理的主要区域
分代管理:
新生代(Young Generation):新对象创建区域
Eden区:对象初次分配区
Survivor区:经过Minor GC后存活的对象
老年代(Old Generation):长期存活的对象
异常:当堆无法分配内存且无法扩展时,抛出OutOfMemoryError
对象分配过程:
六、方法区
方法区(Method Area) 存储已被虚拟机加载的类型信息、常量、静态变量等数据。
方法区结构:
特点:
线程共享:所有线程共享方法区
永久代→元空间:
JDK7及之前:永久代(PermGen),在堆中
JDK8+:元空间(Metaspace),使用本地内存
包含运行时常量池:
存放编译期生成的各种字面量和符号引用
动态性:运行期间也可以将新的常量放入池中
异常:当方法区无法满足内存分配需求时,抛出OutOfMemoryError
运行时常量池
- 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
- 运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
七、StringTable
(一)StringTable 核心概念
-
本质
StringTable(字符串表)是JVM中哈希表(Hash Table) 的实现,用于存储字符串对象的引用。-
键(Key):字符串的哈希值(由字符串内容计算得出)。
-
值(Value):字符串对象在堆中的引用。
-
-
与常量池的关系
-
class文件常量池:存储编译期生成的字面量(Literal)和符号引用(如
"abc"
)。 -
运行时常量池:类加载时,将class常量池加载到方法区中。
-
StringTable:在运行时常量池中的字符串字面量首次被使用时,动态创建实际字符串对象并存入StringTable。
-
(二)核心特性与机制
1. 字符串唯一性(Intern机制)
-
通过
String.intern()
方法,将字符串主动加入StringTable并返回唯一引用。 -
规则:若StringTable中已存在相同内容的字符串,则返回其引用;否则将当前字符串加入表中。
String s1 = new String("hello"); // 在堆中创建对象,未加入StringTable
String s2 = "hello"; // 直接使用StringTable中的引用
String s3 = s1.intern(); // 将s1的字符串内容加入StringTable
System.out.println(s1 == s2); // false:s1在堆,s2在StringTable
System.out.println(s2 == s3); // true:s2和s3指向StringTable同一对象
2. 延迟加载
-
字符串字面量在首次被引用时才创建对象并加入StringTable。
-
示例:
public class LazyLoadExample {
public static void main(String[] args) {
// 仅声明字面量,未主动使用,不会加载到StringTable
String unused = "unused_string";
// 首次使用字面量时加载
System.out.println("hello"); // "hello" 被加入StringTable
}
}
3. 不可变性
-
所有存入StringTable的字符串均为不可变对象(由
final char[]
实现)。 -
修改字符串会创建新对象,不影响原StringTable中的引用。
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份, 放入串池, 会把串池中的对象返回
(三)内存位置演变
JVM版本 | 存储位置 | 特点 |
---|---|---|
JDK 1.6及之前 | 永久代(PermGen) | 固定大小,易触发 OutOfMemoryError: PermGen space 。 |
JDK 1.7+ | 堆内存(Heap) | 可动态扩容,受 -Xmx 控制,GC可回收无引用的字符串。 |
(四)字符串创建流程
(五)性能优化建议
-
调整表大小
通过
-XX:StringTableSize=N
设置桶数量(建议设为质数),减少哈希冲突。
java -XX:StringTableSize=10009 MyApp
- 避免重复字符串
使用 intern()
减少重复大字符串的内存占用(适合重复率高的场景)。
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
String temp = new String("重复数据").intern(); // 复用StringTable中的对象
list.add(temp);
}
-
谨慎使用
intern()
-
高频调用可能引发哈希冲突,导致性能下降。
-
适合长期存活且重复率高的字符串(如数据库字段名)。
-
(六)示例:StringTable 与 GC 交互
public class StringTableGCDemo {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
String temp = "str_" + i; // 字面量加入StringTable
temp = null; // 断开引用
}
System.gc(); // 触发GC,回收无引用的String对象(JDK1.7+)
}
}
-
JDK 1.6:字符串在PermGen中,GC不回收,导致内存泄漏。
-
JDK 1.7+:字符串在堆中,GC可回收无引用的对象。
(七)总结对比表
特性 | 常量池(Constant Pool) | StringTable |
---|---|---|
存储内容 | 字面量、符号引用 | 字符串对象的引用 |
内存位置 | 方法区(元空间) | 堆内存 |
生命周期 | 类加载时生成 | 运行时动态添加 |
垃圾回收 | 不回收 | 可被GC回收(JDK1.7+) |
数据结构 | 表结构(非哈希) | 哈希表 |
关键结论:
StringTable 是 运行时字符串驻留机制 的核心,通过哈希表实现唯一性。
JDK 1.7+ 将其移至堆内存,解决了永久代内存溢出问题,且支持GC回收。
合理使用
intern()
和调整StringTableSize
可优化内存与性能。
(八)面试题
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
// 问
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
八、直接内存
(一) 基本概念
-
定义:直接内存是 JVM 堆外由操作系统直接管理的内存区域。
-
核心类:通过
java.nio.ByteBuffer.allocateDirect()
分配。 -
数据存储:直接存储原始字节数据,不归 JVM GC 管理。
(二)与堆内存的对比
特性 | 直接内存 | 堆内存 |
---|---|---|
内存位置 | 堆外(操作系统管理) | JVM 堆内 |
分配速度 | 较慢(需系统调用) | 较快(JVM 内部管理) |
访问速度 | 快(少一次数据复制) | 较慢(需复制到堆外) |
内存回收 | 手动或基于 Cleaner 机制 | GC 自动回收 |
容量限制 | 受系统内存限制 | 受 -Xmx 限制 |
适用场景 | 高频 I/O、大文件操作 | 常规对象存储 |
(三)核心优势
-
减少数据复制
-
传统 I/O:数据需从内核缓冲区 → JVM 堆缓冲区 → 用户空间(两次复制)。
-
直接内存:数据直接在内核缓冲区处理(零复制),提升效率。
-
应用场景:网络传输、文件读写(如 NIO 的
FileChannel.transferTo()
)。
-
-
突破堆大小限制
// 分配 1GB 直接内存(不受 -Xmx 限制)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
(四)内存分配与回收
-
分配方式:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 分配 1KB 直接内存
回收机制:
-
Cleaner
机制:DirectByteBuffer
被 GC 回收时,触发关联的Cleaner
释放堆外内存。 -
手动释放(不推荐):
// 反射强制释放(仅作演示,实际慎用!)
Method cleanerMethod = buffer.getClass().getMethod("cleaner");
cleanerMethod.setAccessible(true);
Object cleaner = cleanerMethod.invoke(buffer);
Method cleanMethod = cleaner.getClass().getMethod("clean");
cleanMethod.invoke(cleaner);
(五)潜在问题
-
内存泄漏
-
原因:
DirectByteBuffer
对象被回收前,堆外内存不会被释放。 -
风险点:频繁分配大块直接内存且未及时触发 GC。
-
-
OutOfMemoryError
-
错误信息:
Direct buffer memory
-
解决方案:
-
增大 JVM 参数:
-XX:MaxDirectMemorySize=2G
(默认等于-Xmx
)。 -
优化代码:复用
ByteBuffer
或显式调用System.gc()
(不保证立即生效)。
-
-
(六)最佳实践
-
复用缓冲区
// 复用 ByteBuffer 减少分配开销
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
buffer.clear(); // 重置位置标记,准备复用
- 结合内存映射文件
// 使用内存映射文件操作大文件(基于直接内存)
FileChannel channel = FileChannel.open(Path.of("largefile.bin"));
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, channel.size()
);
-
监控工具
-
JVM 参数:
-XX:NativeMemoryTracking=detail
-
命令:
jcmd <pid> VM.native_memory
-
(七)典型应用场景
-
Netty 的 ByteBuf
// Netty 的池化直接内存
ByteBuf directBuf = Unpooled.directBuffer(1024);
-
高性能序列化
-
如 Kryo、FST 直接操作堆外内存避免 GC 停顿。
-
(八)总结
关键点 | 说明 |
---|---|
本质 | JVM 堆外内存,由操作系统管理 |
分配方式 | ByteBuffer.allocateDirect() |
性能优势 | 零复制(减少内核-用户态数据拷贝) |
回收风险 | 依赖 Cleaner 机制,可能内存泄漏 |
适用场景 | 高频 I/O、大文件处理、网络通信框架 |
监控手段 | NMT(Native Memory Tracking)、MaxDirectMemorySize 参数 |
(九)分配和回收原理
- 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
- ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调用 freeMemory 来释放直接内存
九、JVM内存整体结构图
完结撒花🎉