本文最后更新于:2020年9月5日 晚上

JVM系列(一)-运行时数据区域

本篇文章为这一系列的第一部分,我将描述分析:

1. Java虚拟机内存各个区域的概念
2. 各个区域的作用
3. 各个区域服务的对象
4. 各个区域中可能会产生的问题

1.1 运行时数据区域

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

那么,根据《Java虚拟机规范的规定》,Java虚拟机所管理的内存将会分为以下几个运行时数据区域。

所有线程共享的数据区:

1.方法区
2.堆
3.执行引擎
4.本地方法接口

线程隔离的数据区:

1.虚拟机栈VM Stack(Java栈)
2.本地方法栈
3.程序计数器

1.1.1 程序计数器

1.它是一块较小的内存空间,它可以看作是当前线程所执行【字节码的行号指示器】。

2.字节码解释器就是通过改变它的值来控制下一条需要执行的字节码指令。

3.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

Java虚拟机中的多线程是通过【线程轮流切换】、【分配处理器执行时间】的方式实现的。
所以在任何一个时刻,一个处理器都只会执行一条线程中的指令。

4.为了每一个线程在切换后【能恢复到正确的执行位置】,每条线程都需要有一个【独立】的程序计数器。所以,每条线程之间的计数器互不影响,独立存储。(我们称为“线程私有”的内存)

5.线程如果正在执行一个Java方法,计数器会记录【正在执行虚拟机字节码指令的地址】。如果正在执行的是本地(Native)方法,那么计数器的值应该为空(Undefined)。

6.这个内存区域是【唯一一个】在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

1.1.2 Java虚拟机栈

1.它描述的是【Java方法执行的线程内存模型】:每个方法被执行的时候,虚拟机会同步创建一个【栈帧】,栈帧用于存放局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用到执行完毕的过程,对应栈帧在虚拟机栈从【入栈】到【出栈】的过程。

我们常说的“栈”,通常就是这里的虚拟机栈,或者更多时候准确指的是虚拟机栈中【局部变量表】部分。

2.讲到这里大家肯定想问局部变量表是什么,首选我们要知道局部变量表中存放了【编译期】可知的Java虚拟机中的

- 基本数据类型(boolean、byte、char、short、int、float、long、double)
- 对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的【引用指针】,也可能是指向一个代表对象的【句柄】或者其他与此对象【相关的位置】)
- returnAddress类型(指向一条字节码指令的地址)

3.这些数据类型在局部变量表中的存储空间,以【局部变量槽】(Slot)表示。(除64位长度的long和double类型的数据会占用两个变量槽,其他数据类型只占用一个)。

4.局部变量表所需要的内存空间在编译期间完成分配,一个方法在栈帧中分配的局部变量空间是确定的,在方法运行期间不会改变局部变量表的大小(大小指槽的数量)。真正需要多大的内存空间实现一个槽,是由虚拟机自己决定的。

5.在《Java虚拟机规范》中这个内存区域规定了两类异常情况:

1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2.如果虚拟机栈容量在动态扩容时(HotSpot虚拟机栈容量是不可以动态扩展的)无法申请到足够的内存,将抛出OutOfMemoryError异常。

所以,对于HotSpot来说只要线程申请栈空间成功了,就不会出现OOM。

1.1.3 本地方法栈

上面我们看了Java虚拟机栈,接下来我们再来了解本地方法栈又是什么。

1.首先它们两个发挥的作用非常相似,其区别是:

- 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务
- 本地方法栈为虚拟机执行【本地方法】(Native)服务

注意:在有的Java虚拟机(如HotSpot)直接把本地方法栈和虚拟机栈合二为一,所以也会出现上面两种异常。

1.1.4 Java堆

1.Java堆是虚拟机所管理内存中【最大】的部分。它是被【所有线程共享】的一块内存区域,【在虚拟机启动时创建】。

2.唯一目的就是【存放对象实例】。《Java虚拟机规范》中的描述是:所有的对象实例以及数组都应当在堆上分配。(但是随着Java的发展,这个规定在实现上也变得没有那么绝对)

3.Java堆是【垃圾收集器管理】的内存区域(也称为GC堆)。

扩展:从回收内存的角度看,现代垃圾收集器大部分是基于【分代收集理论】设计的。
所以会出现“新生代”“老年代”“永生代”等名词。

4.从分配内存的角度看,所有线程共享的Java堆,可以划分出多个线程私有的【分配缓冲区】(TLAB),以提高对象分配时的效率。

注意:无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,那就是无论哪个区域,存储的都【只能是对象的实例】。

5.《Java虚拟机规范》中规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

6.Java堆既可以被实现为【固定大小的】,也可以是【可扩展的】。现在主流的Java虚拟机都是按照可扩展的实现(通过-Xmx和-Xms设置)。这两个参数有没有看起来很熟悉的感觉。

7.如果在Java堆中没有内存完成实例分配,并且堆无法再扩展,Java虚拟机将会抛出OutOfMemoryError异常。

1.1.5 方法区

1.方法区和Java堆一样,也是各个【线程共享】的内存区域。

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

3.《Java虚拟机规范》对方法区的约束非常宽松,除了和Java堆一样不需要连续内存、可以选择固定大小和可扩展外,甚至还可以选择【不实现垃圾收集】。

4.该区域的内存回收目标主要是针对【常量池的回收】和对【类型的卸载】。

5.如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。

1.1.6 运行时常量池

1.运行时常量池是【方法区的一部分】。

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

3.《Java虚拟机规范》没有对这一部分做任何细节的要求,不同提供商可以按照自己的需要来实现这一区域。不过一般来说,除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的【直接引用】也存储在运行时常量池中。

4.它是方法区的一部分,自然受到【方法区的内存限制】,当常量池无法申请到内存时,会抛出OutOfMemoryError异常。

1.1.7 直接内存

1.它并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,但是这部分经常被使用。

我们前面的文章讲了NIO,在NIO中引入了一种基于通道和缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存。
然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
这样可以避免Java堆和Native堆中来回复制数据。

2.当各个内存区域总和大于物理内存限制,就会导致动态扩展时出现OutOfMemoryError异常。

参考图书:《深入理解Java虚拟机-Java高级特性与最佳实战》第3版

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!博客中转载文章会注明出处,若有版权问题,请及时与我联系!谢谢!

JVM系列(二)-HotSpot虚拟机对象探秘 上一篇
IO模型 下一篇