JVM拾遗:类加载与运行时数据区程序计数器&虚拟机栈

188 阅读15分钟

类加载子系统

类的加载过程

  1. 加载

    通过一个类的全限定名获取定义此类的二进制字节流

    将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口

    补充:类加载的方式

    • 网络获取,如Web Applet
    • zip获取,如jar和war
    • 运行时计算生成,最多为:动态代理
    • 其他文件生成,如JSP
    • 从专有数据库获取.class文件,少见
    • 从加密文件获取,防止被反编译的措施
  2. 链接

    • 验证

      确保文件正确,不危害虚拟机安全

    • 准备

      为类变量分配内存并设置该类变量的默认初始值,整型:0,浮点型:0.0,char:\u0000,引用:null

      这里不包含使用final修饰的static,因为final在编译阶段就会进行分配了,准备阶段会显示初始化

      这里不会为实例变量进行初始化,类变量会分配在方法区中,实例变量会随着对象一起分配到java堆中

    • 解析

      将常量池内部的符号引用转化为直接引用的过程

      事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行

      符号引用就是一组符号来描述所引用的目标。直接引用就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄

      解析动作主要针对类或接口,字段,类方法,接口方法,方法类型等。对应常量池中的相关属性。

  3. 初始化

  • 初始化阶段就是执行类构造器方法()的过程:此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的合并起来。clinit中的指令按照在源文件出现顺序执行(一般情况下,如果是位置上的后声明先赋值,那么结果还是先赋值的结果)
  • <clinit>() 不同于类的构造器:构造器是虚拟机层面的()
  • 若该类有父类,JVM会保证在子类的()执行前,父类已执行完毕
  • JVM保证一个类的()在多线程下被同步加锁,执行且仅执行一次

自定义类加载器

为什么自定义类加载器
  • 隔离加载类:防止与其他中间件jar冲突
  • 修改类的加载方式:只有核心类库需要被Bootstrap Classloader加载,其他在需要的时候再进行加载
  • 扩展加载源
  • 防止源码泄露:加解密
实现步骤
  • JDK1.2后把类的加载逻辑写在findClass中
  • 如果没有过于复杂的需求,可以直接继承URLClassLoader,可以避免充血findClass和获取字节码流的方式,更加简洁。

双亲委派机制

  1. 如果一个类加载器接收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父加载器进行加载
  2. 如果父类加载器还存在其父加载器,进一步向上委托,进行递归,最终到达最顶层的类加载器
  3. 如果父加载器可以完成类加载任务,直接返回;若无法加载,子加载器会尝试进行加载

优势:

  • 避免类的重复加载
  • 沙箱安全机制:保护程序安全,防止核心API被随意篡改,如:
    • 自定义核心类库存在类:java.lang.String
    • 自定义核心类库不存在类:java.lang.shkStart
    • SecurityException:Prohibited package name: java.lang

其他

在JVM中表示两个class对象是否为同一个类的两个必要条件
  • 类的全限定名必须一致
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
对类加载器的引用

  JVM必须知道一个Class是由启动加载器加载的还是用户加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是一致的。

类的主动使用和被动使用

主动使用的七种情况

  • 创建类的实例
  • 访问某个类或者接口的静态变量,或者对该静态变量进行赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类
  • Java虚拟机启动时main()所在的类
  • JDK7开始提供动态语言支持,java.lang.invoke.MethodHandle实例的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic句柄对应的类没有初始化,则初始化

运行时数据区

程序计数器(线程私有)

作用
  • 用来存储指向下一条指令的地址(native方法为空),也就是即将要执行的指令代码
  • 线程切换后能恢复到正确的执行位置(在虚拟机概念模型中,字节码解释器工作时就是用过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等都需要依赖这个程序计数器)
两个常见问题

1.为什么使用程序计数器来记录当前线程的执行地址?

基于时间片轮转的操作系统会让CPU不停的切换各个线程,切换回来之后,需要知道从哪里继续

JVM的字节码解释器需要改变程序计数器的值来明确下一条应该执行什么样的字节码指令

2.程序计数器为什么是线程私有

为了能够准确的记录各个线程正在执行的当前字节码指令地址,且占有的内存很小

虚拟机栈(线程私有)

定义

  栈是运行时的单位,堆是存储的单位,即:栈解决程序的运行时问题,即程序如何执行,如何处理数据。堆解决的是数据存储问题,即数据怎么放,放在哪。

  每个线程在创建时都会创建虚拟机栈,内部保存一个个栈帧,对应一次次的方法调用

  生命周期与线程一致

特点
  • 访问速度仅次于程序计数器
  • JVM直接对栈的操作只有两个:方法开始执行的入栈和方法执行完毕的出栈
  • 对于栈来说不存在垃圾回收的问题,因为只有两个简单的操作
栈运行原理
  • 不同线程中所包含的栈帧是不允许存在互相引用的,即不可能在一个栈帧中引用另外一个线程的栈帧,也是线程私有的原因。
  • 如果当前方法调用了其他方法,方法返回时,当前栈帧会传回此方法的执行结果给前一个栈帧;然后虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
  • Java方法有两种返回方式,**一种是正常的返回,使用return指令;一种是抛出异常。**这两种方式都会导致栈帧被弹出。
栈帧的内部结构
局部变量表
  • 为一个数字数组,主要用于存储方法参数和定义在方法内部的局部变量,这些数据包括基本数据类型,对象引用,以及returnAddress(返回值)类型

    • 局部变量表大小是在编译期定下来的。保存在方法的Code属性的maxmium local variables数据项中。

下为java代码

public static void main(String[] args) {
  //创建分治任务线程池
  ForkJoinPool fjp = new ForkJoinPool(4);
  //创建分治任务
  Fibonacci fib = new Fibonacci(30);
  //启动分治任务
  Integer result = fjp.invoke(fib);
  //输出结果
  System.out.println(result);
}

下为截取部分字节码

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=3, locals=4, args_size=1
       0: new           #2                  // class java/util/concurrent/ForkJoinPool
       3: dup
       4: iconst_4
       5: invokespecial #3                  // Method java/util/concurrent/ForkJoinPool."<init>":(I)V
       8: astore_1
       9: new           #4                  // class fageiguanbing/test/Test$Fibonacci
      12: dup
      13: bipush        30
      15: invokespecial #5                  // Method fageiguanbing/test/Test$Fibonacci."<init>":(I)V
      18: astore_2
      19: aload_1
      20: aload_2
      21: invokevirtual #6                  // Method java/util/concurrent/ForkJoinPool.invoke:(Ljava/util/concurrent/ForkJoinTask;)Ljava/lang/Object;
      24: checkcast     #7                  // class java/lang/Integer
      27: astore_3
      28: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      31: aload_3
      32: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      35: return
    LineNumberTable:
      line 87: 0
      line 89: 9
      line 91: 19
      line 93: 28
      line 94: 35
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      36     0  args   [Ljava/lang/String;
          9      27     1   fjp   Ljava/util/concurrent/ForkJoinPool;
         19      17     2   fib   Lfageiguanbing/test/Test$Fibonacci;
         28       8     3 result   Ljava/lang/Integer;

LineNumberTable 保存代码与字节码映射关系

LocalVariableTable 保存局部变量个数以及对应的生命周期的长度(字节码)

  • 方法嵌套调用的次数由栈的大小决定。栈大小由栈帧决定,栈帧又由参数和局部变量决定

  • 关于slot的理解

    • 局部变量表中最基本的存储单元是slot
    • 在局部变量表里,32位以内的类型只有一个slot(包括returnAddress类型),64位的类型(long,double)占用两个slot。
    • 32位类型都会被转为int存储
    • 栈帧中的局部变量表中的槽位是可以重用的,和作用域有关,以节省资源。
操作数栈
  • 在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈和出栈

    java代码

    int i = 1;
    int j = 2;
    int k = i + j;
    

    字节码

    0 iconst_1
    1 istore_1
    2 iconst_2
    3 istore_2  // 写入
    4 iload_1   // 提取
    5 iload_2
    6 iadd&emsp;&emsp;  // 执行引擎来执行这个操作:加法
    7 istore_3
    8 return
    
  • 也会保存计算过程中间的结果,同时作为计算过程中变量的临时存储空间

  • 数组实现栈结构,但是并不以索引方式访问数组,而是入栈和出栈操作;其所需大小在编译期就已确定,保存在方法的Code属性中,为stack的值。

  • 栈中元素为任意类型,32bit一个栈单位深度,64bit两个栈单位深度

  • 新的栈帧创建之初,操作数栈为空

  • 如果被调用的方法带有返回值的话,其返回值会被压入当前栈帧的操作数栈中

  • 还有,经常说的JVM的解释引擎是基于栈的执行引擎,栈为操作数栈

动态链接(或指向运行时常量池的方法引用)
  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属的方法引用,目的是能够支持当前方法的代码能实现动态链接

  • .java 源文件被编译到字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里。比如:描述一个方法调用另外一个方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转化为调用方法的直接引用

    java代码

    public class DynamicLinkingTest {
        public int num = 0;
    
        public void methodA() {
            System.out.println("methodA()...");
            methodB();
            num++;
        }
    
        public void methodB() {
            System.out.println("methodB()...");
        }
    }
    

    字节码

    public class fageiguanbing.test.DynamicLinkingTest
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #9.#23         // java/lang/Object."<init>":()V
       #2 = Fieldref           #8.#24         // fageiguanbing/test/DynamicLinkingTest.num:I
       #3 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
       #4 = String             #27            // methodA()...
       #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #6 = Methodref          #8.#30         // fageiguanbing/test/DynamicLinkingTest.methodB:()V
       #7 = String             #31            // methodB()...
       #8 = Class              #32            // fageiguanbing/test/DynamicLinkingTest
       #9 = Class              #33            // java/lang/Object
      #10 = Utf8               num
      #11 = Utf8               I
      #12 = Utf8               <init>
      #13 = Utf8               ()V
      #14 = Utf8               Code
      #15 = Utf8               LineNumberTable
      #16 = Utf8               LocalVariableTable
      #17 = Utf8               this
      #18 = Utf8               Lfageiguanbing/test/DynamicLinkingTest;
      #19 = Utf8               methodA
      #20 = Utf8               methodB
      #21 = Utf8               SourceFile
      #22 = Utf8               DynamicLinkingTest.java
      #23 = NameAndType        #12:#13        // "<init>":()V
      #24 = NameAndType        #10:#11        // num:I
      #25 = Class              #34            // java/lang/System
      #26 = NameAndType        #35:#36        // out:Ljava/io/PrintStream;
      #27 = Utf8               methodA()...
      #28 = Class              #37            // java/io/PrintStream
      #29 = NameAndType        #38:#39        // println:(Ljava/lang/String;)V
      #30 = NameAndType        #20:#13        // methodB:()V
      #31 = Utf8               methodB()...
      #32 = Utf8               fageiguanbing/test/DynamicLinkingTest
      #33 = Utf8               java/lang/Object
      #34 = Utf8               java/lang/System
      #35 = Utf8               out
      #36 = Utf8               Ljava/io/PrintStream;
      #37 = Utf8               java/io/PrintStream
      #38 = Utf8               println
      #39 = Utf8               (Ljava/lang/String;)V
    {
      public int num;
        descriptor: I
        flags: ACC_PUBLIC
    
      public fageiguanbing.test.DynamicLinkingTest();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: iconst_0
             6: putfield      #2                  // Field num:I
             9: return
          LineNumberTable:
            line 3: 0
            line 4: 4
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      10     0  this   Lfageiguanbing/test/DynamicLinkingTest;
    
      public void methodA();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #4                  // String methodA()...
             5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: aload_0
             9: invokevirtual #6                  // Method methodB:()V
            12: aload_0
            13: dup
            14: getfield      #2                  // Field num:I
            17: iconst_1
            18: iadd
            19: putfield      #2                  // Field num:I
            22: return
          LineNumberTable:
            line 7: 0
            line 8: 8
            line 9: 12
            line 10: 22
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      23     0  this   Lfageiguanbing/test/DynamicLinkingTest;
    
      public void methodB();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #7                  // String methodB()...
             5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 13: 0
            line 14: 8
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       9     0  this   Lfageiguanbing/test/DynamicLinkingTest;
    }
    

    Constant pool 中的 #数字 即为符号引用

方法返回地址(或方法正常退出或者异常退出的定义)
  • 存放调用该方法的程序计数器的值

  • 一个方法的结束有两种方式:正常执行完毕或者出现未处理异常,非正常退出

    • 正常退出执行返回的字节码指令(return,返回值类型不同指令也不同),会有返回值返回传递给上层调用方法

    • 异常退出:方法在声明了 try catch 之后,字节码会有对应的异常处理表(Exception table),方便在异常发生时找到处理异常(对应的catch)的代码。

      java代码

      public class Test {
          public void m1(){
              try {
                  m2();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      
          private void m2() throws Exception {
              throw new Exception("exception");
          }
      }
      

      部分字节码(省略常量池)

      {
        public fageiguanbing.test.Test();
          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 3: 0
            LocalVariableTable:
              Start  Length  Slot  Name   Signature
                  0       5     0  this   Lfageiguanbing/test/Test;
      
        public void m1();
          descriptor: ()V
          flags: ACC_PUBLIC
          Code:
            stack=1, locals=2, args_size=1
               0: aload_0
               1: invokespecial #2                  // Method m2:()V
               4: goto          12
               7: astore_1
               8: aload_1
               9: invokevirtual #4                  // Method java/lang/Exception.printStackTrace:()V
              12: return
            Exception table:
               from    to  target type
                   0     4     7   Class java/lang/Exception
            LineNumberTable:
              line 6: 0
              line 9: 4
              line 7: 7
              line 8: 8
              line 10: 12
            LocalVariableTable:
              Start  Length  Slot  Name   Signature
                  8       4     1     e   Ljava/lang/Exception;
                  0      13     0  this   Lfageiguanbing/test/Test;
            StackMapTable: number_of_entries = 2
              frame_type = 71 /* same_locals_1_stack_item */
                stack = [ class java/lang/Exception ]
              frame_type = 4 /* same */
      }
      
    • 无论是通过哪种方式退出,在方法退出后都应该返回该方法被调用的位置。方法正常退出时,调用者的程序计数器的值作为方法的返回地址,即调用该方法指令的下一条指令对应的地址。通过异常退出的,返回地址要通过异常表来确定,栈帧一般不会保存这部分信息。

    • 区别:异常退出不会有任何返回值

方法的调用(非常重要)

在JVM中,将符号引用转化为调用方法的直接引用与方法的绑定机制有关。

  • 静态链接:当一个字节码文件被装载到JVM时,如果被调用的方法在编译期可知,且运行期保持不变时。符号引用转换为直接引用的过程是静态链接。

  • 动态链接:被调用的方法在编译期无法确定,只能在程序运行期确定。

对应的方法绑定机制为:早期绑定和晚期绑定。绑定是一个字段,方法或者类的符号引用被替换为直接引用的过程,发生且仅发生一次。

虚方法和非虚方法

非虚方法

  • 编译期确定具体调用,运行时不可变
  • 静态方法,私有方法,final方法,实例构造器,父类方法
  • 其他方法都是虚方法
关于invokedynamic指令

虚拟机中的指令

  • 普通调用指令
    • invokestatic:调用静态方法,解析阶段确定
    • invokespecial:调用方法,私有以及父类方法
    • invokevirtual:调用所有虚方法
    • Invokeinterface:调用接口方法
  • 动态调用指令:动态解析出需要调用的方法,然后执行

invokestatic 和 invokespecial指令调用的方法是非虚方法,其余的(final修饰的除外,final修饰的方法也是invokevirtual调用)方法为虚方法

在java8的lambda表达式出现之后,在java中才能直接生成invokedynamic指令,在java7中主要是为了支持其他动态类型的语言在JVM运行

方法重写的本质
  1. 找到操作数栈顶的第一个元素所执行的对象的实际类型,记做c。
  2. 如果在类型c中找到与常量池中的描述符和名称都相符的方法,则进行全线校验,通过则返回直接引用,不通过抛出java.lang.IllegalAccessError
  3. 否则,按照继承关系从下往上依次对c的各个父类进行第2步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,抛出 java.lang.AbstractMethodError 异常(相当于调用了抽象方法)。

为了提高性能,JVM在类的方法区建立了一个类的虚方法表。每个类中都有一个虚方法表,存放虚方法实际入口。

虚方法表在类加载的链接阶段创建并初始化,类的变量初始值准备完毕之后,JVM会把该类的方法表也初始化。