运行时数据区
运行时数据区
- JVM 运行期间使用的内存区域,官方叫法是运行时数据区,也有叫 JVM 内存模型或 JVM 内存结构
- JMM 是 java 内存模型,不要搞混了,JMM 是规范多线程操作内存的行为
JVM 把使用的内存分成了 5 部分:
- 程序计数器:下一条要执行的字节码指令的行号。线程私有
- 虚拟机栈(线程栈):当前线程使用的局部变量、方法调用链等。线程私有
- 本地方法栈:同虚拟机栈,native 方法使用。线程私有
- 方法区:存放类的信息,比如一个类的类型是什么、有哪些方法、有哪些变量。线程共享
- 堆:最大的一块内存空间,内部又划分为新生代(伊甸园和两个幸存区)、老年代,垃圾回收的主要区域。线程共享
程序计数器
线程私有,存储下一条字节码指令的行号
-
也叫 PC 寄存器,存储下一条要执行的指令行号
- 前端编译器把 java 编译成 class 文件,后端编译器把 class 文件编译成字节码指令,字节码指令是 CPU 可以执行的内容
- CPU 是交替执行多个线程的(上下文切换),每次切换线程前需要记录下一次要执行的指令行号,下一次接着这个行号继续执行
- 还有条件判断、循环等操作,下一条指令是哪个条件,或者是否跳出循环等,都需要记录在线程内部
-
生命周期跟随线程的创建和结束
-
JVM 中唯一一个不会出现 OOM 的区域
虚拟机栈
线程私有,存放栈帧,一个栈帧就是一个方法,栈帧里面主要存储当前方法需要操作的局部变量和操作数栈
-
在线程增加一个方法调用叫做栈帧入栈,调用完一个方法,这个方法就出栈
-
既然栈帧对应方法调用,那么栈帧里面存储的就是方法调用的相关信息
- 局部变量表:存储方法形参和定义在方法体内部的局部变量
- 如果是非静态方法,会有个 this 变量;非静态方法是没有的,所以静态方法中不能使用 this 关键字
- 不会赋默认值(成员变量才会),所以方法里面定义一个未赋值的变量,在使用的时候 idea 会提示"变量未初始化"
- 这里的对象会作为 GC Root 对象
- 操作数栈:对局部变量进行操作的行为列表,比如读取、运算、比较、修改等操作
- 比如修改局部变量a = a+1,操作数栈存放的内容就是:读取a、a+1、a修改为a+1
- 每个操作都会有对应的命令:load、push、store 等
- 动态链接:比如一个 User 类,对于 JVM 来说这只是一个符号,JVM 要找到这个符号对应的 Class 类,这个指向关系就是动态链接
- 方法返回地址
- 局部变量表:存储方法形参和定义在方法体内部的局部变量
-
运行速度很快,但是空间不大,jdk5 之前一个线程默认大小是 256k,jdk5 之后默认 1M(可自定义)
- 为什么快?内部会使用 CPU 缓存(不是全都是 CPU 缓存,是部分)
- 当内存大小超标时也会发生内存不足:StackOverflow
-
虽然线程私有但还是可能线程不安全,因为栈里的变量可能是一个对象,而对象可能是共享的
本地方法栈
和虚拟机栈类似,不过栈帧是 native 方法运行中使用的内容
方法区
线程共享,存放类信息,比如一个类的类型是什么、有哪些方法、有哪些变量
- JDK1.8 之前叫永久代,1.8 及之后叫元空间
- 永久代使用堆内存,元空间使用直接内存
- 方法区只是定义,定义大致为:存放类信息的地方。永久代存在堆中的,元空间存放在本地内存中
- 垃圾回收效率最低,回收效率从快到慢依次为:新生代(堆) > 老年代(堆) > 方法区
// User:方法区
// user:栈
// new 出来的对象保存在堆中
new Thread(() -> {
User user = new User();
}).start();
堆
一个jvm实例只有一个堆,所有线程共享;jvm 管理的最大的一块内存,垃圾回收的主要区域
-
几乎所有对象都是放在堆中的(不是所有)
- JVM为每个线程在Eden区分配一小块私有内存(TLAB),默认大小约为Eden区的1%(可通过
-XX:TLABSize调整) - 当 new 出来的对象使用的内存空间
<=TLAB 剩余空间,直接栈上分配(哪怕对象逃逸了,只要 TLAB 空间足够,对象也不会存放在堆中) - 标量替换发生时对象也不会存放在堆上(实际对象都不会创建了)
- JVM为每个线程在Eden区分配一小块私有内存(TLAB),默认大小约为Eden区的1%(可通过
-
常量池存放在方法区,运行时常量池存放在堆中,【常量池】和【运行时常量池】两者别搞混了
- 常量池是用来描述类信息的,所以放在方法区
- 运行时常量池就是字符串字面量
- 静态变量也是放在堆中的
-
对在内存结构上又分为老年代、新生代
- 新生代又分为伊甸园区和幸存区
- 幸存区又平均分成两块,幸存一区、幸存二区
- 默认老年代和新生代内存大小比例为
2:1 - 默认伊甸园和两个幸存区的内存大小比例为
8:1:1
-
如果是超大对象,可能直接分配在老年代,具体多大的对象视为超大对象这个是可以配置的
对象两种逃逸的方式
- 方法逃逸:对象作为方法返回值或传递给其他方法
- 线程逃逸:对象被赋值给类变量或可以被其他线程访问的实例变量
// 未逃逸:对象在方法中创建和使用,没有 return 出去(其他地方不会使用到)
void processOrder() {
Order temp = new Order(); // 创建
temp.addItem(item1);
calculateTax(temp); // 使用
}
// 方法逃逸:虽然在访达内部创建,但是 return 出去了,外部还会继续使用
Order createOrder() {
Order order = new Order();
return order;
}
// 线程逃逸:方法内部创建,但是赋值给了类的静态变量,其他线程能够使用到
class Shared {
static Order sharedOrder;
void saveOrder() {
sharedOrder = new Order();
}
}
标量替换
使用基本类型替换对象创建
// 原始代码
void foo() {
Point p = new Point(1, 2);
System.out.println(p.x + p.y);
}
// 优化后(标量替换)
void foo() {
int x = 1, y = 2; // 直接使用基本类型
System.out.println(x + y);
}

浙公网安备 33010602011771号