美文网首页
Java 内存模型

Java 内存模型

作者: Vic_wkx | 来源:发表于2021-03-16 23:24 被阅读0次

    一段简单的 Java 程序,Test.java:

    class Test {
        public static void main(String[] args) {
            System.out.println("Hello, world");
        }
    }
    

    首先使用 javac 命令编译成 Test.class:

    javac Test.java
    

    然后通过 java 命令执行:

    java Test 
    

    输出如下:

    Hello, world
    

    Java 程序在运行过程中,首先会将 .java 文件编译成 .class 字节码文件,Java 程序访问 Java 类时,会使用 ClassLoader 将 .class 文件加载到 JVM (Java visual machine)内存中。

    JVM 中的内存可以划分为两大块:线程间共享区域和线程私有区域。线程间共享区域又分为堆、方法区,线程私有区域分为虚拟机栈、本地方法栈、程序计数器。

    Java 内存模型

    一、堆

    堆(Heap)是 JVM 所管理的内存中最大的一块,它的唯一目的就是存放对象实例。几乎所有的对象实例都是在堆上分配的,因此它也是 GC(Garbage Collector)管理的主要区域。因为堆是线程间共享的,所以分配在堆上的对象如果被多个线程同时访问,需要考虑线程安全问题。

    堆中的内存可以划分为新生代和老年代,新生代又分为 Eden 区和 Survivor 区,在 GC 回收时,不同区域会采用不同的回收策略。

    1.1 OutOfMemoryError

    理论上,虚拟机栈、堆、方法区都有可能发生 OOM(OutOfMemoryError),但大多数情况下是发生在堆中。如以下代码:

    import java.util.ArrayList;
    import java.util.List;
    
    class Test {
        public static void main(String[] args) {
            List<Test> tests = new ArrayList<>();
            while (true) {
                tests.add(new Test());
            }
        }
    }
    

    我们在一个死循环中不断 new 出对象,这会不断占用堆中的内存,当堆内存不够时,必然产生 OOM:

    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
            at java.base/java.util.Arrays.copyOf(Arrays.java:3721)
            at java.base/java.util.Arrays.copyOf(Arrays.java:3690)
            at java.base/java.util.ArrayList.grow(ArrayList.java:237)
            at java.base/java.util.ArrayList.grow(ArrayList.java:242)
            at java.base/java.util.ArrayList.add(ArrayList.java:485)
            at java.base/java.util.ArrayList.add(ArrayList.java:498)
            at Test.main(Test.java:8)
    

    二、方法区

    方法区(Method Area)主要用来存储 JVM 中已经加载的类信息(版本、字段、方法、接口)、常量、静态变量、即时编译器编译后的代码和数据。

    方法区只是 JVM 规范中规定的一块区域,并不是实际实现。HotSpot 在 JDK 1.7 之前使用 “永久区”(Perm 区)来实现方法区,在 JDK 1.8 之后 “永久区”就被移除了,取而代之的是一种叫做“元空间”(metaspace)的实现方式。

    三、虚拟机栈

    虚拟机栈用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在线程中为这个方法创建一个栈帧。所有的栈帧都被放在虚拟机栈中。

    3.1 栈帧

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的一种数据结构。

    一个线程包含多个栈帧,每个栈帧内部主要包含局部变量表、操作数栈、动态链接、返回地址。

    3.1.1 局部变量表

    顾名思义,局部变量表用于存储方法内部创建的局部变量,包括调用方法时传递过来的实参。在 .java 文件被编译成 .class 文件时,就会在方法的 Code 属性表中的 max_locals 数据项中,确定该方法需要分配的最大局部变量表的容量。

    比如,我们创建一个 Test.java 文件:

    class Test {
        public static int add(int k) {
            int i = 1;
            int j = 2;
            return i + j + k;
        }
    }
    

    先使用 javac Test.java 编译出 .class 文件,然后使用 javap -v Test.class 查看字节码:

    class Test
      minor version: 0
      major version: 56
      flags: (0x0020) ACC_SUPER
      this_class: #2                          // Test
      super_class: #3                         // java/lang/Object
      interfaces: 0, fields: 0, methods: 2, attributes: 1
    Constant pool:
       #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
       #2 = Class              #13            // Test
       #3 = Class              #14            // java/lang/Object
       #4 = Utf8               <init>
       #5 = Utf8               ()V
       #6 = Utf8               Code
       #7 = Utf8               LineNumberTable
       #8 = Utf8               add
       #9 = Utf8               (I)I
      #10 = Utf8               SourceFile
      #11 = Utf8               Test.java
      #12 = NameAndType        #4:#5          // "<init>":()V
      #13 = Utf8               Test
      #14 = Utf8               java/lang/Object
    {
      Test();
        descriptor: ()V
        flags: (0x0000)
        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 int add(int);
        descriptor: (I)I
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=3, args_size=1
             0: iconst_1
             1: istore_1
             2: iconst_2
             3: istore_2
             4: iload_1
             5: iload_2
             6: iadd
             7: iload_0
             8: iadd
             9: ireturn
          LineNumberTable:
            line 3: 0
            line 4: 2
            line 5: 4
    }
    SourceFile: "Test.java"
    

    其中的 locals=3 就表示局部变量表的长度是 3,也就是说经过编译后,局部变量表的长度已经确定是 3,分别保存参数 i、j、k。

    3.1.2 操作数栈

    操作数栈(Operand Stack)也常被称为操作栈,与局部变量表一样,操作数栈的最大深度在编译后就确定了。对应上例中的 stack=2,栈中的元素可以是任意 Java 数据类型。

    当一个方法刚刚开始执行时,操作数栈是空的,在方法执行过程中,会有各种字节码指令被压入和弹出操作数栈。

    在上例中,各指令的含义如下:

    • iconst_1 将常量 1 压入操作数栈。int 取 -1~5 时采用 iconst,否则取 -128~127 时采用 bipush,否则取 -32768~32767 时采用 sipush 指令,否则采用 ldc 指令。
    • istore_1 将操作数栈顶元素出栈,放入局部变量表索引为 1 的位置
    • iload_1 将局部变量表中索引为 1 的值压入操作数栈
    • iadd 将操作数栈栈顶的两个元素出栈,相加后重新压入栈顶
    • ireturn 指令,将操作数栈顶的元素返回给上层方法。至此方法执行完毕,局部变量表和操作数栈也会相继被销毁。

    3.1.3 动态链接

    在一个 .class 文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其所在内存地址中的直接引用,而符号引用存在于方法区中。

    Java 虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的就是为了支持方法调用过程中的动态链接(Dynamic Linking)。

    3.1.4 返回地址

    当一个方法执行后,有两种方式退出:

    • 正常退出:代码执行完成或者遇到 return 等方法返回指令
    • 异常退出:执行时遇到未处理的异常

    无论哪种方式退出,方法退出后都需要返回到方法被调用的位置,程序才能继续执行(比如发生异常时,可以在上层调用处再 catch 此异常)。“返回地址”就是用来记录当前方法返回地址的。

    3.1.5 StackOverflowError

    当方法无限递归调用时,就会出现 StackOverflowError。如以下代码:

    class Test {
        public static void main(String[] args) {
            main(null);
        }
    }
    

    运行时就会出现以下错误:

    Exception in thread "main" java.lang.StackOverflowError
            at Test.main(Test.java:3)
            at Test.main(Test.java:3)
            at Test.main(Test.java:3)
            ...
    

    前文说到,每运行一个方法,线程中都会创建一个栈帧。在这个递归调用中,每个方法都没有运行结束,所以每个方法都不会退出,所以每个创建出的栈帧都不会被销毁,最终必然导致 StackOverflowError。

    四、本地方法栈

    本地方法栈(Native Method Stack)和虚拟机栈基本相同,只不过是针对 native 方法,在 HotSpot 中已经将本地方法栈和虚拟机栈合二为一了。

    五、程序计数器

    Java 程序是多线程的,CPU 可以在多个线程中分配执行时间片段。当某一个线程被挂起时,需要记录当前线程正在执行的位置,以便 CPU 重新执行此线程时,知道从何处开始。程序计数器就是用来记录当前线程正在执行的位置的。

    程序计数器中没有规定 OOM 异常,当一个线程正在执行 Java 方法时,程序计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行 native 方法,这个计数器的值为空(Undefined)。

    Java 内存模型详情

    参考文章

    Android 工程师进阶 34 讲
    The Java® Virtual Machine Specification

    相关文章

      网友评论

          本文标题:Java 内存模型

          本文链接:https://www.haomeiwen.com/subject/tnkrcltx.html