当前位置:   article > 正文

JVM基本概念及内存管理模型_jdk17内存模型

jdk17内存模型
1. JVM基本概念

        JVM(Java Virtual Machine)是Java虚拟机的缩写,是Java平台的核心组件之一,它是一种能够在不同操作系统上运行Java字节码的虚拟机。JVM屏蔽了与具体操作系统平台相关的信息,将Java程序编译成字节码,并在运行时将其解释执行,这样就可以在多种平台上不加修改地运行,从而实现跨平台运行的效果。JVM还提供了Java程序运行所需的内存管理、垃圾回收、安全性、类加载、字节码解释执行和程序性能优化等功能,同时也提供了一些API和工具,使得Java程序开发更加方便和高效。

2. JVM的主要组成部分

JVM的主要组成部分包括类加载器、运行时数据区、执行引擎和JIT(Just In Time compilation)编译器等。
在这里插入图片描述

  • 类加载器:负责加载Java类文件,并将其转换成JVM内部的运行时数据结构;
  • 运行时数据区:包括堆、栈、方法区、运行时常量池等,用于存储Java程序运行时的数据和信息;
  • 执行引擎:负责解释执行Java字节码,并对程序进行优化;
  • JIT编译器:负责在程序运行时动态地将字节码编译成本地机器代码的技术,从而提高程序的执行效率。
3. JVM执行流程
  1. JVM执行过程流程图:
    在这里插入图片描述
  2. JVM具体执行步骤:
    (1)程序员利用IDE集成开发工具编写Java源代码,源文件的后缀为.java;
    (2)再由Java编译器javac将此源文件编译成JVM可识别的class文件(字节码文件);
    (3)再由 JVM 中的类加载器加载该编译后的字节码文件到运行时数据区;
    (4)最后由JVM 执行引擎去执行。
4. JVM内存管理模型

        Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。不同的Java虚拟机其运行时数据区定义也会有所不同。例如:从HotSpot JDK1.8 开始,与HotSpot JDK1.7有如下区别:

  • 由元空间(Metaspace)作为方法区的实现,并从JVM移到了本地内存中。

JDK1.7版本主要分为以下几个区域:
在这里插入图片描述

JDK1.8版本相对JDK1.7版本做了如下调整:
在这里插入图片描述

4.1 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
(1)线程私有内存:
    由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的, 在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。
(2)记录正在执行的虚拟机字节码指令的地址:
    如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。
(3)不会抛OutOfMemoryError异常:
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

4.2 Java虚拟机栈

虚拟机栈描述的是线程中的方法的内存模型:每个方法被执行的时候,都会在虚拟机栈中同步创建一个栈帧(stack frame),方法被执行时入栈,执行完后出栈。
(1)线程私有内存:
    与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
(2)由多个栈帧构成:
    每个方法在执行的同时都会创建一个栈帧(Stack Frame[1])用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
在这里插入图片描述
(3)局部变量表:
    局部变量表中存储了基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、和对象的引用(String、数组、对象等)以及returnAddress类型,但是不存储对象的内容。局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小。局部变量的容量以变量槽(Variable Slot)为最小单位,每个变量槽最大存储32位的数据类型。对于64位的数据类型(long、double),JVM 会为其分配两个连续的变量槽来存储。JVM 通过索引定位的方式使用局部变量表,索引的范围从0开始至局部变量表中最大的 Slot 数量。普通方法与 static 方法在第 0 个槽位的存储有所不同。非 static 方法的第 0 个槽位存储方法所属对象实例的引用。为了尽可能的节省栈帧空间,局部变量表中的 Slot 是可以复用的。方法中定义的局部变量,其作用域不一定会覆盖整个方法。当方法运行时,如果已经超出了某个变量的作用域,即变量失效了,那这个变量对应的 Slot 就可以交给其他变量使用,也就是所谓的 Slot 复用。Slot 复用虽然节省了栈帧空间,但是会直接影响到系统的垃圾收集行为。
(4)栈帧在虚拟机栈中的入栈到出栈:
    每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
(5)会抛出StackOverflowError和OutOfMemoryError异常:
    在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部 分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

4.3 本地方法栈

本地方法栈与虚拟机栈的作用是相似的,只不过虚拟机栈执行的是java方法,本地方法栈执行的是native方法。
(1)为虚拟机使用到的Native方法服务:
    本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
(2)抛出StackOverflowError和OutOfMemoryError异常:
    与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
(3)Native方法是什么:
JDK 中有很多方法是使用 Native 修饰的。Native 方法不是以 Java 语言实现的,而是以本地语言实现的(比如 C 或 C++)。

4.4 Java堆

Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。
(1)堆内存结构:
    Java堆是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代、老年代和永久代(对JDK 1.7的HotSpot虚拟机而言,在JDK1.8之后为metaspace替代永久代);再细致一点新生代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。新生代和老年代是垃圾回收的主要区域。
在这里插入图片描述
(2)堆内存分代的意义:
    从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率。
    对堆内存进行分代后,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。
(3)新生代:
    新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。
    HotSpot将新生代划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
 GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
(4)老年代:
    在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
(5)永久代:
    永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
(6)所有线程共享内存:
    Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
(7)存放对象实例:
    此内存区域的唯一目的就是存放类初始化生成的对象,几乎所有的对象实例都在这里分配内存,包括基本数据类型的数组也是对象实例。
(8)字符串常量池:
    字符串常量池是JVM用来维护字符串实例的一个引用表,它被实现为一个全局的StringTable,底层是一个c++的hashtable。它将字符串的字面量作为key,实际堆中创建的String对象的引用作为value存储在StringTable中。当编译器遇到双引号包裹的字符串时,会拿这个字符串的值(也就是字面量)去StringTable中查找是否有相同值的key,如果有,则直接返回这个key对应到value(也就是实际对象的引用)。如果没有,则会在堆区创建一个新的字符串对象,然后把这个新建的字符串对象的引用当中value存放到StringTable中,并返回该引用。字符串常量池在jdk1.7之前是存放在方法区,从1.7开始存放在堆区。
(9)静态变量:
    被static和final修饰的变量也称为全局变量,每个全局常量在编译的时候就会被赋值。
(10)线程分配缓冲区:
    线程私有,但是不影响java堆的共性,主要是为了提升对象分配时的效率。
(11)可以处于物理上不连续的内存空间中:
    根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。
(12)会抛出OutOfMemoryError异常:
    在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

4.5 方法区

它用于存储已被虚拟机加载的类型信息、class常量池、运行时常量池、即时编译器编译后的代码缓存等。
(1)不同版本演变历程:

  • JDK6及之前,方法区的实现为永久代,静态变量存放在永久代中,字符串常量池(StringTable)位于运行时常量池中。
  • JDK7时,方法区的实现为永久代,但已经逐步“去永久代”,静态变量、字符串常量池从方法区移除,保存在堆中。
  • JDK8开始,方法区的实现为本地内存的元空间,不在属于JVM内存范围,不过字符串常量池、静态变量仍在堆中。

(2)类型信息:对每个加载的类型 (类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  • 这个类型的完整有效名称(全名=包名.类名)。
  • 这个类型直接父类的完整有效名(对于interface或是java.lang. Object,都没有父类)。
  • 这个类型的修饰符( public, abstract,final的某个子集)。
  • 这个类型实现接口的一个有序列表。

(3)域(Field)信息:JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。域的相关信息包括:

  • 域名称;
  • 域类型;
  • 域修饰符(public,private,protected,static,final, volatile,transient的某个子集)。

(4)方法(Method)信息:JVM必须在方法区中保存类型的所有方法的相关信息以及方法的声明顺序。方法的相关信息包括:

  • 方法名称;
  • 方法的返回类型(或void);
  • 方法参数的数量和类型(按顺序);
  • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集);
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native方法除外);
  • 异常表(abstract和 native方法除外),每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引。

(5)class常量池:每个class的字节码文件中都有一个常量池,里面是编译后即知的该class会用到的字面量符号引用

  • 字面量:字面量大约相当于Java代码中的双引号字符串和常量的实际的值,包括:
    • 文本字符串,即代码中用双引号包裹的字符串部分的值;
    • 用final修饰的成员变量的值;
    • 八种基本类型的值;
  • 符号引用主要包括:
    • 类和方法的全限定名;
    • 字段的名称和描述符;
    • 方法的名称和描述符;

(6)运行时常量池:相较于Class文件常量池,运行时常量池更具动态性,在类加载后会将Class文件常量池内容导入到运行时常量池中,在运行期间也可以将新的常量放入池中。

  • JVM在加载某个class的时候,需要完成以下任务:
    • 通过该class的全限定名来获取它的二进制字节流,即读取其字节码文件;
    • 将读入的字节流从静态存储结构转换为方法区中的运行时的数据结构;
    • 在Java堆中生成该class对应的类对象,代表该class原信息。这个类对象的类型是java.lang.Class,它与普通对象不同的地方在于,普通对象一般都是在new之后创建的,而类对象是在类加载的时候创建的,且是单例。
  • 在进行上面第二步的时候,会将class文件常量池内容导入运行时常量池。运行时常量池中的常量对应的内容只是字面量,比如一个"张三",它还不是String对象;当Java程序在运行时执行到这个"张三"字面量时,会去字符串常量池里找该字面量的对象引用是否存在,存在则直接返回该引用,不存在则在Java堆里创建该字面量对应的String对象,并将其引用置于字符串常量池中,然后返回该引用。
  • Java中的基本类型的包装类(Byte、Short、Integer、Long、Character这5种)都实现了常量池技术,默认创建[-128,127]的相应类型的缓存,但是,超出此范围仍然会去创建新的对象。 浮点数类型的包装类Float、Double并没有实现常量池技术。
  • intern()会先去查询字符串常量池中是否有已经存在该字面量的引用,如果存在,则返回字符串常量池中的引用;如果不存在,在JDK1.6下是复制一份原字面量引用的对象到字符串常量池中,而在JDK1.7后,则是检查堆中是否有该字面量的对象,有的话就将存在的对象的引用放到字符串常量池中并返回出去,没有的话就在堆中创建一个新的该字面量对象并将新创建的对象的引用放入字符串常量池中,再将新创建的对象的引用返回出去。

(7)所有线程共享内存:
    方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

(8)垃圾回收条件苛刻:
    Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回 收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。

4.6 直接内存

直接内存是在 java 堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于 java 堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在 java 堆外,因此它的大小不会直接受限于 Xmx (虚拟机参数)指定的最大堆大小,但是系统内存是有限的, java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。直接内存位于本地内存,不属于JVM内存,不受GC管理,但是也会在物理内存耗尽的时候报OOM。

  • 直接内存并非 JVMS 定义的标准 Java 运行时内存。
  • JDK1.4 加入了新的 NIO 机制,目的是防止 Java 堆 和 Native 堆之间往复的数据复制带来的性能损耗,此后 NIO 可以使用 Native 的方式直接在 Native 堆分配内存。
  • 直接内存区域是全局共享的内存区域。
  • 直接内存区域可以进行自动内存管理(GC),但机制并不完善。
  • 本机的 Native 堆(直接内存) 不受 JVM 堆内存大小限制。可能出现 OutOfMemoryError 异常。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小惠珠哦/article/detail/1014569
推荐阅读
相关标签
  

闽ICP备14008679号