java虚拟机内存管理

2023-12-30 15:30:57

概要

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

在这里插入图片描述
其中各个部分的概述如下:

名称特征作用配置参数异常
程序计数器线程私有,生命周期与线程相同字节码行号指示器
虚拟机栈线程私有,生命周期与线程相同,使用连续的内存空间存储信息如上图-XssStackOverflowError/
OutOfMemoryError
线程共享,生命周期与虚拟机相同,可以不使用连续的内存地址保存对象实例,所有对象实例(包括数组)都要在堆上分配-Xms -Xsx -XmnOutOfMemoryError
方法区线程共享,生命周期与虚拟机相同,
可以不使用连续的内存地址
存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据-XX:PermSize:16M
-XX:MaxPermSize64M
-XX:MetaspaceSize=16M
-XX:MaxMetaspaceSize=64M
OutOfMemoryError
本地方法栈线程私有为虚拟机使用到的Native 方法服务StackOverflowError/
OutOfMemoryError

JVM分为五大模块: 类装载器子系统 、 运行时数据区 、 执行引擎 、 本地方法接口 和 垃圾收集模块 。

在这里插入图片描述

一、jdk7与jdk8内存结构的差异

Java7和Java8内存结构的不同主要体现在方法区的实现
方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。
我们通常使用的Java SE都是由Sun JDKOpenJDK所提供,这也是应用最广泛的版本。而该版本使用的VM就是HotSpot VM。通常情况下,我们所讲的java虚拟机指的就是HotSpot的版本。

JDK7的内存结构
在这里插入图片描述
永久代是 hotspot 在1.7及之前才有的设计,1.8+,以及其他虚拟机并不存。可以说,永久代是1.7的 hotspot 偷懒的结果,他在堆里划分了一块来实现方法区的功能,叫永久代
因为这样可以借助堆的垃圾回收来管理方法区的内存,而不用单独为方法区再去编写内存管理程序。
同时代的其他虚拟机,如 J9 , Jrockit 等,没有这个概念。后来 hotspot认识到,永久代来做这件事不是一个好主意。1.7已经从永久代拿走了一部分数据(静态变量和运行时常量池转移到了堆中),直到1.8+彻底去掉了永久代,方法区大部分迁移到了 metaspace (注意不是全部,不是全部)

JDK8的内存结构
在这里插入图片描述
从jdk1.8开始已经将方法区中实现的永久代去掉了,并用元空间( class metadata space )代替了之前的永久代,元空间的存储位置是:本地内存/直接内存,并且将方法区大部分迁移到了元空间。

方法区Java8之后的变化小结:

  • 移除了永久代(PermGen),替换为元空间(Metaspace
  • 永久代中的class metadata(类元信息)转移到了native memory(本地内存,而不是虚拟机)
  • 永久代中的interned Strings(字符串常量池) 和 class static variables(类静态变量)转移到了Java heap
  • 永久代参数(PermSize MaxPermSize)-> 元空间参数(MetaspaceSize MaxMetaspaceSize

Java8 为什么使用元空间替代永久代,这样做有什么好处呢?

官方给出的解释是:

  1. 移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。
  2. 永久代内存经常不够用或发生内存溢出,抛出异常java.lang.OutOfMemoryError: PermGen 。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,但回收率都偏低,成绩很难令人满意;
  3. PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等,而jdk1.8以后的元空间大小就只受本机总内存的限制(如果不设置参数的话),因为它使用的是本地内存。

二、程序计数器

程序计数器(Program Counter Register):也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

PC寄存器的特点

(1)区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存,虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
(2)当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined
(3)程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
(4)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

在这里插入图片描述
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处
理器只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数
器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

三、虚拟机栈

3.1 什么是虚拟机栈

Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

在这里插入图片描述

public class StackDemo {
    public static void main(String[] args) {
        StackDemo sd = new StackDemo();
        sd.A();
    }

    public void A() {
        int a = 10;
        System.out.println(" method A start");
        System.out.println(a);
        B();
        System.out.println("method A end");
    }

    public void B() {
        int b = 20;
        System.out.println(" method B start");
        C();
        System.out.println("method B end");
    }

    private void C() {
        int c = 30;
        System.out.println(" method C start");
        System.out.println("method C end");
    }
}

在这里插入图片描述

3.2 什么是栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
在这里插入图片描述

3.3 栈帧的组成

栈帧大体都包含四个区域:局部变量表、操作数栈、动态连接、 返回地址

局部变量表
部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的longdouble类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

操作数栈

操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

操作数栈作用小结:

  1. 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
  2. 操作数栈就是JVM执行引擎的一个工作区, Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指-操作数栈
  3. 为了实现java的跨平台,选择了面向操作数栈的指令集架构而没有选择直接基于CPU寄存器的指令集架构(由执行引擎面向更底层),基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束,但是栈架构指令集的主要缺点是执行速度相对来说会稍慢一些,因为栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于CPU来说,内存始终是执行速度的瓶颈;

动态链接
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
动态链接的作用是将符号引用转换成直接引用

返回地址
方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

四、本地方法栈

本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
特点:

  1. 本地方法栈加载native方法
  2. 是线程私有,生命周期跟线程相同,每个线程都有一个
  3. 跟java虚拟虚拟机栈一样,规定了两种类型异常:
    a) StackOverFlowError :线程请求的栈深度大于所允许的深度。
    b) OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存

五、堆

对于Java应用程序来说, Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。 Java堆是被所 有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。

5.1 堆的特点

  1. Java虚拟机所管理的内存中最大的一块
  2. jvm所有线程共享(堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB)
  3. 在虚拟机启动时创建
  4. 几乎所有的对象实例以及数组都在这里分配内存
  5. java堆时垃圾收集器管理的主要区域
  6. 从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
  7. 堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms-Xmx控制)。
  8. 方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除
  9. 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

5.2 堆的结构

现在垃圾回收器都使用分代理论,堆空间也分类如下:
在Java7 Hotspot虚拟机中将Java堆内存分为3个部分:

  • 青年代Young Generation
  • 老年代Old Generation
  • 永久代Permanent Generation

在这里插入图片描述
在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不存在了

在这里插入图片描述

5.3 堆的参数配置

参考:参数配置

JVM中存储java对象可以被分为两类:

  1. 年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Spacefromto)。
  2. 年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁.

-XX:NewRatio=ratio
Sets the ratio between young and old generation sizes. By default, this option is set to 2. The following example shows how to set the young/old ratio to 1:
-XX:NewRatio=1

-XX:NewRatio=2:默认值,标识新生代占1,老年代占2,新生代占整个堆的1/3;
修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5

XX:SurvivorRatio=ratio
Sets the ratio between eden space size and survivor space size. By default, this option is set to 8. The following example shows how to set the eden/survivor space ratio to 4:
-XX:SurvivorRatio=4

Eden空间和另外两个Survivor空间占比分别为8:1:1
可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8

在这里插入图片描述
堆的总大小可由-Xms -Xmx来配置

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

六、方法区

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

方法区中存储的信息大致可分以下两类:

  1. 类信息:主要指类相关的版本、字段、方法、接口描述、引用等
  2. 运行时常量池:编译阶段生成的常量与符号引用、运行时加入的动态变量

方法区在虚拟机规范里这是一个逻辑概念,元空间、永久代是方法区具体的落地实现。

在这里插入图片描述
在jdk1.6里,用永久代来实现方法区,物理空间上用的时堆的内存(目的是利用堆的垃圾回收来管理方
法区的内存)。字符串常量是运行时常量池的一部分,也就是归属于方法区,放在了永久代里。这个时候经常会出现的一个错误就是:java.lang.OutOfMemoryError: PermGen space
jdk1.7已经从永久代拿了一部分数据(静态变量和运行时常量池)转移到了堆中

6.1 方法区结构

在这里插入图片描述
类加载器将Class文件加载到内存之后,将类的信息存储到方法区中

方法区中存储的内容:

  • 类型信息(域信息、方法信息)
  • 运行时常量池

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

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

域信息
域信息,即为类的属性,成员变量
JVM必须在方法区中保存类所有的成员变量相关信息及声明顺序。
域的相关信息包括:域名称、域类型、域修饰符(pυblicprivateprotectedstaticfinalvolatiletransient的某个子集)

方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  1. 方法名称方法的返回类型(或void
  2. 方法参数的数量和类型(按顺序)
  3. 方法的修饰符publicprivateprotectedstaticfinalsynchronizednativeabstract的一个子集
  4. 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstractnative方法除外)
  5. 异常表( abstractnative方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏
    移地址、被捕获的异常类的常量池索引

6.2 运行时常量池

在jvm规范中,方法区除了存储类信息之外,还包含了运行时常量池。这里
首先要来讲一下常量池的分类:

  • Class常量池(静态常量池)
  • 运行时常量池
  • 字符串常量池(没有明确的官方定义,其目的是为了更好地使用String

常量池经常会被搞混,要准确地理解,首先来看基本定义

静态常量池:存放编译期间生成的各种字面量与符号引用
运行时常量池:常量池表在运行时的表现形式

编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池(静态常量池)中的信息加载到内存中,存储在了方法区的运行时常量池中。

什么叫字面量与符号引用呢?

静态常量池

.class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池 ( Constant Pool Table ),用于存放编译期间生成的各种字面量和符号引用,之所以说它是静态的常量池是因为这些都只是躺在 .class 文件中的静态数据,此时还没被加载到内存.

/**
 * 1. 使用jdk1.8编译
 * 2. 使用 javap -v ClassConstantPool.class
 */
public class ClassConstantPool {
    private static String a = "abc";
    private String f = "def";
    private static int b = 123;
    private final int c = 456;
    private int d = 789;
    private float e;
    Gucci gucci = new Gucci();

    class Gucci {

    }
}

反编译后,截取部分信息如下:
在这里插入图片描述

字面量:给基本类型变量的赋值就叫做字面量或字面值,字面量是编译后生成的产物。
比如:String a = "b",这里的“b”就是字符串字面量,同样类推还有整数字面量,浮点类型字面量,字符字面量
符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA 在编译的时候一个每个 java 类都会被编译成一个 class 文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(类加载的一个过程)就是为了把这个符号引用转化成为真正的地址。
比如: ClassConstantPool 类被编译成一个 class 文件时,发现引用了 Gucci 类,,但是在编译时并不知道 Gucci 类的实际内存地址,因此只能使用符号引用( com/ocean/constance/ClassConstantPool$Gucci )来代替。而在类装载器装
Guuci 类时,此时可以通过虚拟机获取 Guuci类 的实际内存地址,因此便可以将符号 com/ocean/constance/ClassConstantPool$Gucci 替换为 Guuci 类的实际内存地址。

运行时常量池

运行时常量池( Runtime Constant Pool )是每一个类或接口的常量池( Constant_Pool )的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。

运行时常量池是在类加载完成之后,将 静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。 另外运行时常量池的物理存储位置要注意两点:

  1. 运行时常量池在 JDK1.7 版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。
  2. 在 JDK1.8 中,使用元空间代替永久代来实现方法区,但是方法区从定义上并没有改变,所谓 “Your father will always be your father” 。变动的只是方法区中内容的物理存放位置,运行时常量池和字符串常量池被移动到了堆中而并没有在元空间。但是不论它们物理上如何存放,逻辑上还是属于方法区的。

字符串常量池

字符串常量池这个概念是有争议的,很多正式的虚拟机规范文档,都没有对这个概念作一个明确的官方定义。

以 JDK1.8 为例,字符串常量池是存放在堆中,并且与 java.lang.String类有很大关系。设计这块内存区域的原因在于: String 对象作为 Java 语言中重要的数据类型,是内存中占据空间最大的一个对象。高效地使用字符串,可以提升系统的整体性能。

七、元空间

在JDK1.7之前,HotSpot 虚拟机用永久代来实现方法区。而从 JDK 1.8 开始,移除永久代,用元空间来实现方法区,它位于本地内存中,而不是虚拟机内存中。

永久代跟元空间对比如下:

  • 存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
  • 存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信
    息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。

Metaspace相关参数:

  • XX:MetaspaceSize·,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如
    果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。如果没有使用该参数来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。如果设置了该参数,当Metaspace剩余空间不足,会抛出:java.lang.OutOfMemoryError: Metaspace space
  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
    -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

文章来源:https://blog.csdn.net/weixin_42612223/article/details/135208263
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。