深入浅出Java虚拟机篇-运行时的数据区域


  • 文章大容来自《深入理解Java虚拟机》

    一、运行时数据区域(JVM内存模型)

​ 在Java虚拟机中执行Java程序的过程都会把他所管理的内存划分为若干个不同的数据区域。这些区域各有各自的用途,有的区域随着虚拟机的进程的启动而一直存在,有的区域则是随着用户线程的启动而启动,结束而销毁。这些区域根据《Java虚拟机规范》的规定共同组成了运行时区域。也就是JVM的内存模型的组成。

img

1.1 程序计数器(Program Counter Register)

1.1.1 程序计数器是什么?🧐又能干什么?

程序计数器简单的理解,他是当前线程所执行的字节码行号指示器。通过它可以知道下一条要执行字节码指令。他是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等都需要依赖它来完成。

注意:由于Java虚拟机中的多线程是通过线程轮流切换来分配处理器执行时间的,也就是在任何一个时刻,处理器都只会处理一条线程中的指令,并且的话处理器不敢保证每个线程的程序都会执行完成,也就是说当线程一执行一半切换到线程二时,如果没有程序计数器记录当前线程中指令执行的位置,那就会导致每次线程再次被处理器执行时,就会从头再来。因此,为了线程切换后能恢复到正确的执行位置每条线程都需要一个自己独有的程序计数器,各个线程之间的程序计数器互不影响,独立储存。所以我们称这一类的内存区域为”线程私有“.

1.2 JAVA虚拟机栈(Java Virtual Machine Stack)

​ java虚拟机栈是线程私有的,他的生命周期与线程相同。虚拟机栈是Java方法执行的线程内存模型:每个方法被执行时,都会同步创建一个栈帧(Stack Frame)用于储存局部变量表操作数栈动态链接方法出口等信息。每个方法的调用到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

1.2.1 局部变量表

​ 局部变量表存放了编译期何种Java虚拟机基本数据类型、对象引用(引用指针、句柄或者于此对象相关的位置)和returnAddress。

​ 这些数据在局部变量表中都以局部变量槽(Slot)来表示,其中64位的长度的long和double类型的数据会占两个变量槽,其余的只会占一个。

1.2.2 异常

在《Java虚拟机规范中》中,虚拟机栈规定了两类的异常状况:

StackOverflowError:当线程请求的栈深度大于了虚拟机所允许的栈深度就会抛出该异常 StackOverflowError(堆栈溢出错误)

demo:

    private static int stackLength;
    public static void stackLeak(){
        stackLength++;
        stackLeak();
    }

    /**
     * 最大堆大小: -Xmx5M
     */
    public static void main(String[] args) {
        try {
            stackLeak();
        }catch (Throwable e){
            e.printStackTrace();
            System.out.println("stack length: "+ stackLength);
        }

    }
    // 虚拟机栈深度为:23812  抛出StackOverFlowError异常
    // Exception in thread "main" java.lang.StackOverflowError
    // stack length: 23812

在上面的案例中我们可以看出他是一段简单的递归调用,但是并没有指定结束条件。就导致了成了一个死循环,这时我们在下图中可以看出来,他会不断的进行入栈,直到栈深度大于了虚拟机限制的深度,抛出了 java.lang.StackOverflowError

在这里插入图片描述

OutOfMemoryError: 当栈扩展时无法申请到足够的内存时就会抛出OutOfMemoryError。但是在HotSpot 中虚拟机栈容量是不可以的动态扩展的,也就是不会出现OOM, 但其实不然,如果申请时就失败,仍然会出现OOM异常。

1.3 本地方法栈(Native Method Stack)

本地方法栈与虚拟机栈类似,区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈是为了虚拟机栈使用的。

1.4 Java 堆(Heap)

​ Java堆是线程共享的内存区域,在虚拟机创建时创建,此内存区域的唯一目的就是存放实例对象,在《JAVA虚拟机规范》中队java 堆描述是:”The heap is the runtime data area from which memory for all class instances and arrays is allocated(所有的Java对象实例以及数组在堆上分配)“。这个几乎就很有深度。随着Java语言的发展,现在可以看到未来可能会出现值类型的支持,由于即时编译的技术进步,像逃逸分析、栈上分配、标量替换优化手段,Java对象实例分配在堆已经不在呢么绝对了。

Java是垃圾回收管理的内存区域,因此也称作”GC堆”,现在垃圾收集器大部分都是基于分代收集理论设计的,因此经常会出现 ”新生代“、”老年代“、”Eden 空间“、”From Survivor 空间“、”To Survivor 空间“等名词。 在十几年前, HotSpot虚拟机内部的垃圾回收内存器是全部 经典分代来设计的:新生代、老年代收集器搭配工作。在这种背景下这些说发还是能不会产生太多的歧义的。但是到了如今的今天,垃圾收集器技术与十几年前早已不同,HotSpot里面也出现了非分代设计的垃圾收集器。

下图为分代设计的垃圾收集器内存模型:

img

1.3.2 异常

​ 当Java堆中没有内存完成实力分配,并且无法扩展时,Java虚拟机就会抛出 OutOfMemoryError异常。

demo:

/** 
  * -Xmx2M
  * Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  *     at com.xphu.DemoTest.main(DemoTest.java:5)
  */
public static void main(String[] args) {
    byte[] bytes = new byte[1024 * 1024 * 1024];
    System.out.println(bytes.length);
}

    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BdFPQuzF-1622222314288)(C:\Users\24267\AppData\Roaming\Typora\typora-user-images\image-20210529002804273.png)]

1.5 方法区(Method Area)

​ 方法区和堆一样,是线程共享的内存区域,它用来储存虚拟机加载的类信息、常量、静态变量以及即时编译后的代码缓存数据。他还有一个别称叫做 非堆 ,目的就是为了区分java堆。

​ 方法区在JDK8之前,许多人都叫做永久代。,其实本质上本质上两者并不是等价的,因为仅仅是当时的HosSpot虚拟机设计团队选择吧垃圾收集器的分代设计至方法区,或者说使用永久代来实现方法区而已,使得HotSpot虚拟机的垃圾回收器能够像管理Java堆一样管理者部分的内存,省的专门为方法区编写内存管理代码的工作。这部分内存的区域的回收主要针对常量池的回收和对类型的卸载。

1.6 运行时常量池(Runtime Constant Poll)

​ 运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Poll Ttable), 用于存放编译期间生成的各种字面量与符号引用,这部分内用将在类加载后存档到方法区的运行时常量池中。