本篇文章为大家展示了怎么用最通俗的方法讲解JVM内存模型,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
备注:本文讲的基于JDK1.8,且1.8之前和之后差距略大,本文对1.8之前的版本只会略微介绍.
JVM说白了,就是个程序,而这个程序运行起来后,就是台计算机,而且和我们平时使用的计算机非常相似,他就是一台虚拟计算机. 那什么是JVM内存模型?就是几个大神写了一个在计算机上运行的虚拟计算机的内存模型. 那计算机的内存模型是什么样的?
各部分功能,相信不从事该行业的人都有相当一部分知道他的大概作用,但我们还是粗略解释一下 名称|速度|介绍 --|--|-- 寄存器|速度特别快|暂存指令等短小精干的数据. 栈|速度块|空间连续. 堆|速度慢|空间不连续,但比硬盘可快多了. 硬盘|速度最慢|就是个仓库.
那么!本篇文章就会在此图中进行讲解 下面多图慎入!
接下来,我们在这个电脑上添加一个虚拟机 既然我们说虚拟机和计算机是一样的,那我们就把上述的堆栈等一堆东西都建一个放进电脑里. 那么放到哪呢?
寄存器放不了.
栈太小.
堆可以,空间不连续我们可以自己搞.
硬盘是一个物理存储,也不行.
由上得出,放到堆里,于是有了下面的样子.
这个图也很好懂,就是把寄存器,堆,栈,硬盘都放到操作系统的堆中了. OK,我们把虚拟机放进来了,那么接下来呢?好像没什么头绪. 既然虚拟机有了,那我们把它运行起来吧. 现在有两个问题
它是怎么运行的? JVM就是个C语言程序
这个程序的功能是什么? 运行的是.class文件.
简单的说,这个程序在运行的时候,会启动一个功能,叫类加载器,这个类加载器加载.class文件后,会把文件中的不同内容,放入到堆栈这些不同的区域中. 那么这些区域都分别放了写什么呢? 区域名称|存储内容|特点 :-|:-|:- 寄存器|代码运行到了哪一行(行话:当前线程正在执行的字节码的行号指示器)|空间小,不会溢出,随线程生灭 本地方法栈|JVM执行的native方法|HotSpot虚拟机不区分虚拟机栈和本地方法栈,两者是一块的 栈|1.局部变量 2.操作栈 3,动态链接 4.返回地址|先进后出,桶式结构 堆|1.实例对象 2.数组 3.字符串常量池 4.静态常量|垃圾回收器会回收没被引用的对象和数组 元数据区(1.8前叫方法区)|1.类信息 2.编译后的代码 3.运行时常量池|1.7前叫方法区,在堆中称为非堆,1.7后放入了本地内存,叫元数据区 接下来我一个个详细解释一下
这个知识点比较简单,==本地方法栈服务的对象是JVM执行的native方法== 总之,线程开始调用本地方法时,不受JVM约束.太多的nativa方法会影响虚拟机的可移植性.
为什么把堆放在栈前讲,是因为这部分比较重要,而且是基础部分.
堆中的内容是线程共有的,所有线程访问堆是同一个区域.
堆中存放的数据是对象实例和数组 例如:
User user = new User();//User是系统中常见的Model类 ↑ └─ new 出来的这个东西,就在堆中,controller同理 ↓ UserController uc = new UserController();//mvc模式下常见的类
堆最大,里面的东西也最多.里面的东西越放越多,但内存就那么大,总有放满的一天,于是,堆中没用的东西就要被回收. 于是这群大神将堆分了几个区,分别为:
字符串常量池 : 其实是C++写的一个hash表,所有的字符串都保存在常量池中. 在http://hg.openjdk.java.net/jdk6/jdk6/hotspot/file/9732f3600a48/src/share/vm/classfile/symbolTable.hpp定义 老年代 : 比例约为 2 新生代 : 比例约为 1 其中新生代又分为: Eden区 : 占新生代的 8/10 Suivivor 0 区 : 占新生代的 1/10 Suivivor 1 区 : 占新生代的 1/10 //当然大小和比例可以通过命令来修改
如图:
再换一张官方的图
这张图可以使用JDK自带的 : jdk/bin/jvisualvm.exe. 打开后选择 - 工具 - 插件 - 可用插件 - 安装VisualGC - 重启软件 - 左侧选择JVM进程 - 右边就会显示Visual GC
那么JVM对各个区域是如何使用的呢?
绝大部分对象生成时都在Eden区,当Eden区装填满的时候,会触发Young GC。
Young GC的时候,在Eden区执行清除,没有被引用的对象直接回收,依然存活的对象会被移送到Survivor区.
Survivor 区分为S0和S1两块内存空间,送到哪块空间呢?
每次Young GC的时候,将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态.
如果Young GC要移送的对象大于Survivor区容量上限,则直接移交给老年代.
那会不会有顽强对象一直留在Surivivor区呢? 答案是不会的,每个对象都有一个计数器,每次YGC都会加1.计数器默认为15,如果某个对象在Survivor 区交换14次之后,则晋升至老年代.
对象在堆的生命周期如下:
至于虚拟机如何将对象标记为未被引用,可以查看 : GC算法.
为什么计算机学科中将这块区域叫为堆(heap),而不是其他任何名词呢? 其实是因为这里的数据是不连续的,也就是分配内存地址是这里一个,那里一个. 如图:
堆的内存是不整齐的,是乱的.是非连续的,就是一堆杂乱的东西,所以称之为堆.
栈中存放的是什么? 栈中其实就是和当前执行方法相关的数据. 栈首先有个首要的特点,他是桶状的,是一个先入后出(FILO)的数据结构.如图:
但栈是线程私有的,而我们的系统通常不只有一个线程,所以栈实际中应当是这样的. 如图:
那图中这些都是什么呢?我们来结合图来说:
空栈 : 首先栈中原本是空的
创建栈 : 在某个线程创建时,虚拟机会为线程创建一个该线程私有的栈.
创建栈帧 : 线程开始执行到第一个方法时,就会在栈中创建一个栈帧,而最新创建的栈帧称为当前栈帧
栈帧中存储的是该方法的一系列信息,包括如下:
1. 局部变量表 用于存放方法参数和方法内部定义的局部变量 局部变量表的容量以变量槽 [Slot] 为最小单位。 在编译期由Code属性中的 [max_locals] 确定局部变量表的大小. 2. 操作数栈 可以理解成在哪里执行当前的这一行代码. 3. 动态链接 在运行时将类常量池中的符号引用转换为直接引用. 简单来说,就是我们的类在编译好后,并不知道其中的代码所调用的方法的地址是什么. 只有在执行到该方法时,才知道调用的具体是哪个实例的方法. 4. 方法返回地址 其实就是标记一个退出的指令,或是遇到异常.则返回到上层栈帧. 下面是术语,可以加深理解 当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令;一种是遇见异常,并且这个异常没有在方法体内得到处理。 无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来 帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
Java在执行时,,就是将各种指令往栈中写入和提取
查看一段代码的字节码可以更好的理解JVM是如何对操作数栈和局部变量表进行操作的.
package com.jasmine.Java高级.JVM.字节码; public class TestJVMStack { public static int a = 123; public int simpleMethod(){ int x = 13; int y = 14; int z = x + y; return z; } public static void main(String[] args) { TestJVMStack s = new TestJVMStack(); System.out.println(s.simpleMethod()); } }
上述代码的字节码为:(略长,不想了解可直接到下面看对于操作栈的操作.)
Classfile /E:/WorkSpace/Idea/MyJava/target/classes/com/jasmine/Java高级/JVM/字节码/TestJVMStack.class Last modified 2019-8-27; size 854 bytes MD5 checksum 15fab830f998782e5087b8626274d45c Compiled from "TestJVMStack.java" public class com.jasmine.Java高级.JVM.字节码.TestJVMStack minor version: 0 major version: 52 /* 类的访问标识 ACC_PUBLIC:代表public ACC_SUPER :用于兼容早期的编译器,新编译器都设置该标记. */ flags: ACC_PUBLIC, ACC_SUPER // 类常量池,也叫 Class常量池 // 第一列为常量类型 // 第二列表示引用的常量或者utf8类型常量值 // 如#1的类型是class,引用的是#2的值 Constant pool: #1 = Class #2 // com/jasmine/Java高级/JVM/字节码/TestJVMStack #2 = Utf8 com/jasmine/Java高级/JVM/字节码/TestJVMStack #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8 a #6 = Utf8 I #7 = Utf8 <clinit> //代表是类初始化阶段 #8 = Utf8 ()V #9 = Utf8 Code #10 = Fieldref #1.#11 // com/jasmine/Java高级/JVM/字节码/TestJVMStack.a:I #11 = NameAndType #5:#6 // a:I #12 = Utf8 LineNumberTable #13 = Utf8 LocalVariableTable #14 = Utf8 <init> // 代表是实例初始化阶段,说白了就是构造方法 #15 = Methodref #3.#16 // java/lang/Object."<init>":()V #16 = NameAndType #14:#8 // "<init>":()V #17 = Utf8 this #18 = Utf8 Lcom/jasmine/Java高级/JVM/字节码/TestJVMStack; #19 = Utf8 simpleMethod #20 = Utf8 ()I #21 = Utf8 x #22 = Utf8 y #23 = Utf8 z #24 = Utf8 main #25 = Utf8 ([Ljava/lang/String;)V #26 = Methodref #1.#16 // com/jasmine/Java高级/JVM/字节码/TestJVMStack."<init>":()V #27 = Fieldref #28.#30 // java/lang/System.out:Ljava/io/PrintStream; #28 = Class #29 // java/lang/System #29 = Utf8 java/lang/System #30 = NameAndType #31:#32 // out:Ljava/io/PrintStream; #31 = Utf8 out #32 = Utf8 Ljava/io/PrintStream; #33 = Methodref #1.#34 // com/jasmine/Java高级/JVM/字节码/TestJVMStack.simpleMethod:()I #34 = NameAndType #19:#20 // simpleMethod:()I #35 = Methodref #36.#38 // java/io/PrintStream.println:(I)V #36 = Class #37 // java/io/PrintStream #37 = Utf8 java/io/PrintStream #38 = NameAndType #39:#40 // println:(I)V #39 = Utf8 println #40 = Utf8 (I)V #41 = Utf8 args #42 = Utf8 [Ljava/lang/String; #43 = Utf8 s #44 = Utf8 SourceFile #45 = Utf8 TestJVMStack.java { // 代表有一个静态变量a,修饰是public static public static int a; descriptor: I flags: ACC_PUBLIC, ACC_STATIC static {}; descriptor: ()V flags: ACC_STATIC Code: // stack : 最大操作数栈,JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度,此处为1 // locals : 局部变量所需的存储空间,单位为Slot,Slot是虚拟机为局部变量分配内存时所使用的最小单位,为4个字节大小. // args_size : 方法参数的个数,这里是0 stack=1, locals=0, args_size=0 0: bipush 123 2: putstatic #10 // Field a:I 5: return // LineNumberTable 该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。 LineNumberTable: line 60: 0 LocalVariableTable: Start Length Slot Name Signature public com.jasmine.Java高级.JVM.字节码.TestJVMStack(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #15 // Method java/lang/Object."<init>":()V 4: return // LineNumberTable 该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系。 LineNumberTable: line 6: 0 // LocalVariableTable 该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/jasmine/Java高级/JVM/字节码/TestJVMStack; public int simpleMethod(); descriptor: ()I flags: ACC_PUBLIC Code: // 这里普通的方法参数的个数为1是因为所有类中的方法都有个隐藏参数this stack=2, locals=4, args_size=1 /******************************************************* * 对操作数栈的操作主要看这里,下面有对这段的详细描述 ******************************************************/ 0: bipush 13 2: istore_1 3: bipush 14 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: iload_3 11: ireturn LineNumberTable: line 62: 0 line 63: 3 line 64: 6 line 66: 10 LocalVariableTable: Start Length Slot Name Signature 0 12 0 this Lcom/jasmine/Java高级/JVM/字节码/TestJVMStack; 3 9 1 x I 6 6 2 y I 10 2 3 z I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #1 // class com/jasmine/Java高级/JVM/字节码/TestJVMStack 3: dup 4: invokespecial #26 // Method "<init>":()V 7: astore_1 8: getstatic #27 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_1 12: invokevirtual #33 // Method simpleMethod:()I 15: invokevirtual #35 // Method java/io/PrintStream.println:(I)V 18: return LineNumberTable: line 70: 0 line 71: 8 line 72: 18 LocalVariableTable: Start Length Slot Name Signature 0 19 0 args [Ljava/lang/String; 8 11 1 s Lcom/jasmine/Java高级/JVM/字节码/TestJVMStack; } SourceFile: "TestJVMStack.java"
上述字节码中的下段代码就是JVM对操作栈的执行顺序.
// 对应代码 13; 0: bipush 13 // 将一个8位带符号整数 13 压入操作栈顶 // 对应代码 x = 13; 2: istore_1 // 从栈顶弹出,并将int类型值存入局部变量表的slot_1中 // 对应代码 14; 3: bipush 14 // 将一个8位带符号整数 14 压入操作栈顶 // 对应代码 y = 14; 5: istore_2 // 从栈顶弹出,并将int类型值存入局部变量表的slot_2中 // 对应代码 x; 6: iload_1 // 从局部变量表的slot_1中装载int类型值,压入操作栈顶 // 对应代码 y; 7: iload_2 // 从局部变量表的slot_2中装载int类型值,压入操作栈顶 // 对应代码 x + y; 8: iadd // 操作数栈中的前两个int相加,并将结果压入操作数栈顶 // 对应代码 z = x + y; 9: istore_3 // 从栈顶弹出,并将int类型值存入局部变量表的slot_3中 // 对应代码 z; 10: iload_3 // 从局部变量表的slot_3中装载int类型值,压入操作栈顶 // 对应代码 return z; 11: ireturn // 返回栈顶元素
由上可见,每次操作其实都是对栈顶或栈顶的多个连续的操作栈进行操作.方法执行完后,会根据方法返回地址,返回上层方法,也就是上一个栈帧,如果全部栈帧都执行完,就认为该线程的内容执行完毕,线程结束生命周期.
JDK 1.7 之前 Java虚拟机规范中定义方法区是堆的一个逻辑部分,但是别名Non-Heap(非堆),以与Java堆区分. JDK 1.8 将方法区从堆中移了出来,==放入了本地内存==中,并且改名为==元数据区==,这是不同版本虚拟机变化最大的地方.
元数据区和堆一样,都是线程共享的.整个虚拟机中只有一个元数据区. 元数据区的大小受到本机内存容量限制,并且允许指定大小,若不指定,元数据区会根据应用程序运行时的需求动态设置大小 元数据区的大小如果达到参数[MaxMetaspaceSize]设置的值,将会触发对死亡对象和类加载器的回收.
元数据区中存放已经被虚拟机加载的 :
1. 运行时常量池 是Class常量池的运行时表现形式. 2. 字段和方法数据 3. 构造函数和普通方法的字节码内容 字面量和静态变量被移到了堆中
如下图:
元数据区其实是由一个个的类加载器存储区组成的.当类加载器不再存活,则该类加载器对应的元数据区被回收.
每一个线程都包含自己的寄存器,保存当前线程执行到了哪一行.
还有一部分,顺带一提 CodeCache是代码缓存区 主要存放JIT所编译的代码 还有Java所使用的本地方法代码也会存储在codecache中. 不同的jvm、不同的启动方式codecache的默认值大小也不尽相同。 ==他也独立在堆之外,是线程共享的==
JIT : 在部分商用虚拟机中(如HotSpot),Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器.
到此
我们介绍了6个模块,分别为:
1. PC寄存器(程序计数器) 2. 本地方法栈 3. 虚拟机栈 4. 堆 5. 元空间 6. CodeCache
那么,最开始那张图就变成了这样:
这就是Java的内存模型了
上面说到的3个常量池
字符串常量池
运行时常量池
类常量池
上述内容就是怎么用最通俗的方法讲解JVM内存模型,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注亿速云行业资讯频道。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。