JVM——运行时数据区域(内存区域)

概述

当我们通过类的加载、验证、准备解析和初始化这几个阶段完成后,就会用到执行引擎对我们的类进行使用,同时执行引擎将会使用到我们运行时数据区。
在这里插入图片描述
Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,即运行时数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域是依赖用户线程的启动和结束而建立和销毁。

Java 虚拟机将所管理的内存分为以下几个运行时数据区域:
在这里插入图片描述

(1) JDK8 之前的 JVM 内存区域图:

在这里插入图片描述

(2)JDK8 及以后的 JVM 内存布局:

在这里插入图片描述

(3)前后变化

在这里插入图片描述

各部分的特点和作用如下:
在这里插入图片描述
数量关系:

1)一个 Java 程序对应一个进程;
2)一个进程对应一个 JVM 实例;
3)一个 JVM 中只有一个运行时数据区;
4)一个 运行时数据区只有一个堆和方法区,多个线程共享;
5)一个 运行时数据区有多个程序计数器、虚拟机栈和本地方法栈,每个线程私有;

1、程序计数器PC

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。
线程私有的,每条线程都有一个独立的程序计数器,它的生命周期与线程相同
程序计数器不存在垃圾回收问题而且此内存区域是唯一一个在 java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域
程序计数器是运行数据区域中访问速度最快的。
如果执行的是 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是 native 方法,计数器为空

主要的后台系统线程在Hotspot JVM里主要是以下几个:

1)虚拟机线程:这种线程的操作是需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括"stop-the-world"的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销。
2)周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行。
3)GC线程:这种线程对在JVM里不同种类的垃圾收集行为提供了支持。
4)编译线程:这种线程在运行时会将字节码编译成到本地代码。
5)信号调度线程:这种线程接收信号并发送给JVM,在它内部通过调用适当的方法进行处理。

2、虚拟机栈

虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法的调用和返回等信息
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程:方法执行时入栈,对当前栈帧进行操作,执行完则出栈。
虚拟机栈的访问速度仅次于程序计数器。
虚拟机栈不存在垃圾回收问题,但是此内存区域在 Java 虚拟机中会出现 OutOfMemoryError 情况

线程私有的,它的生命周期与线程相同,因此不存在数据安全问题

虚拟机栈规定了两种异常状况:

(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常
(2)如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常

在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构在执行引擎运行时,所有指令都只能针对当前栈帧进行操作
在这里插入图片描述

在这里插入图片描述

(1)局部变量表

1)局部变量表是存放方法参数和局部变量的区域。 局部变量表存放了各种基本类型、对象引用和returnAddress类型(指向了一条字节码指令地址)。局部变量没有准备阶段,必须显式初始化如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。字节码指令中的 STORE 指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内。

如果是基本数据类型,直接存放在局部变量表中;如果是引用数据类型(数组、类、接口),则在局部变量表中存放引用类型数据的地址
在这里插入图片描述

2)局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小
3)局部变量表的槽slot

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4)补充说明
在这里插入图片描述

(2) 操作栈

操作栈是个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。
在这里插入图片描述

i++ 和 ++i 的区别:

i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store
memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。
之前之所以说 i++ 不是原子操作,即使使用 volatile修饰也不是线程安全,就是因为,可能 i被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。

(3)动态链接

每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。
在这里插入图片描述

(4)方法返回地址
在这里插入图片描述

方法执行时有两种退出情况:

① 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
② 异常退出。

无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

① 返回值压入上层调用栈帧。
② 异常信息抛给能够处理的栈帧。
③ PC计数器指向方法调用后的下一条指令。

3、本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务
在JVM规范中,并没有对本地方法栈的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在 HotSopt 虚拟机中直接就把本地方法栈和虚拟机栈合二为一。

线程私有的,它的生命周期与线程相同
与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常
在这里插入图片描述
线程开始调用本地方法时,会进入不再受 JVM 约束的世界。本地方法可以通过 JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和 JVM 相同的能力和权限。 当大量本地方法出现时,势必会削弱 JVM 对系统的控制力,因为它的出错信息都比较黑盒。对内存不足的情况,本地方法栈还是会抛出 nativeheapOutOfMemory。
在这里插入图片描述
本地方法

这里是引用
在这里插入图片描述
在这里插入图片描述

5、堆

Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是用来存储对象实例以及数组(当然,引用是存放在 Java 栈中的),几乎所有的对象实例和数组都在这里分配内存。
在这里插入图片描述

堆内存用于存放由 new 创建的对象和数组。在堆中分配的内存,由 java 虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象。
在这里插入图片描述
堆一般实现成可扩展内存大小,使用“-Xms”与“-Xmx”控制堆的最小与最大内存,扩展动作交由虚拟机执行。但由于该行为比较消耗性能,因此一般将堆的最大最小内存设为相等。
在这里插入图片描述

堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。
在这里插入图片描述
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。

Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常

注意:在 JDK1.7 后,字符串常量池从永久代中剥离出来,存放在堆中

6、方法区

方法区(Method Area)是各个线程共享的内存区域,它用于存储每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。注意:在 Class 文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

方法区是一片连续的堆空间,通过 -XX:MaxPermSize 来设定永久代最大可分配空间,当 JVM 加载的类信息容量超过了这个值,会报 OOM:PermGen 错误。
在这里插入图片描述
永久代的 GC 是和老年代(old generation)捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过反射获取到的类型、方法名、字段名称、访问修饰符等信息就是从方法区获取到的。在使用到 CGLib 对类进行增强时,增强的类越多,就需要越大的方法区类存储动态生成的 Class 信息,当存放方法区数据的内存溢出时,会报OutOfMemoryError 异常。

在 JDK1.8 中也就是 Metaspace 内存溢出,可以通过参数 JVM 参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 设置 Metaspace 的空间大小。

Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

Class 文件常量池与运行时常量池

在这里插入图片描述 在这里插入图片描述
在这里插入图片描述
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

垃圾回收

在这里插入图片描述 在这里插入图片描述

注意:jdk1.8 后方法区(Method Area)被元空间(Metaspace)代替

7、方法区的消失与元空间

(1)方法区改变
在这里插入图片描述
在这里插入图片描述

(2)元空间

JDK8 之前,Hotspot 中方法区的实现是永久代(Perm)。
JDK8 永久代已经不存在,开始使用元空间(Metaspace)。Java8 将永久代一分为二:一部分是元空间,另一部分放到堆了。Java8 将符号引用移到在 native heap 中(本地内存),将字符串常量和静态类型变量移到普通的堆区中(这个影响了String的intern()方法的行为)。其他内容(类和类加载器的元数据信息、编译后的代码数据等)移至元空间,元空间并没有处于堆内存上而是直接在本地内存分配
在这里插入图片描述

移除永久代的工作从 JDK1.7 就开始了。JDK1.7 中,存储在永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap,但永久代仍存在于 JDK1.7 中,并没完全移除。譬如符号引用(Symbols)转移到了Hative heap;字面量(字符串池)和类的静态变量(class statics)转移到了 Java heap

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存(MetaSpace 是属于直接内存而不是 JVM 分配的内存)。
在这里插入图片描述

元空间的大小仅受本地内存限制,可以通过以下参数来指定元空间大小:
在这里插入图片描述
元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而不会再出现像永久代的内存溢出错误了,也不会出现泄漏的数据移到交换区这样的事情。用户可以为元空间设置一个可用空间最大值,不设置默认根据类的元数据大小动态增加元空间的容量。对于一个 64 位的服务器端 JVM 来说,其默认的–XX:MetaspaceSize 值为 21MB。也就是说默认的元空间大小是21MB。

元空间的组成

Metaspace 由两大部分组成:Klass Metaspace 和 NoKlass Metaspace。
1)Klass Metaspace

Klass Metaspace 就是用来存 klass 的,就是 class 文件在 jvm 里的运行时数据结构(不过我们看到的类似A.class其实是存在heap里的,是java.lang.Class的对象实例)。
这部分默认放在 Compressed Class Pointer Space 中,是一块连续的内存区域,
紧接着 Heap,和之前的 perm 一样。通过 -XX:CompressedClassSpaceSize 来控制这块内存的大小,默认是 1G。
下图展示了对象的存储模型,_mark 是对象的 Mark Word,_klass 是元数据指针。
在这里插入图片描述
Compressed Class Pointer Space 不是必须有的,如果设置了 -XX:-UseCompressedClassPointers,或者 -Xmx 设置大于 32G,就不会有这块内存,这种情况下 klass 都会存在 NoKlass Metaspace 里。
在这里插入图片描述

2)NoKlass Metaspace

NoKlass Metaspace 专门来存 klass 相关的其他的内容,比如 method,constantPool 等,可以由多块不连续的内存组成
这块内存是必须的,虽然叫做 NoKlass Metaspace,但是也可以存 klass 的内容,上面已经提到了对应场景。
NoKlass Metaspace 在本地内存中分配

元空间的内存管理

在 metaspace 中,类和其元数据的生命周期与其对应的类加载器相同,只要类的类加载器是存活的,在 Metaspace 中的类元数据也是存活的,不能被回收当 GC 发现某个类加载器不再存活了,会把对应的空间整个回收

每个加载器有单独的存储空间。
省掉了GC扫描及压缩的时间。

Metaspace VM 使用一个块分配器(chunking allocator)来管理 Metaspace 空间的内存分配。块的大小依赖于类加载器的类型。
Metaspace VM 中有一个全局的可使用的块列表(a global free list of chunks)。当类加载器需要一个块的时候,类加载器从全局块列表中取出一个块,添加到它自己维护的块列表中。当类加载器死亡,它的块将会被释放,归还给全局的块列表。
块(chunk)会进一步被划分成blocks,每个block存储一个元数据单元(a unit of metadata)。Chunk中Blocks的是分配线性的(pointer bump)。这些chunks被分配在内存映射空间(memory mapped(mmapped) spaces)之外。在一个全局的虚拟内存映射空间(global virtual mmapped spaces)的链表,当任何虚拟空间变为空时,就将该虚拟空间归还回操作系统。
在这里插入图片描述

(3)本地内存
在这里插入图片描述

(4)为什么要使用元空间取代永久代的实现?

1)字符串存在永久代中,容易出现性能问题和内存溢出,比如常见的java.lang.OutOfMemoryError: PermGen space 异常(注意是永久代异常信息)。我们也可以通过启动参数来控制方法区的大小:-XX:PermSize 设置方法区最小空间;-XX:MaxPermSize 设置方法区最大空间。
2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
3) 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
4)将 HotSpot 与 JRockit 合二为一。
在这里插入图片描述

总结

1、 程序计数器

(1)当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
(2)程序计数器是唯一一个在Java虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

2、 Java虚拟栈

(1)存放基本数据类型、对象的引用、方法出口等,线程私有。
(2)栈容量超过 Java 虚拟机栈的最大容量,会抛出 StackOverflowError 异常;也就是栈溢出错误!方法递归产生
(3)如果 Java 虚拟机栈可以动态扩展,无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的 Java 虚拟机栈,会抛出 OutOfMemoryError 异常。也就是OOM内存溢出错误!(线程启动过多)
(4)参数 -Xss 调整JVM栈的大小

3、 Native方法栈

(1)和虚拟栈相似,只不过它服务于Native方法,线程私有。
(2)HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。

4、 Java堆

java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。
Java堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
默认Eden:from :to = 8:1:1

5、 方法区

(1)存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等,回收目标主要是常量池的回收和类型的卸载,各线程共享
(2)方法区在JDK1.7的时候叫做永久代,到JDK1.8之后废弃了永久代改为元空间(meta space)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值