文章资讯
七、JVM内存结构
2022-07-22 10:15  浏览:1
JVM 被分为三个主要的子系统:类加载器子系统、运行时数据区和执行引擎

在 Java 虚拟机规范中,定义了五种运行时数据区,分别是 Java 堆、方法区、虚拟机栈、本地方法栈、程序计数器 ! 顺道提一句运行时常量池也会进入方法区,也就是说方法区中就已经包括了常量池,
Java 堆是内存空间占据的最大一块区域了,Java 堆是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里,HotSpot 虚拟机来说,在 JDK1.7 的时候,方法区被称作为永久代, 从 JDK1.8 开始,Metaspace (元空间)也就是我们所谓的方法区!,当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据,方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式,也就是说,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间,
以 HotSpot 虚拟机来说,在 JDK1.8 之前,方法区也被称作为永久代,这个方法区会发生我们常见的 java.lang.OutOfMemoryError: PermGen space 异常,注意是永久代异常信息,我们也可以通过启动参数来控制方法区的大小,JDK6 及之前,方法区的实现是永久代,静态变量和字符串常量池存放在永久代中,其中,字符串常量池存放在运行时常量池
,JDK7,方法区的实现还是永久代。但是静态变量以及字符串常量池已经被移到了堆中
,JDK8 之后就没有永久代这一说法变成叫做元空间(meta space),而且将老年代与元空间剥离。元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而不会再出现像永久代的内存溢出错误了,也不会出现泄漏的数据移到交换区这样的事情,
1、为永久代设置空间大小很难确定,2、对永久代进行调优十分困难,因为永久代的回收频率比较低,只在 FullGC 的时候才会被回收,FullGC 只会在老年代或者永久代空间不足时才会触发。如果有大量的字符串被创建,放在永久代,由于永久代的回收频率低,会导致永久代空间不足。如果放到堆里,能够及时回收内存,将下面的测试代码使用 javac 编译为 *.class 文件,先将示例代码编译为 *.class 文件,然后将 class 文件反编译为 JVM 指令码。然后观察 .class 字节码中到底包含了哪些部分,从上面的反编译字节码中可以看到,Class 的常量池其实就是一张记录着该类的一些常量、方法描述、类描述、变量描述信息的表,常量池中主要存放两类数据,一是字面量、二是符号引用,常量池,符号引用,在解释器解释执行每条 JVM 指令码的时候,根据这些指令码的符号地址去常量池中找到对应的描述。然后解释器就知道该执行哪个类的那个方法、方法的参数是什么等,上面我们分析了常量池其实就是一张对照表,常量池是 *.class 文件中的。当类的字节码被加载到内存中后,他的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的符号地址变为真实地址(内存中的地址),和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务;,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存,程序计数器的主要两个作用,

JVM内存结构
  • 一、题目
  • 二、JVM整体体系
  • 三、JAVA堆
  • 四、方法区
    • 4.1、方法区、元空间、永久代之间的关系
    • 4.2、JDK6 及之前的方法区
    • 4.3、JDK7的方法区
    • 4.4、JDK 1.8 以后的方法区
    • 4.5、为什么要将永久代替换成元空间
    • 4.6、字符串常量池为什么放到堆
    • 4.7、运行时常量池
      • 1、类的二级制字节码包含哪些信息
      • 2、反编译验证
      • 3、什么是常量池以及常量池的作用
        • a、什么是常量池
        • b、常量池中有哪些内容
        • c、常量池的作用
        • d、运行时常量区
  • 五、虚拟机栈
  • 六、本地方法栈
  • 七、程序计数器

一、题目

说说你对 JVM 内存区域的了解

二、JVM整体体系

JVM 被分为三个主要的子系统:类加载器子系统、运行时数据区和执行引擎

在 Java 虚拟机规范中,定义了五种运行时数据区,分别是 Java 堆、方法区、虚拟机栈、本地方法栈、程序计数器 ! 顺道提一句运行时常量池也会进入方法区,也就是说方法区中就已经包括了常量池

特别注意其中 堆和方法区是 线程共享 的。其他都是 线程私有 的

三、JAVA堆


Java 堆是内存空间占据的最大一块区域了,Java 堆是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里

四、方法区

HotSpot 虚拟机来说,在 JDK1.7 的时候,方法区被称作为永久代, 从 JDK1.8 开始,Metaspace (元空间)也就是我们所谓的方法区!

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

4.1、方法区、元空间、永久代之间的关系

方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式

也就是说,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现便成为元空间


以 HotSpot 虚拟机来说,在 JDK1.8 之前,方法区也被称作为永久代,这个方法区会发生我们常见的 java.lang.OutOfMemoryError: PermGen space 异常,注意是永久代异常信息,我们也可以通过启动参数来控制方法区的大小

-XX:PermSize:设置方法区最小空间 -XX:MaxPermSize:设置方法区最大空间

4.2、JDK6 及之前的方法区

JDK6 及之前,方法区的实现是永久代,静态变量和字符串常量池存放在永久代中,其中,字符串常量池存放在运行时常量池

4.3、JDK7的方法区

JDK7,方法区的实现还是永久代。但是静态变量以及字符串常量池已经被移到了堆中

4.4、JDK 1.8 以后的方法区

JDK8 之后就没有永久代这一说法变成叫做元空间(meta space),而且将老年代与元空间剥离。元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而不会再出现像永久代的内存溢出错误了,也不会出现泄漏的数据移到交换区这样的事情

4.5、为什么要将永久代替换成元空间


1、为永久代设置空间大小很难确定

一个应用动态加载的类的多少是很难确定的,如果永久代设置的过小,会频繁触发 FullGC,并且可能会出现 OOM;而元空间的大小只收本地内存限制,这样出现 OOM 的机会比较小

2、对永久代进行调优十分困难

调优是为了减少 FullGC,如果永久代频繁触发 FullGC,会使永久代调优变得困难

4.6、字符串常量池为什么放到堆

因为永久代的回收频率比较低,只在 FullGC 的时候才会被回收,FullGC 只会在老年代或者永久代空间不足时才会触发。如果有大量的字符串被创建,放在永久代,由于永久代的回收频率低,会导致永久代空间不足。如果放到堆里,能够及时回收内存

4.7、运行时常量池 1、类的二级制字节码包含哪些信息
  • 常量池
  • 类的基本信息(比如:类的访问权限、类的名称、实现了哪些接口)
  • 类的方法定义(包含了虚拟机指令,也就是把我们代码编译为了虚拟机指令)
2、反编译验证

将下面的测试代码使用 javac 编译为 *.class 文件

public class HelloWorld {
 
    public static void main(String[] args) {
 
        System.out.println("hello world");
    }
}

先将示例代码编译为 *.class 文件,然后将 class 文件反编译为 JVM 指令码。然后观察 .class 字节码中到底包含了哪些部分

// ==================类的描述信息====================
Classfile /xx/xx/xx/xx/HelloWorld.class
  Last modified 2021-10-12; size 569 bytes
  MD5 checksum 7f4f0fe4b6e6d04ddaf30401a7b04f07
  Compiled from "HelloWorld.java"
public class org.memory.jvm.t5.HelloWorld
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
    
// =======================常量池=========================
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."
 
  ":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // org/memory/jvm/t5/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               
  
    #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lorg/memory/jvm/t5/HelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "
   
    ":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 hello world #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 org/memory/jvm/t5/HelloWorld #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V // =================虚拟机中执行编译的方法======================= {
     public org.memory.jvm.t5.HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."
    
     ":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/memory/jvm/t5/HelloWorld; // main 方法 JVM 指令码 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V // main 方法访问修饰符描述 flags: ACC_PUBLIC, ACC_STATIC // main 方法中的代码执行部分 // =============解释器读取下面的 JVM 指令解释并执行================ Code: stack=2, locals=1, args_size=1 // 从常量池中符号地址为 #2 的地方,先获取静态变量System.out // Field java/lang/System.out:Ljava/io/PrintStream; 0: getstatic #2 // 从常量池中符号地址为 #3 的地方加载常量 hello world // String hello world 3: ldc #3 // 从常量池中符号地址为 #4 的地方获取要执行的方法描述,并执行方法输出hello world // Method java/io/PrintStream.println:(Ljava/lang/String;)V 5: invokevirtual #4 // main方法返回 8: return============= // 行号映射表 LineNumberTable: line 9: 0 line 10: 8 // 局部变量表 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; } 
    
   
  
 
3、什么是常量池以及常量池的作用 a、什么是常量池

从上面的反编译字节码中可以看到,Class 的常量池其实就是一张记录着该类的一些常量、方法描述、类描述、变量描述信息的表

b、常量池中有哪些内容

常量池中主要存放两类数据,一是字面量、二是符号引用

常量池

  • 比如 String类型的字符串值或者定义为 final 类型的常量的值

符号引用

  • 类或接口的全限定名(包括他的父类和所实现的接口)
  • 变量或方法的名称
  • 变量或方法的描述信息
  • this
c、常量池的作用

在解释器解释执行每条 JVM 指令码的时候,根据这些指令码的符号地址去常量池中找到对应的描述。然后解释器就知道该执行哪个类的那个方法、方法的参数是什么等

  1. 当解释器解释执行 main 方法的时候,读取到下面的 11 行 JVM 指令码 0: getstatic #2
  2. getstatic 指令表示获取一个静态变量,#2 表示该静态变量的符号地址,解释器通过 #2 符号地址去常量池中查找 #2 对应的静态变量
  3. 然后解释器继续向下运行,执行第 13 行的 3: ldc #3 指令,该指令的含义是:从常量池中加载符号地址为 #3 的常量
  4. 然后解释器继续向下运行,执行第 15 行的 5: invokevirtual #4 指令,该指令的含义是:执行方法,那么要执行哪个方法呢?执行常量池中符号地址为 #4 的方法
 // main 方法 JVM 指令码
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    // main 方法访问修饰符描述
    flags: ACC_PUBLIC, ACC_STATIC
    // main 方法中的代码执行部分
    // ==========解释器读取下面的 JVM 指令解释并执行==============             
    Code:
      stack=2, locals=1, args_size=1
         // 从常量池中符号地址为 #2 的地方,先获取静态变量 System.out
         // Field java/lang/System.out:Ljava/io/PrintStream;
         0: getstatic     #2                  
         // 从常量池中符号地址为 #3 的地方加载常量 hello world
         // String hello world
         3: ldc           #3                  
         // 从常量池中符号地址为 #4 的地方获取要执行的方法描述,并执行方法输出 hello world
         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         5: invokevirtual #4                  
         // main方法返回
         8: return
    // ============解释器读取上面的 JVM 指令解释并执行============
d、运行时常量区

上面我们分析了常量池其实就是一张对照表,常量池是 *.class 文件中的。当类的字节码被加载到内存中后,他的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的符号地址变为真实地址(内存中的地址)

五、虚拟机栈
  • Java 虚拟机栈是线程私有的,生命周期和线程相同**,描述的是 Java 方法执行的内存模型,方法调用的数据都是通过栈传递的**;
  • 实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息;
六、本地方法栈

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

本地方法执行时,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息;

七、程序计数器

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存

程序计数器的主要两个作用

  • 字节码解释器通过改变程序计数器来选取下一条需要执行的字节码指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理;
  • 多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道该线程上次运行到哪儿了;

转载请标明出处,原文地址:https://blog.csdn.net/weixin_41835916 如果觉得本文对您有帮助,请点击支持一下,您的支持是我写作最大的动力,谢谢。

,