Jvm 运行时数据区

726 阅读8分钟

写在前面

本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机。

概述

JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机的进程启动而存在,有的区域则依赖于用户线程的启动和结束而建立和销毁。

HotSpot 运行时数据区

运行时数据区在 HotSpot 1.8 之前的版本和 1.8 版本有所不同,主要是 方法区移到元空间 了。

图 1-1:JDK1.8 之前 JVM 运行时数据区
图 1-2:JDK1.8 JVM 运行时数据区

线程私有区域

程序计数器(PROGRAM COUNTER REGISTER)

程序计数器是一块很小的区域,它存储的是当前线程正在执行的字节码的地址(在这里,其实有两个“当前”,一个是:当前正在被 CPU 执行的线程,另一个是:当前这个被执行的线程中正在被执行的字节码指令)。字节码解释器工作时就是改变程序计数器的值来选取下一条需要执行的字节码。对于单核心而言,多线程是通过线程轮流切换的方式实现的,在任一时刻只有一个线程能够得到 CPU 的执行权从而执行线程中的字节码指令,因此,为了使线程切换后能够恢复到正在执行的字节码的位置,每个线程都需要拥有自己的程序计数器。

注意:程序计数器是唯一的一块在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 的区域。由于它是线程私有的,所以它的生命周期随着线程的创建而创建,随着线程的结束而死亡 。

虚拟机栈(VM STACK)

虚拟机栈也是线程私有的,所以它的生命周期与程序计数器相同。虚拟机栈描述的是 Java 方法执行的内存模型。

每个方法在执行的时候都会创建一个栈帧(一个方法对应一个栈帧,栈帧即栈的基本单位)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被线程执行从开始到结束,就对应着一个栈帧在虚拟机栈中入栈(压栈)和出栈(弹栈)的过程。局部变量表中存放了编译可知的各种基本数据类型(byte,short,int,long,float,double,char,boolean)、对象引用(reference 类型,它存储的是:对象的地址或者是指向代表对象的句柄)。

Java 虚拟机规范中规定了虚拟机栈可能出现的两种异常状况:StackOverflowError 和 OutOfMemoryError。

StackOverflowError: 若当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候会抛出 StackOverflowError。

OutOfMemoryError: 若虚拟机栈动态扩展过程中,如果线程请求申请栈空间无法申请到足够的内存,就会抛出 OutOfMemoryError。

本地方法栈(NATIVE METHOD STACK)

本地方法栈与虚拟机栈类似,虚拟机栈是执行 Java 方法开辟的内存空间,而本地方法栈是执行 Native 方法开辟的内存空间。

与虚拟机栈一样,本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常,抛出条件也是类似的。

线程间共享的内存区域

堆(HEAP)

堆是所有线程共享的一块区域,主要用来存放对象和数组。

在 Java 虚拟机规范中有描述:所有的对象实例和数组都要在堆上分配,但是 随着 JIT(JUST-IN-TIME)编译器的发展与逃逸分析技术的逐渐成熟,并不是所有对象都只在堆上分配了,比如:随着逃逸分析技术的逐渐成熟,在即时能被回收的对象也有可能会在虚拟机栈上分配。

由于现在都采用分代回收算法,所以从内存回收的角度来看,堆还可以细分为:新生代、老年代。新生代又可以分为:Eden 空间、From Survivor 空间、To Survivor 空间。

注意:1.8 中已经彻底将方法区的实现由之前的永久代改为元空间。

方法区(METHOD AREA)

方法区和堆一样也是所有线程共享的一块区域,主要用来存储已经被虚拟机加载的类信息、常量(final 修饰的)、静态变量、即时编译器(JIT)编译后产生的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

永久代就是方法区域?

早些时候,很多开发者更愿意称方法区为“永久代”。其实“永久代”这个称呼的由来是因为 HotSpot 团队并不打算为方法区重新设计垃圾回收算法,为了在方法区中能够沿用堆中的分代回收算法,所以按照堆中的命名方式,将方法去称为“永久代”。对于 JRocket、J9 而言是不存在“永久代”的概念的,所以当 HotSpot 1.8 和 JRocket 合并时,就彻底放弃了“永久代”的概念(其实从 1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。

方法区的垃圾回收很困难!!!

由于 Java 虚拟机规范对方法区的限制非常松,甚至可以不实现垃圾回收,一般而言,这个区域的内存回收很不令人满意,尤其是类型的卸载,条件非常苛刻,但是由于现代框架大量的依赖于 JIT 技术,导致方法区的占用比逐渐提高,所以对于方法区的回收至关重要。根据 Java 虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

运行时常量池(RUNTIME CONSTANT POOL)

JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

这块区域在 1.7 之前原来是方法区的一部分,Class 文件中有一项信息是常量池(或者说是一张常量表,Class 文件以表存储数据)。

图 1-3:Class 文件常量池

运行时常量池存储的东西较为复杂,主要分为字面量和符号引用

字面量

存放的字面量主要包括 常量(final 修饰的),比如:final int x = 1、静态变量(static 修饰的),还有一些其他的字面量。

符号引用

符号引用主要包括:类的完全限定名、字段名称和描述符、方法名称和描述符,包括很多符号,比如:() 也可以看做符号引用。

字面量和符号引用将在类加载(ClassLoader 加载 Class 字节码文件)后进入方法区的运行时常量池中存放。不过,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。运行时常量池相对于 Class 文件常量池一个重要的特征就是具备动态性,Java 语言并不要求常量一定产生于编译期的 Class 文件的常量池中,也并不是只有 Class 文件常量池中的常量才能够进入运行时常量池中,在线程执行方法的过程当中可能产生新的常量存放到运行时常量池中,例如:String 类的 intern() 方法。当运行时常量池无法申请到内存的时候就会抛出 OutOfMemoryError 异常。

直接内存(DIRECT MEMORY)

直接内存并不是 JVM 运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

在 JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

总结

Java 虚拟机包含的内容很多,本篇文章也只是对 Java 内存管理模块的 Java 虚拟机运行时数据区做了简要的分析,关于内存管理模块的其他部分后续会继续更新,敬请期待!

参考

公众号

如果大家想要实时关注我更新的文章以及我分享的干货的话,可以关注我的公众号 我们都是小白鼠