前言

JVMJava 程序员绕不过去的坎,今天学习下 Java 虚拟机运行时是怎么分配内存的。

JVM 规范中规定, JVM 运行时内存分为程序计数器、虚拟机栈、本地方法栈、方法区、堆,这5个部分,如下图所示。

程序计数器

用来记录程序执行到哪行代码。在方法调用、循环、分支这些操作的时候,我们需要记录程序执行的位置,方便调用完后回到之前的地方。或者CPU切换到其他线程,当前线程被挂起的时候,如果不记录的话,CPU切换回来的时候不知道之前执行到哪一行代码了。

程序计数器是线程私有的,每个线程都有自己的程序计数器。

虚拟机栈

虚拟机栈算是最复杂的一个了,包含了栈帧,局部变量表,操作数栈,动态链接,返回地址。通常虚拟机栈也叫线程栈,里面的数据是线程私有的。

虚拟机栈默认大小是1024,通过 java -XX:+PrintFlagsFinal -version |grep Thread 来查看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
uintx ParallelGCThreads                         = 4                                   {product}
intx ThreadPriorityPolicy                      = 0                                   {product}
bool ThreadPriorityVerbose                     = false                               {product}
uintx ThreadSafetyMargin                        = 52428800                            {product}
// 大小是 1024
intx ThreadStackSize                           = 1024                                {pd product}
bool TraceDynamicGCThreads                     = false                               {product}
bool UseBoundThreads                           = true                                {product}
bool UseBsdPosixThreadCPUClocks                = true                                {product}
bool UseDynamicNumberOfGCThreads               = false                               {product}
bool UseThreadPriorities                       = true                                {pd product}
bool VMThreadHintNoPreempt                     = false                               {product}
intx VMThreadPriority                          = -1                                  {product}
intx VMThreadStackSize                         = 1024                                {pd product}
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

栈帧

栈帧可以看做是一个方法的容器,里面存放了局部变量表、操作数栈、动态链接和返回地址,线程进行方法调用的时候会为每一个方法创建栈帧,所以一个线程可以对应多个栈帧,我们程序的入口 main 函数就是第一个栈帧。

局部变量表

局部变量表中存放了函数中创建的局部变量和函数声明的形参,比如

1
2
3
public void add(int a,int b){
    int c = a + b;
}

这个例子中局部变量表的大小是3,存放了 a , b , c

操作数栈

操作数栈存放的是我们要操作的数字,比如上面的例子中, ab 是我们要操作的数字,就会把这两个数字放到操作数中,等待操作。操作数栈有点像一个操作的中转站,把要操作的东西放入这里,操作完后放回去。

动态链接

动态链接的作用是我们在方法中调用了另外的方法,我们需要知道这个方法在哪里,所以需要将这个方法的符号引用转换为直接地址,而符号引用存在于方法区中。

返回地址

返回地址有两种情况,一种正常返回,另一种是异常返回。

正常返回的时候通常就是调用者的程序计数器的地址。异常的时候由异常处理器表决定。

一图胜千言

本地方法栈

本地方法栈和虚拟机栈基本相同,不同的是这里针对的是本地方法,这里不再赘述。

方法区

这个名字取的很奇怪,不知道为什么要叫方法区。方法区里面存放的是类的信息,有版本、字段、方法、接口、常量(1.6之前常量池就是在这个地方,1.7之后放到了堆中)、静态变量。

方法区是线程共享的。

这个应该是我们平时接触最多的一类了,平常我们 new 出来的对象都是放到这个区域, GC 回收也主要是对这个区域进行回收。

堆和方法区一样是线程共享的。

堆中的对象按照存储的时间不同,划分为不同的区域,如下所示

一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class HelloWorld
{
    public static void main(String[] args)
    {
        System.out.println(add(1,2));
    }

    public static int add(int a,int b){
        int c = a + b;
        return c;
    }
}

通过 javap -v 反编译后得到如下字节码指令

 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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
Classfile ~/HelloWorld.class
Last modified Apr 10, 2020; size 466 bytes
MD5 checksum 5fab716f162702283230df72c1cb4b51
Compiled from "HelloWorld.java"
class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #5.#20         // HelloWorld.add:(II)I
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(I)V
   #5 = Class              #23            // HelloWorld
   #6 = Class              #24            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               add
  #14 = Utf8               (II)I
  #15 = Utf8               SourceFile
  #16 = Utf8               HelloWorld.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Class              #25            // java/lang/System
  #19 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;
  #20 = NameAndType        #13:#14        // add:(II)I
  #21 = Class              #28            // java/io/PrintStream
  #22 = NameAndType        #29:#30        // println:(I)V
  #23 = Utf8               HelloWorld
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (I)V
{
  HelloWorld();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: iconst_1
         4: iconst_2
         5: invokestatic  #3                  // Method add:(II)I
         8: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        11: return
      LineNumberTable:
        line 5: 0
        line 6: 11

  public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_2
         5: ireturn
      LineNumberTable:
        line 9: 0
        line 10: 4
}
SourceFile: "HelloWorld.java"

我们先从程序的入口看,在 main 中可以看到这么一行 5: invokestatic #3 // Method add:(II)I 这一行静态调用了 #3 ,这个 #3 我们可以在常量池中找到,如下所示 #3 = Methodref #5.#20 // HelloWorld.add:(II)I 从这里可以看出 #3add 的方法引用,我们跟进去看看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static int add(int, int);
descriptor: (II)I
       flags: ACC_PUBLIC, ACC_STATIC
       Code:
         stack=2, locals=3, args_size=2
            0: iload_0
            1: iload_1
            2: iadd
            3: istore_2
            4: iload_2
            5: ireturn
         LineNumberTable:
           line 9: 0
           line 10: 4

Code 中可以看到 stack=2 所以操作数栈的大小是2, locals=3add 方法中有变量 abc 所以局部变量表的长度是3。后面的 args_size 就是形参的个数。 在看具体的指令之前,先来了解一下各个指令是干什么用的

  • iload_x 将局部变量表中x索引的值加载到操作数栈栈顶
  • istore_x 将操作数栈顶元素存入到局部变量表中x索引的位置
  • iadd 将操作数栈顶的两个元素出栈然后相加,并把相加后的结果重新压入栈顶
  • ireturn 返回方法结束

所以对于 add 方法来说,执行的过程就是先把 a 放入操作数栈顶,再把 b 放入操作数栈顶。然后把 a, b 都出栈进行相加,得到结果 3,放入操作数栈顶,这时候操作数栈就只有一个3了。最后 istore_2 把3存放到局部变量表索引为 2 的位置(下标从0开始)。

总结

通过上面的学习,知道了 JVM 内存分为程序计数器、虚拟机栈、本地方法栈、方法区、堆。 通过反编译 Java 后看到字节码,有种当年学汇编的感觉,一个加法需要好几条指令。

参考