JVM

JVM

JVM分为五个区域:方法区、堆、虚拟机栈、本地方法栈、程序计数器

img

image-20210326154551848

1. 程序计数器

一块线程私有的很小的内存空间(每一个线程都有自己的计数器),作为当前线程的行号指示器;

为什么需要程序计数器

我们知道对于一个处理器(如果是多核cpu那就是一核),在一个确定的时刻都只会执行一条线程中的指令,一条线程中有多个指令,为了线程切换可以恢复到正确执行位置,每个线程都需有独立的一个程序计数器,不同线程之间的程序计数器互不影响,独立存储。

注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。

这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域(程序计数器是用来指向下一条要执行的指令的位置,所以他是一个可预见大小的值,没有必要outOfMemory)。

2. 虚拟机栈 JVM Stacks

俗称”栈“,线程私有,生命周期与线程相同。

img

栈帧(Frame)是什么?

栈帧是一种数据结构,用于虚拟机进行方法的调用和执行。

栈帧是虚拟机栈的栈元素,也就是入栈和出栈的一个单元。

栈帧在内存中的位置?

内存 -> 运行时数据区 -> 某个线程对应的虚拟机栈 -> 这里就是栈帧了

入栈与出栈代表什么?

每个方法的执行和结束对应着栈帧的入栈和出栈。

入栈表示被调用,出栈表示执行完毕或者返回异常。

一个虚拟机栈对应一个线程,当前CPU调度的那个线程叫做活动线程;一个栈帧对应一个方法,活动线程的虚拟机栈里最顶部的栈帧代表了当前正在执行的方法,而这个栈帧也被叫做‘当前栈帧’。

栈帧既然是个数据结构,都有哪些数据?

局部变量表、操作数栈、动态链接、方法返回地址、附加信息。

栈帧的大小是什么时候确定的?

编译程序代码的时候,就已经确定了局部变量表和操作数栈的大小,而且在方法表的Code属性中写好了。不会受到运行期数据的影响。

什么是局部变量表?

是一片逻辑连续的内存空间,最小单位是Slot,用来存放方法参数和方法内部定义的局部变量。我觉得可以想成Slot数组....

JVMS7:“any parameters are passed in consecutive local variables starting from local variable 0”

虚拟机没有明确指明一个Slot的内存空间大小。但是boolean、byte、char、short、int、float、reference、returnAddress类型的数据都可以用32位空间或更小的内存来存放。这些类型占用一个Slot。Java中的long和double类型是64位,占用两个Slot。(只有double和long是jvms里明确规定的64位数据类型)

reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。

returnAddress类型:指向一条字节码指令的地址

虚拟机如何调用这个局部变量表?

局部变量表是有索引的,就像数组一样。从0开始,到表的最大索引(Slot的数量-1)

要注意的是,方法参数的个数 + 局部变量的个数 ≠ Slot的数量。因为Slot的空间是可以复用的,当pc计数器的值已经超出了某个变量的作用域时,下一个变量不必使用新的Slot空间,可以去覆盖前面那个空间。(这部分内容在P183页)

特别地,JVMS7:

On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language)

手动翻译:在一个实例方法的调用时,局部变量表的第0位是一个指向当前对象的引用,也就是Java里的this。

什么是操作数栈?

Each frame (§2.6) contains a last-in-first-out (LIFO) stack known as its operand stack.

翻译:每个栈帧都包含一个被叫做操作数栈的后进先出的栈。叫操作栈,或者操作数栈。

1.栈桢刚创建时,里面的操作数栈是空的。

2.Java虚拟机提供指令来让操作数栈对一些数据进行入栈操作,比如可以把局部变量表里的数据、实例的字段等数据入栈。

3.同时也有指令来支持出栈操作。

4.向其他方法传参的参数,也存在操作数栈中。

5.其他方法返回的结果,返回时存在操作数栈中。

操作数栈本身就是一个普通的栈吗?

其实就是普通的栈,再加上数据结构所支持的一些指令和操作。

同时加上一些对类型的区分:

操作数栈是区分类型的,操作数栈中严格区分类型,而且指令和类型也好严格匹配。

栈桢之间是完全独立的吗?

虚拟机做了一些优化:为了避免过多的 方法间参数的复制传递、方法返回值的复制传递 等一些操作,就让一部分数据进行栈桢间共享。

什么是动态链接?

一个方法调用另一个方法,或者一个类使用另一个类的成员变量时,总得知道被调用者的名字吧?(你可以不认识它本身,但调用它就需要知道他的名字)。符号引用就相当于名字,这些被调用者的名字就存放在Java字节码文件里。

名字是知道了,但是Java真正运行起来的时候,真的能靠这个名字(符号引用)就能找到相应的类和方法吗?

需要解析成相应的直接引用,利用直接引用来准确地找到。

举个例子,就相当于我在0X0300H这个地址存入了一个数526,为了方便编程,我把这个给这个地址起了个别名叫A, 以后我编程的时候(运行之前)可以用别名A来暗示访问这个空间的数据,但其实程序运行起来后,实质上还是去寻找0X0300H这片空间来获取526这个数据的。

这样的符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接。

每一个栈帧内部都要包含一个指向运行时常量池的引用来支持动态链接的实现。

需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

Java虚拟机栈可能出现两种类型的异常:

  1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  2. 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

3. 本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

4. 堆 Heap

线程共享,占用内存最大,JVM启动时创建,存放一切对象实例和数组

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。堆内存是所有线程共有的,可以分为两个部分:年轻代老年代

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的(有逃逸分析)。

注意:它是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆。根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx-Xms),如果堆中没有足够内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

字符串常量池(string pool / String table):

字符串常量池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。string pool在每个HotSpot VM的实例只有一份,被所有的类共享。在jdk1.8后,将String常量池放到了堆中。

package com.example;
 
public class Test {
 
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";//在编译时会做优化,直接进行String s3 = "ab";
        String s4 = s1 + s2;//new StringBuilder().append("a“).append("b").toString
        String s5 = "ab";
        String s6 =s4.intern();
 
        System.out.println(s3 == s4);
        //false: toString()方法调用了new String(),只要有new String(),那么就会在堆中开辟一个空间生成新对象实例,同时会引用String Pool中的该字符串(没有的话就会在String Pool中创建)
 
        System.out.println(s3 == s5);
 		//都是对String Pool中"ab"的引用
        System.out.println(s3 == s6);
        //intern()方法会去String Pool中寻找有没有该字符串,如果有则引用
    }
 
}

GC后面再讲

5. 方法区(Method Area)

img

img

img

image-20210326155509699

img

  • 方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。为了和堆区分,也有“非堆(Non-Heap)"的叫法;方法区在启动时创建逻辑上是堆的一部分(在jvm厂商处实现方式不一样)

  • java虚拟机对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。

  • 实现区域
    永久代:存储包括类信息、常量、字符串常量、类静态变量、即时编译器编译后的代码等数据。可以通过 -XX:PermSize-XX:MaxPermSize 来进行调节。当内存不足时,会导致 OutOfMemoryError 异常。JDK8 彻底将永久代移除出 HotSpot JVM,将其原有的数据迁移至 Java Heap 或 Native Heap(Metaspace),取代它的是另一个内存区域被称为元空间(Metaspace)。

  • 元空间(Metaspace):元空间是方法区的在 HotSpot JVM 中的实现,方法区主要用于存储类信息、常量池、方法数据、方法代码、符号引用等。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。,理论上取决于32位/64位系统内存大小,可以通过 -XX:MetaspaceSize-XX:MaxMetaspaceSize 配置内存大小。

  • JVM 为每个加载的类和接口都创建一个 java.lang.Class 实例(JDK6 存储在方法区,JDK6 之后存储在 Java 堆),这个对象存储了所有这个字节码内存块的相关信息,如平时使用的 this.getClass().getName() this.getClass().getDeclaredMethods() this.getClass().getDeclaredFields(),可以获取类的各种信息,都是通过这个 Class 引用获取。

  • class文件常量池(class constant pool)

    可以理解为 *.class文件的资源仓库

    类基本信息:举例HelloWorld.class

    C:\Users\55202\IdeaProjects\untitled1\out\production\untitled1>javap -v HelloWorld.class
    Classfile /C:/Users/55202/IdeaProjects/untitled1/out/production/untitled1/HelloWorld.class
      Last modified 2021-3-26; size 601 bytes
      MD5 checksum 5687fddf5f7080b11d876985f0a54b45
      Compiled from "HelloWorld.java"
    public class HelloWorld
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    

    类方法定义(包括虚拟机指令)

    {
      public HelloWorld();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 1: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   LHelloWorld;
    
      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    //Fieldjava/lang/System.out:Ljava/io/PrintStream;
             //getstatic即是获取静态成员变量System.out,#2指的是引用常量池中的#2,去下面常量池找
             3: ldc           #3    // String HelloWorld
             5: invokevirtual #4	
             //虚方法调用,去找java/io/PrintStream类中的println方法,参数类型是(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 3: 0
            line 4: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  args   [Ljava/lang/String;
    }
    
    

    Constant Pool 常量池:就是一张表,虚拟机从中找到要执行的类名、方法名、参数类型、字面量("HelloWorld"字符串,基本类型Integer、Boolean、Final字段的常量)型等信息:

    Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte、Short、Integer、Long、Character、Boolean,另外 Float 和 Double 类型的包装类则没有实现。另外 Byte、Short、Integer、Long、Character 这5种整型的包装类也只是在对应值在-128到127之间时才可使用对象池。

    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            // HelloWorld
       #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #5 = Class              #23            // HelloWorld
       #6 = Class              #26            // java/lang/Object
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               LocalVariableTable
      #12 = Utf8               this
      #13 = Utf8               LHelloWorld;
      #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          // "<init>":()V
      #21 = Class              #27            // 找到#27的字符串 java/lang/System
      #22 = NameAndType        #28:#29        // out:Ljava/io/PrintStream;
      #23 = Utf8               HelloWorld
      #24 = Class              #30            // java/io/PrintStream
      #25 = NameAndType        #31:#32        // println:(Ljava/lang/String;)V
      #26 = Utf8               java/lang/Object
      #27 = Utf8               java/lang/System
      #28 = Utf8               out					//成员变量名:out
      #29 = Utf8               Ljava/io/PrintStream; //类型:java/io/PrintStream
      #30 = Utf8               java/io/PrintStream
      #31 = Utf8               println
      #32 = Utf8               (Ljava/lang/String;)V
    
    
  • 运行时常量池(runtime constant pool):常量池是*.class文件中的,当类被加载时,其常量池信息被加入到运行时常量池,把符号地址变成真实地址

    常量池只有类文件在编译的时候才会产生,而且是存储在类文件*.class中的。而运行时常量池是在方法区,而且可在JVM运行期间动态向运行时常量池中写入数据,比如String的 intern() 。

posted @ 2021-03-25 17:53  幽灵化石  阅读(54)  评论(0编辑  收藏  举报