JVM-运行时数据区域

1.运行时数据区域

java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干不同的数据区域。

这些数据区域包括:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

1.1 程序计数器

程序计数器是一块较小的内存空间,是当前线程所执行的字节码的行号指示器。在java虚拟机的概念模型(代表了所有虚拟机的统一外观)中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

线程私有!!原因:线程是轮流使用处理器,在交换处理器的使用权的过程中一定会出现上下文切换,为了线程切换后能恢复到正确位置,需要各条线程之间的程序计数器互不影响,独立存储。

线程执行的是java方法:程序计数器记录的是正在执行的虚拟机字节码指令的地址。

线程执行的是本地native方法,程序计数器的值为空。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

1.2 java虚拟机栈

线程私有!!

java虚拟机栈描述的是java方法执行的内存模型。

首先需要来说一下栈帧的概念。

javac -g jvm/demo2.java 编译

1
2
3
4
5
6
7
public int add(int a,int b){
double c = 1.0;
Person p1 = new Person();
String d = "123456";
int f = 2;
return a + b;
}

javap -v jvm/demo2.class 反编译

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
public int add(int, int);
descriptor: (II)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=8, args_size=3
0: dconst_1
1: dstore_3
2: new #2 // class jvm/Person
5: dup
6: invokespecial #3 // Method jvm/Person."<init>":()V
9: astore 5
11: ldc #4 // String 123456
13: astore 6
15: iconst_2
16: istore 7
18: iload_1
19: iload_2
20: iadd
21: ireturn
LineNumberTable:
line 9: 0
line 10: 2
line 11: 11
line 12: 15
line 13: 18
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Ljvm/demo2;
0 22 1 a I
0 22 2 b I
2 20 3 c D
11 11 5 p1 Ljvm/Person;
15 7 6 d Ljava/lang/String;
18 4 7 f I

栈帧存储了:

  • 局部变量表

    它存放了方法参数方法内定义的局部变量。一个变量槽的大小是由具体的虚拟机来定义的,一般来说一个变量槽大小为32bit,所以long和double需要两个变量槽,其他数据类型需要1个。

    局部变量表中,最基本的存储单元是Slot(变量槽),下面表中的Slot表示的是索引开始的位置,可以看到int类型的a变量变量槽的下标是从1开始,int类型的b变量变量槽的下标是从2开始,所以a变量占用1个变量槽,而double类型的c变量槽的下标是从3开始的,引用类型对象p1是从5开始的,因此double类型占用2个变量槽。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    LocalVariableTable:
    Start Length Slot Name Signature
    0 22 0 this Ljvm/demo2;
    0 22 1 a I
    0 22 2 b I
    2 20 3 c D
    11 11 5 p1 Ljvm/Person;
    15 7 6 d Ljava/lang/String;
    18 4 7 f I

    这个内存区域会出现两种异常情况:

    1.如果线程请求的栈深度大于虚拟机所允许的深度(即栈帧太大或者虚拟机栈容量太小),将抛出StackOverflowError

    2.如果java虚拟机栈的容量可以动态扩展,当栈扩展到无法申请到足够的内存会抛出OutOfMemoryError异常。(HotSpot虚拟机的栈容量是不允许动态扩展的,出现这种异常的唯一原因就是(系统可以创建多线程)在创建线程申请内存时就因无法获得足够的内存而出现OOM :unable to create native thread)

  • 操作数栈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Code:
    stack=2, locals=8, args_size=3
    0: dconst_1
    1: dstore_3
    2: new #2 // class jvm/Person
    5: dup
    6: invokespecial #3 // Method jvm/Person."<init>":()V
    9: astore 5
    11: ldc #4 // String 123456
    13: astore 6
    15: iconst_2
    16: istore 7
    18: iload_1
    19: iload_2
    20: iadd
    21: ireturn

    stack=2表示操作数栈大小为2, locals=8,表示局部变量表的长度(Slot的数量)为8, args_size=3表示该方法的形参个数。如果是实例方法,第一个形参是this引用。

  • 动态连接

    动态链接又称为指向运行时常量池的方法引用。每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接( Dynamic Linking)。在Java源文件被编译到字节码文件中时,编译期间生成的各种字面量和符号引用都保存在class文件的常量池里。这部分内容将在类加载后存放到方法区的运行时常量池中。

    比如 11: ldc #4 // String 123456 #4表示去常量池中#4的位置找,可以发现是String类型,它的值在#39,是字符串123456

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Constant pool:
    #1 = Methodref #10.#37 // java/lang/Object."<init>":()V
    #2 = Class #38 // jvm/Person
    #3 = Methodref #2.#37 // jvm/Person."<init>":()V
    #4 = String #39 // 123456
    #5 = Class #40 // jvm/demo2
    #6 = Methodref #5.#37 // jvm/demo2."<init>":()V
    #7 = Fieldref #41.#42 // java/lang/System.out:Ljava/io/PrintStream;
    #8 = Methodref #5.#43 // jvm/demo2.add:(II)I
    ..............
    #39 = Utf8 123456
  • 方法的返回地址

每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

1.3 本地方法栈

线程私有

本地方法栈为虚拟机使用到的本地方法服务。和虚拟机栈差不多(为java方法服务)

1.4 java堆

所有线程共享

java几乎所有的对象实例都是在堆中分配内存。

java堆可能分为新生代、老年代。

新生代:顾名思义,用来存放新生的对象。新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。

老年代:一定时间内都不被回收的对象放在老年代中。老年代的对象比较稳定,所以MajorGC不会频繁执行。

Eden空间、From Survivor空间、To Survivor空间等总结到垃圾收集器时再详细说。

这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个java虚拟机具体实现的固有内存布局(比如HotSpot虚拟机中有采用分代设计的、以及不采用分代设计的垃圾收集器),更不是《java虚拟机规范》中对java堆的进一步细致划分。

在java堆中没有内存完成分配,并且堆也无法再扩展时,java虚拟机将抛出OOM异常。

1.5 方法区

所有线程共享

用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

永久代:主要存放Class和Meta(元数据)的信息。

永久代中很少进行垃圾收集行为,但并非数据进入方法区就“永久”存在了,这个区域的垃圾回收主要是对常量池的回收和对类型的卸载。

JDK8以前,HotSpot虚拟机用永久代来实现方法区。JDK7以前,运行时常量池和字符串常量池放在方法区中,JDK7,字符串常量池被放在了堆中,运行时常量池还在方法区(永久代)中。

JDK8,HotSpot废弃了永久代的概念,采用本地内存的元空间实现方法区。此时运行时常量池还在方法区中,只是现在方法区在本地内存中。

如果方法区无法满足新的内存分配需求时,将抛出OOM(内存溢出)异常。

1.5.1 运行时常量池

运行时常量池是方法区的一部分。

类加载后,保存class文件中描述的符号引用,以及由符号引用翻译出来的直接引用。在运行期间新的常量也可以放入运行时常量池中( String.intern() ),它具有动态性。

当常量池无法申请到内存时,将抛出OOM(内存溢出)异常。


2.常量池、字符串常量池、运行时常量池的区别

2.1 常量池

常量池,即class文件常量池。java文件被编译成 class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool),用于存放编译器生成的各种字面量( Literal )和 符号引用(Symbolic References)。

2.2 字符串常量池

在JDK1.6及更早版本中【String Pool】位于【方法区】

在JDK7及以上版本中【String Pool】位于【堆】

字符串常量池中专门开辟了一个内存区域,定义为StringTable,底层是使用哈希表的形式存储的。

2.3 运行时常量池

运行时常量池始终位于方法区中。常量池中的信息在运行的时候会被加载到运行时常量池

在JDK1.7中【运行时常量池】位于【永久代】

在JDK1.8中【运行时常量池】位于【元空间】

具体我们以一个例子来说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class demo2 {
public static void main(String args[]) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = "a" + "b";
String s5 = s1 + s2;
String s6 = new String("c") + new String("d");
String s7 = new String(s2);
String s8 = s7.intern();
String s9 = "cd";
}
}

反编译结果:

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
62
63
64
  public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=10, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: ldc #4 // String ab
11: astore 4
13: aload_1
14: aload_2
15: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)L
java/lang/String;
20: astore 5
22: new #6 // class java/lang/String
25: dup
26: ldc #7 // String c
28: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String;)V
31: new #6 // class java/lang/String
34: dup
35: ldc #9 // String d
37: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String;)V
40: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)L
java/lang/String;
45: astore 6
47: new #6 // class java/lang/String
50: dup
51: aload_2
52: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String;)V
55: astore 7
57: aload 7
59: invokevirtual #10 // Method java/lang/String.intern:()Ljava/lang/String;
62: astore 8
64: ldc #11 // String cd
66: astore 9
68: return
LineNumberTable:
line 9: 0
line 10: 3
line 11: 6
line 12: 9
line 13: 13
line 14: 22
line 15: 47
line 16: 57
line 17: 64
line 23: 68
LocalVariableTable:
Start Length Slot Name Signature
0 69 0 args [Ljava/lang/String;
3 66 1 s1 Ljava/lang/String;
6 63 2 s2 Ljava/lang/String;
9 60 3 s3 Ljava/lang/String;
13 56 4 s4 Ljava/lang/String;
22 47 5 s5 Ljava/lang/String;
47 22 6 s6 Ljava/lang/String;
57 12 7 s7 Ljava/lang/String;
64 5 8 s8 Ljava/lang/String;
68 1 9 s9 Ljava/lang/String;

首先javac编译器进行编译,在常量池中生成字节码信息。这时a b ab 等都是常量池中的符号,还不是java对象。

String s1 = “a” ;

在运行时,运行到0: ldc #2 // String a的时候,才会将常量池#2位置的a符号变为a字符串对象(懒加载),然后去StringTabe中查找是否有a这个字符串对象, 如果没有,将其放入其中,如果有,这个对象就直接指向StringTable中的这个a字符串对象。

String s3 = “ab”;

String s4 = “a” + “b”;

在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 常量折叠(Constant Folding) 的代码优化。常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,所以这句话在编译过之后实际上就等于 String s4 = “ab”;

这一行也可以看出来s3和s4就是一样的。

1
2
3
4
6: ldc           #4                  // String ab
8: astore_3
9: ldc #4 // String ab
11: astore 4

String s5 = s1 + s2;

字符串对象拼接。

底层等于 new String(“a”+”b”); 它指向存放在堆中新分配的地址中的对象。因此s5指向这个地址。

1
2
3
4
5
13: aload_1
14: aload_2
15: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)L
java/lang/String;
20: astore 5

String s6 = new String(“c”) + new String(“d”);

String s9 = “cd”;

编译时,在常量池中生成c d符号,运行时,创建”c”对象,如果StringTable中没有,就加入到其中,创建”d”对象,如果StringTable中没有,就加入到其中,然后拼接”c” 和”d”为”cd”,在堆中申请内存空间,存入其中,返回这个内存空间的地址。”cd”并没有存入StringTable中。

所以s6 != s9

1
2
3
4
5
6
7
8
9
10
11
        22: new           #6                  // class java/lang/String
25: dup
26: ldc #7 // String c
28: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String;)V
31: new #6 // class java/lang/String
34: dup
35: ldc #9 // String d
37: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String;)V
40: invokedynamic #5, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)L
java/lang/String;
45: astore 6

String s7 = new String(s2);
String s8 = s7.intern();

1
2
3
4
5
6
7
8
47: new           #6                  // class java/lang/String
50: dup
51: aload_2
52: invokespecial #8 // Method java/lang/String."<init>":(Ljava/lang/String;)V
55: astore 7
57: aload 7
59: invokevirtual #10 // Method java/lang/String.intern:()Ljava/lang/String;
62: astore 8

intern方法的作用:主动将串池中还没有的字符串对象放入串池。

  • 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
  • 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

JVM-运行时数据区域
https://vickkkyz.fun/2022/03/24/Java/JVM/2.java内存区域和内存异常/第二章/
作者
Vickkkyz
发布于
2022年3月24日
许可协议