简单理解JVM内存结构组成

一、什么是JVM

定义
Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境),是JRE的一部分,一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM就是为了支持与操作系统无关,实现跨平台性。

二、内存结构

整体包括:类装载器、运行时数据区、执行引擎、垃圾回收。

其中运行时数据区包括:方法区、堆、虚拟机栈、程序计数器、本地方法栈五部分组成。

如下图:

程序计数器

作用:用于保存jvm下一条要执行指令的地址。

特点:

  1. ​ 线程私有

    • CPU会为每个线程分配时间片,当当前线程时间片使用完以后,CPU会切换到另外的线程并执行其中的代码。
    • 程序计数器是每个线程所私有的,当另外的线程时间片执行完以后,会返回来执行当前线程代码,通过程序计数器,就可以知道应该执行哪一条指令。
  2. ​ 不会存在内存溢出

    ​ 因为程序计算器所维护的就是下一条待执行的命令的地址,所以不存在OutOfMemoryError。

虚拟机栈

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

在 Java 虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可以动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

小结:

每个线程运行需要的内存空间,称为虚拟机栈
每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
内存溢出:Java.lang.stackOverflowError 栈内存溢出
发生原因:
1、虚拟机栈中,栈帧过多(无限递归);
2、每个栈帧所占用过大 ;

本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作
系统底层交互,所以需要用到本地方法 。

对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Colleted Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;新生代又被划分为 Eden 空间(伊甸园)、From Survivor 空间(幸存区from)、To Survivor(幸存区to) 空间等。

根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

小结:

通过new关键字创建的对象都会被放在堆内存
所有线程共享,堆内存中的对象都需要考虑线程安全问题
有垃圾回收机制
java.lang.OutofMemoryError :java heap space. 堆内存溢出
堆内存诊断工具:jps、jmap、jconsole、jvirsalvm

方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它永固存储已被虚拟机加载的类信息、常量、静态常量、及时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

jdk7及以前:习惯上把方法区,称为永久代。

jdk8开始: 使用元空间取代了永久代。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于,元空间不在虚拟机设置的内存中,而是使用本地内存

根据 Java 虚拟机规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

小结:

  • 线程共享

  • 存储已被虚拟机加载的类信息、常量、静态常量、编译后的代码

  • 存在内存溢出 ,1.8以前会导致永久代内存溢出,1.8以后会导致元空间内存溢出

常量池

定义:可以看作是一张常量表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等。

一个java源文件中的类、接口,编译后产生一个字节码文件。而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池(包含:数量值、字符串值、类引用、字段引用、方法引用),这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池。

运行时常量池

运行时常量池相对于 Class 文件常量池的另一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译器才能产生,也就是并非置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。

小结:

具备动态性

  • 可以使用String类的intern()方法将新的常量放入池中

  • 常量池:就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法
    名、参数类型、字面量信息

  • 运行时常量池:常量池是.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

反编译class文件查看常量池

在控制台输入 javap -v 类的绝对路径 ,然后能在控制台看到反编译以后类的信息了

javap -v C:\Users\Administrator\Desktop\Practice\pachong\Test\target\classes\com\u
ser\Demo01.class

类的基本信息:

Classfile /C:/Users/Administrator/Desktop/Practice/pachong/Test/target/classes/com/user/Demo01.class
  Last modified 2021-12-21; size 539 bytes
  MD5 checksum b65bd01fa7519f59669b74170637c276
  Compiled from "Demo01.java"
public class com.user.Demo01
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER

常量池:

Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()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            // com/user/Demo01
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/user/Demo01;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               Demo01.java
  #20 = NameAndType        #7:#8          // "<init>":()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               com/user/Demo01
  #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 static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

posted @ 2021-12-21 15:41  时光巷陌  阅读(524)  评论(0编辑  收藏  举报