11.JDK8内存模型本地方法栈虚拟机栈栈帧结构(局部变量表操作数栈方法出口虚拟机栈与本地方法栈的关系寄存器方法区堆(Heap)jvm中的常量池Metaspace(元空间))

Posted to.to

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11.JDK8内存模型本地方法栈虚拟机栈栈帧结构(局部变量表操作数栈方法出口虚拟机栈与本地方法栈的关系寄存器方法区堆(Heap)jvm中的常量池Metaspace(元空间))相关的知识,希望对你有一定的参考价值。

11.JDK8内存模型
11.1.本地方法栈(Native Method Stacks)
11.2.虚拟机栈(Java Virtual Machine Stacks)
11.3.栈帧结构
11.3.1.局部变量表
11.3.2.操作数栈
11.3.3.方法出口
11.4.虚拟机栈与本地方法栈的关系
11.5.寄存器(The pc Register)
11.6.方法区(Method Area)
11.7.堆(Heap)
11.8.jvm中的常量池
11.9.Metaspace(元空间)
11.10.堆内存划分

11.JDK8内存模型

本文转自:https://www.jianshu.com/p/44df41ebdbf6

通常谈到JVM的内存模型,一般人会想到堆和栈等,那么堆和栈如何理解呢?
是运行时的单位
是存储的单位
通俗来说解决的是程序如何和运行,数据如何处理的问题;而堆解决的是数据如何存储,存储在哪儿的问题。

如上图所示,java虚拟机内存模型主要分为以上五个部分,这里以jdk8为学习对象。

11.1.本地方法栈(Native Method Stacks)

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的。其区别在于虚拟机栈为虚拟机执行Java方法所服务,而本地方法栈则是为虚拟机使用到的native方法所服务。

本地方法栈也是一个私有(线程私有)的内存区域,也是后进先出。

虚拟机可以自由实现它,有的虚拟机(如HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常

11.2.虚拟机栈(Java Virtual Machine Stacks)

每个Java线程都有一个私有Java虚拟机栈,与该线程同时创建。

在虚拟机栈内,每个方法会生成一个栈帧。每个栈帧代表一次次的方法调用,一个方法的执行到执行完成的过程,代表栈帧从入栈到出栈的过程。

虚拟机栈会抛出StackOverflowError和OutOfMemoryError。

11.3.栈帧结构

下图表示了栈帧的组成结构:

11.3.1.局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量

11.3.2.操作数栈

操作数栈是一个后入先出的栈。

一个方法刚开始执行时操作数栈是空的,方法执行过程中会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。

例如执行iadd指令时,就会将最接近栈顶的两个int元素取出并相加,然后将相加的结果再入栈。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点。比如刚才的iadd指令,它取出的元素必须是int的,不能出现诸如long和float类型的变量。

虽然概念模型中不同栈帧之间是完全相互独立的,但大多虚拟机实现中会有一些优化处理:令两个栈帧出现一部分重叠,让下面的栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起重叠在一起,这样在进行方法调用时就可以公用一部分数据,无须进行额外的参数复制传递,如图所示:

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。

静态解析:我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。

动态链接:除去静态解析的另外一部分将在每一次运行期间转化为直接引用,这部分称为动态链接。

所以要执行某个方法时,某个指令(例如invokevirtual)将常量池中的引用作为参数,而根据这个引用就可以找到真正的栈帧。

关于方法的解析与调用,参考:https://blog.csdn.net/reachwang/article/details/103058653

11.3.3.方法出口

方法出口也可以通俗的理解为方法返回方式:在jvm中,方法返回方式有两种:正常和异常

正常出口:当程序执行遇到方法返回的字节码指令,就完成此次方法执行,并根据调用方指定的返回值返回(可以无返回值)。

异常出口:方法在执行中遇到了异常,并且在方法体内没有得到处理,会导致方法退出,这时候不会有任何返回值给调用方。

程序正常退出时,相当于把当前栈帧出栈,调用pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。

程序异常退出时:当程序发生异常时,返回地址需要通过异常表来确定,在栈帧中没有保存异常表。

11.4.虚拟机栈与本地方法栈的关系

为了更好地理解虚拟机栈和本地方法栈的结构模型以及关系,我们以网上的例子简单描述下,如下图:

11.5.寄存器(The pc Register)

Java虚拟机可以支持多个线程同时执行,每个Java线程都有其自己的 pc(程序计数器)寄存器。在任何时候,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。(如果不是native,则该pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果线程当前正在执行的方法是native,则Java虚拟机的pc寄存器值未定义。

pc寄存器中的值就是当前指令所在的内存地址,即returnAddress类型的数据,当线程执行native方法时,pc中的值为undefined。

11.6.方法区(Method Area)

Java虚拟机具有一个在所有Java虚拟机线程之间共享的方法区域。该方法区域类似于常规语言的编译代码的存储区域,或者类似于操作系统过程中的“文本”段。它存储每个类的结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码,包括用于类和实例初始化以及接口初始化的特殊方法。

方法区是在虚拟机启动时创建的。尽管方法区在逻辑上是堆的一部分,但是可以选择不进行垃圾回收或压缩。该规范没有规定方法区域的位置或用于管理已编译代码的策略。方法区域可以是固定大小的,或者可以根据计算的需要进行扩展,如果不需要更大的方法区域,则可以缩小。方法区域的内存不必是连续的。

可能抛出OutOfMemoryError异常。

11.7.五、堆(Heap)

Java虚拟机具有一个在所有Java虚拟机线程之间共享的堆。堆是运行时数据区,从中分配所有类实例和数组的内存。

堆是在虚拟机启动时创建的。对象的堆存储由GC(垃圾收集器)回收;对象永远不会显式释放。Java虚拟机可以根据实现者的系统要求选择GC。堆的大小可以是固定的,也可以根据计算要求进行扩展,如果不需要更大的堆,则可以将其收缩。堆的内存不必是连续的。

可能抛出OutOfMemoryError异常。

在jdk1.8之前的版本对内存空间是不同的,主要区别在于:1.8中删除了永久代,新增了元空间。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过参数来指定元空间的大小。

11.8.jvm中的常量池

参考文章:https://blog.csdn.net/weixin_40999907/article/details/87907083
方法区:运行时常量池
Class文件:常量池
堆:String常量池

11.9.Metaspace(元空间)

本部分转自:https://blog.csdn.net/universe_ant/article/details/58585854

其实,移除永久代的工作从JDK 1.7就开始了。JDK 1.7中,存储在永久代的部分数据就已经转移到Java Heap或者Native Heap。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap。我们可以通过一段程序来比较JDK 1.6、JDK 1.7与JDK 1.8的区别,以字符串常量为例:

package com.paddx.test.memory;

import java.util.ArrayList;
import java.util.List;

public class StringOomMock {

	static String base = "string";
	
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		for(int i = 0; i < Integer.MAX_VALUE; i++) {
			String str = base + base;
			base = str;
			list.add(str.intern());
		}
	}
	
}

JDK1.6的运行结果:

JDK1.7的运行结果:

JDK 1.8的运行结果:

从上述结果可以看出,JDK 1.6下,会出现“PermGen space”的内存溢出,而在JDK 1.7和JDK 1.8中,会出现堆内存溢出,并且JDK 1.8中参数PermSize和MaxPermSize已经失效。因此,可以大致验证JDK 1.7和JDK 1.8中将字符串常量由永久代转移到堆中,并且JDK 1.8中已经不存在永久代的结论。现在我们来看一看元空间到底是一个什么东西?

JDK1.8对JVM架构的改造将类元数据放到本地内存中,另外,将常量池和静态变量放到Java堆里。HotSpot VM将会为类的元数据明确分配和释放本地内存。在这种架构下,类元信息就突破了原来-XX:MaxPermSize的限制,现在可以使用更多的本地内存。这样就从一定程度上解决了原来在运行时生成大量类造成经常Full GC问题,如运行时使用反射、代理等。所以升级以后Java堆空间可能会增加。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间的最大区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对改值进行调整:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对改值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面的两个指定大小的选项外,还有两个与GC相关的属性:

-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间导致的垃圾收集器。

-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。

现在我们在JDK 1.8重新运行一下上面第二部分(PermGen(永久代))的代码,不过这次不再指定PermSize和MaxPermSize。而是制定MetaspaceSize和MaxMetaspaceSize的大小。输出结果如下:

11.10.堆内存划分

以下转自:https://www.cnblogs.com/cjsblog/p/9850300.html

在JDK1.7以及其前期的JDK版本中,堆内存通常被分为三块区域:Young Generation、Old Generation、Permanent Generation for VM Matedata。

在JDK1.8中把存放元数据中的永久内存从堆内存中移到了本地内存中,JDK1.8中JVM堆内存结构就变成了如下:

推统计信息

以上是关于11.JDK8内存模型本地方法栈虚拟机栈栈帧结构(局部变量表操作数栈方法出口虚拟机栈与本地方法栈的关系寄存器方法区堆(Heap)jvm中的常量池Metaspace(元空间))的主要内容,如果未能解决你的问题,请参考以下文章

jvm内存区域之虚拟机栈

JVM内幕:Java虚拟机详解

JVM内幕:Java虚拟机详解

JVM之Java虚拟机详解

02-JVM内存模型:虚拟机栈与本地方法栈

JVM内幕:Java虚拟机详解