一、前言
笔者工作中一直在CRUD,了解JVM的唯一途径也就是八股文。一直想的有体系的学习一下JVM的知识,为了揭开JVM神秘的面纱,笔者根据《深入理解JAVA虚拟机》结合B站学习视频进行学习,希望能翻过这座神秘的大山,为此记录学习笔记。此系列是根据笔者自身对JVM的认识路线展开的。
二、JAVA内存区域
1.学习目的与思考
- 认识虚拟机中内存是如何划分的 (
理论
) - 哪些区域,什么样的代码和操作会导致内存溢出的异常(
理论+实践
)
谈及虚拟机,首先映入脑海的便是JVM内存模型。值得注意的是,随着JDK版本的迭代,内存区域的划分存在显著差异。笔者虽然掌握基础的内存分区原理,但对不同版本间的具体演变却缺乏系统性认知——这恰恰是我们需要明确的第一点认知盲区。在实际工作中,频繁遭遇的OOM异常若仅依赖浅层记忆应对,往往陷入“知其然却难究其所以然”的困境。因此,我们不妨跳出教科书式的标准化叙述,以初学者的视角回归本质,重新梳理内存模型的核心逻辑与动态演变,从而构建更扎实的问题分析能力。
2.运行时数据区
区域划分
根据线程是否共享数据区
可以把运行看出两部分区域
1.所有线程共享的数据区: 方法区
,堆
2.线程隔离的数据区: 虚拟机栈
,本地方法栈
,程序计数器
线程隔离的内存: 因为JAVA虚拟机的多线程是通过线程轮流切换,分配处理器执行时间的方式来实现的。如果是当前线程所私有的数据,且不想被其他线程所影响,就需要存储在线程私有的内存
中,为此就需要线程中开辟一块隔离是数据区域进行存储。例如"程序计数器"。
2.1.程序计数器
程序计数器(Program Counter Register)是较小一块内存内存空间。
概念
: 程序计数器是当前线程所执行的字节码的行号指示器。作用
: 字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计时器来完成。注意点
:
1.如果执行的是本地(Native)方法,计数器值为空(Undefined)
2.计数器是唯一一个不会发生OOM的区域
Java 程序运行的核心流程
:.java 源代码文件被编译成 .class 字节码文件,然后由 JVM 虚拟机加载并执行。JVM
会对字节码进行解释或编译优化,最终将其转化为一系列线性的字节码指令供 CPU 执行。
程序计数器的发挥的作用
:在多线程环境中,某个线程在执行其字节码指令序列时,可能会被操作系统中断(例如时间片用完)。当该线程后续重新获得 CPU 执行权时,JVM会立即查看该线程私有的程序计数器。程序计数器中存储的值指明了线程上次执行到的字节码指令地址。JVM正是根据这个地址,精确地定位到下一条要执行的指令,从而实现了线程执行的无缝恢复。
2.2.Java虚拟机栈
1.概念:
JAVA虚拟机栈(Java Virtual Machine Stack)是线程私有
的,生命周期和线程相同。
- 每个方法被调用直至执行完毕,就对应一个栈帧在虚拟机栈中从
入栈到出栈
的过程。 - JAVA虚拟机栈是JAVA方法执行的线程内存模型。 每个方法被执行的时候,JAVA都会同步创建一个
栈帧
用于存储局部变量表
,操作数栈
,动态连接
,方法出口
等信息。
虚拟机栈是线程私有的内存区域(想象是"抽屉柜"),一个虚拟机栈中有多个栈帧(想象是"抽屉"),当前线程只能从最顶部执行工作。
以下是栈帧中的内容(“抽屉可以存储的东西”)
1.局部变量表: 存放临时变量,例如: int a=5;
2.操作数栈: 执行计算时的临时区域,例如: a+b,首先a,b在临时区
3.动态连接: 记录方法调用其他方法的内存地址
4: 方法出口: 调用完方法后返回的位置
下文会介绍入栈,出栈的流程,见4.JAVA虚拟机栈流程演示
2.局部变量表:
局部变量表
存储了编译期可知的各种JAVA虚拟机以下内容:
- 基本数据类型(boolean,byte,char,short,int,float,long,double)
- 对象引用(可以是一个对象的引用地址)
- -returnAddress类型(指向一个字节码命令的地址)
存储方式:以上数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示的,其中long,double数据会占用两个变量槽,其余的值占用一个。局部变量所需要的内存空间在编译期间完成分配,在运行期间不会改变局部变量表的"大小"(大小指的是数量)
3.存在的异常情况
StrackOverflowError
: 栈溢出异常,线程请求的栈深度大于JVM允许的深度,会抛出此异常
OutOfMemoryError
:内存异常异常, 虚拟机栈是可以动态拓展,当拓展无法申请到足够的内存,会抛出此异常
4.JAVA虚拟机栈流程演示
public static void main(String[] args) {
int x = 10;
int y = 20;
int result = add(x, y); // 调用add方法
System.out.println(result);
}
static int add(int a, int b) {
int sum = a + b;
return sum;
}
阶段 | 步骤 |
---|---|
1.main方法启动(栈帧入栈 ) | 首先局部变量表 放入x=10,y=20,args.操作数栈 待用,方法出口 标记方法结束 |
2.执行add方法(新栈帧入栈) | 将a=10,b=2传递到新的栈帧 的局部变量表 中, 在操作数栈 中间计算a=10,b=20,执行iadd指令,得到30,结果sum=30存回局部变量表 中,方法出口 返回到mian方法的add()所在行 |
3.add方法结束(栈帧出栈 ) | 将结果30传回给mian方法的操作数栈 ,清空add方法的栈帧 (局部变量销毁) |
4.main方法继续执行 | 操作数栈 收到30,存入到局部变量表 中,执行打印(也是新的栈帧) |
2.3.本地方法栈
1.概念
java虚拟机栈执行的是JAVA方法,而本地方法栈执行的是虚拟机使用到的本地(Native)方法
2.存在的异常情况
StackOverflowError和OOM
因为《JAVA虚拟机规范》中,没有强制要求虚拟机要实现此区域,有的虚拟机会将本地方法栈和java虚拟机栈合二为一。例如Hot-spot虚拟机。
2.4.Java堆
1.概念
JAVA堆是虚拟机所管理的内存中最大的一块内存区域,它是线程共享的区域,内存区域主要存储对象实例,"几乎"所有的实例对象都在堆上。
随着即时编译技术的进步,逃逸分析和标量替换的相关内容,JAVA实例"几乎"都在堆上创建,而不是"绝对"在堆上创建了。
2.分代垃圾回收
JAVA堆是垃圾收集器管理的内存区域,从回收的角度看,经典的Hot-spot虚拟机,垃圾回收算法是新生代和老年代,永久代,Eden空间,FromSurvivor空间,ToSurvivor空间。
内存分代是垃圾回收器的共同特性和风格,不是固定的内存布局。
关于JAVA堆的知识,更多的是在如何进行垃圾回收,这里主要对JVM堆内存进行初步的认识,后面篇章会针对堆内存的垃圾回收进行展开说明。
3.线程私有分配缓冲区(TLAB)
从内存分配角度,所有线程可以从共享的堆中可以分配出多个线程私有的分配缓存区,用以提高对象分配时的效率。
4.堆内存空间如何分配的
- JAVA堆在物理上是
不连续的内存空间
,但在逻辑上应该被视为连续的 - JAVA堆内存
可以是固定
的,也是可以伸缩
的,可以通过-Xms
-Xms
设定可拓展
5.会出现的异常
OOM会在对象实例没有足够的内存分配,也无法再拓展时,会出现OOM的情况
6.总结
Java堆作为线程共享的对象存储中枢,依托分代GC机制实现内存自动化回收,并通过-Xms/-Xmx参数化调控实现性能优化,是预防OOM的核心内存区。
2.5.方法区
1.概念
方法区是各个线程共享的内存区域,主要存储已经被虚拟机加载的类型信息,常量,静态变量,及时编译器编译后的代码缓存。
2.各版本的变化
版本 | 变化 | 备注 |
---|---|---|
JDK6及以前(永久代(PermGen)作为方法区实现 ) | 方法区通过永久代 (位于JVM堆内存中)实现 | 存储内容 1.类元数据(类型信息、方法字节码、字段描述符)2.运行时常量池(含字符串常量池StringTable) 3.静态变量(static变量) |
JDK 7 (“去永久代”启动 ) | 1.保留永久代,但将字符串常量池(StringTable)和静态变量移入堆内存 2.符号引用(Symbols)移至本地内存(Native Heap) | 变化原因 : 1.字符串常量池在永久代回收效率低(仅Full GC触发),移到堆后可被Young GC及时回收2.减少永久代OOM风险,但永久代本身仍存在 |
JDK 8 (元空间(Metaspace)取代永久代 ) | 方法区由元空间 实现,直接使用本地内存 | 变化原因 : 避免永久代OOM,元空间大小仅受物理内存限制 |
3.存储内容的变化
内容 | JDK 6 | JDK 7 | JDK 8 |
---|---|---|---|
类元数据 | 永久代 | 永久代 | 元空间 |
运行时常量池 | 永久代 | 永久代 | 元空间 |
字符串常量池 | 永久代 | 堆 | 堆 |
静态变量 | 永久代 | 堆 | 堆 |
JIT编译代码 | 永久代 | 永久代 | 元空间 |
方法区是JVM规范的概念,而永久代和元空间是HotSpot的具体实现。从JDK6到JDK8,核心变化是:
物理存储从JVM堆转移至本地内存(元空间),字符串常量池与静态变量持久化在堆中,解决了永久代的内存限制和GC效率问题,显著提升动态类加载场景的稳定性。
4.运行时常量池
运行时常量池是方法区的一部分,常量池表表主要存储以下内容:
存储内容 | 包括 |
---|---|
编译期生成的各种字面量 | 字符串,数值,类/接口的全限定名,final常量 |
符号引用 | 字段的符号引用,方法,类的引用 |
运行时常量池具有
动态性
,主要表现在不仅在它允许在程序执行期间动态添加新的常量,而非局限于编译期预设的常量集合。
5.运行时常量池和字符串常量池的区别
特性 | 运行时常量池 | 字符串常量池 |
---|---|---|
位置 | 方法区/元空间 (Java 8+) | 堆内存 (Heap, JDK 7+) |
存储内容 | 类文件中的所有常量:符号引用、数值字面量、字符串字面量的引用 | 实际字符串对象的值 |
作用 | 动态链接、类元数据关联 | 字符串去重、减少内存占用 |
生命周期 | 类加载时创建,类卸载时销毁 | JVM启动时创建,全局共享 |
溢出错误 | OutOfMemoryError: Metaspace | OutOfMemoryError: Java heap space |
代码示例
public class ConstantPoolDemo {
public static void main(String[] args) {
// 情景1:字面量赋值
String s1 = "java"; // 首次出现,创建于字符串常量池
String s2 = "java"; // 复用字符串常量池中的对象
// 情景2:new创建对象
String s3 = new String("java"); // 在堆中创建新对象
String s4 = new String("java"); // 另一个堆中对象
// 情景3:intern操作
String s5 = s3.intern(); // 返回字符串常量池中的引用
// 情景4:动态生成的字符串
String s6 = new StringBuilder("ja").append("va").toString();
System.out.println("s1 == s2: " + (s1 == s2)); // true (都指向字符串常量池)
System.out.println("s1 == s3: " + (s1 == s3)); // false (常量池 vs 堆对象)
System.out.println("s3 == s4: " + (s3 == s4)); // false (两个不同堆对象)
System.out.println("s1 == s5: " + (s1 == s5)); // true (intern返回常量池引用)
System.out.println("s1 == s6: " + (s1 == s6)); // false (动态生成的堆对象)
System.out.println("s1.intern() == s6.intern(): " +
(s1.intern() == s6.intern())); // true (都指向常量池同一对象
)
}
}
其中有两点是不需要注意的是:
intern()
:s3.intern()将尝试把s3的字符串值放入常量池,由于常量池已有"java",直接返回常量池引用- s6在运行时动态生成,不经过字面量处理,不会自动进入常量池,除非显式调用intern(),当常量池存在相同字符串,则返回常量池引用。所以s1.intern() == s6.intern();
内存分布图
重要结论
:运行时常量池存储的是对字符串对象的引用,而字符串常量池存储的是实际的字符串对象。字面量赋值直接使用常量池,而new String()会创建新对象,需要通过intern()方法才能与常量池交互。
2.6.其他
本地内存
本地内存(Direct Memory)并不是虚拟机运行时数据区的一部分,而是《Java虚拟机规范》中定义的内存区域。
在JDK1.4中加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用来进行操作。
查阅文档:
https://www.cnblogs.com/xiaojiesir/p/15590092.html
https://www.cnblogs.com/xiaojiesir/p/15590092.html