JVM运行时数据区

JVM运行时数据区是Java虚拟机在执行Java程序时使用的内存区域,它被划分为几个不同的部分,每个部分有特定的用途,下面是JVM运行时数据区主要组成部分。


方法区(Method Area)

  1. 线程共享的内存区域
  2. 存储已被JVM加载的:类信息、常量、静态变量、即时编译器编译后的代码
  3. 在HotSpot JVM中也被称为”永久代”(PermGen),但在Java 8中被”元空间”(Metaspace)取代
  4. 当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常

Java堆(Java Heap)

  1. 线程共享的内存区域
  2. 在JVM启动时创建
  3. 存储所有对象实例和数组
  4. GC管理的主要区域(“GC堆”)
  5. 内存回收角度来看java堆可分为:新生代和老生代。
  6. 堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

程序计数器(Program Counter Register)

  1. 线程私有的内存区域
  2. 记录当前线程所执行的字节码行号指示器
  3. 执行Java方法时记录正在执行的虚拟机字节码指令地址
  4. 执行Native方法时值为空(Undefined)
  5. 唯一一个在JVM规范中没有规定任何OutOfMemoryError情况的区域

在Java中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑(CPU)。但在CPU上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是基于时间片的,也就是当前的这一纳秒是分配给那个指令的。

线程A在看直播:

现在线程B来了一个视频电话,就会抢夺线程A的时间片,则打断了线程A,线程A就会挂起:

当线程B的视频电话结束,这时线程A在做什么呢?线程是最小的执行单位,他不具备记忆功能,它只负责去干,那这个记忆就由程序计数器来记录:


Java虚拟机栈(Java Virtual Machine Stacks)

  1. 线程私有的内存区域
  2. 生命周期与线程相同
  3. 存储栈帧(Stack Frame),每个方法执行时都会创建一个栈帧
  4. 可能抛出StackOverflowError(栈深度超过限制)和OutOfMemoryError(无法扩展栈时)
  5. 栈帧包含:
  • 局部变量表(Local Variable Table):存储方法参数和局部变量
  • 操作数栈(Operand Stack):方法执行的工作区
  • 动态链接(Dynamic Linking):指向运行时常量池的方法引用
  • 方法返回地址(Return Address):方法执行完毕后的返回位置

常见的QA

一个方法调用另一个方法,会创建很多栈帧吗?

答:会创建,如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面。

栈指向堆是什么意思?

栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址。

递归的调用自己会创建很多栈帧吗?

递归会创建多个栈帧,就是一直排下去。


本地方法栈(Native Method Stack)

  1. 线程私有的内存区域
  2. 为JVM使用到的Native方法服务
  3. 与Java虚拟机栈类似,只是为Native方法服务
  4. 可能抛出StackOverflowError和OutOfMemoryError

运行时常量池(Runtime Constant Pool)

  1. 方法区的一部分
  2. 存储编译期生成的各种字面量和符号引用
  3. 动态性:可以在运行时将新的常量放入池中(String.intern())
  4. 可能抛出OutOfMemoryError

直接内存(Direct Memory)

  1. 不是JVM规范定义的内存区域
  2. 通过NIO的DirectByteBuffer分配
  3. 不受Java堆大小限制,但受本机总内存限制
  4. 可能抛出OutOfMemoryError

直接内存与堆内存的区别

  1. 接内存申请空间耗费很高的性能,堆内存申请空间耗费比较低
  2. 直接内存的IO读写的性能要优于堆内存,在多次读写操作的情况相差非常明显

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.nio.ByteBuffer;

/**
* 直接内存 与 堆内存的比较
*/
public class ByteBufferCompare {
public static void main(String[] args) {
allocateCompare(); //分配比较
operateCompare(); //读写比较
}

/**
* 直接内存 和 堆内存的 分配空间比较
*/
public static void allocateCompare() {
int time = 10000000; //操作次数
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {

ByteBuffer buffer = ByteBuffer.allocate(2); //非直接内存分配申请
}
long et = System.currentTimeMillis();
System.out.println("在进行" + time + "次分配操作时,堆内存:分配耗时:" + (et - st) + "ms");
long st_heap = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
ByteBuffer buffer = ByteBuffer.allocateDirect(2); //直接内存分配申请
}
long et_direct = System.currentTimeMillis();
System.out.println("在进行" + time + "次分配操作时,直接内存:分配耗时:" + (et_direct - st_heap) + "ms");
}

/**
* 直接内存 和 堆内存的 读写性能比较
*/
public static void operateCompare() {
//如果报错修改这里,把数字改小一点
int time = 1000000000;
ByteBuffer buffer = ByteBuffer.allocate(2 * time);
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
buffer.putChar('a');
}
buffer.flip();
for (int i = 0; i < time; i++) {
buffer.getChar();
}
long et = System.currentTimeMillis();
System.out.println("在进行" + time + "次读写操作时,堆内存:读写耗时:" + (et - st) + "ms");
ByteBuffer buffer_d = ByteBuffer.allocateDirect(2 * time);
long st_direct = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
buffer_d.putChar('a');
}
buffer_d.flip();
for (int i = 0; i < time; i++) {
buffer_d.getChar();
}
long et_direct = System.currentTimeMillis();
System.out.println("在进行" + time + "次读写操作时,直接内存:读写耗时:" + (et_direct - st_direct) + "ms");
}
}

运行结果

1
2
3
4
在进行10000000次分配操作时,堆内存:分配耗时:98ms
在进行10000000次分配操作时,直接内存:分配耗时:8895ms
在进行1000000000次读写操作时,堆内存:读写耗时:5666ms
在进行1000000000次读写操作时,直接内存:读写耗时:884ms

这些内存区域协同工作,支持Java程序的执行,每个区域都有其特定的用途和生命周期管理方式。