Java类加载过程

时间:2019-12-11 01:05:40   收藏:0   阅读:133

类加载过程已经是老生常谈模式了,我曾在讲解tomcat的书中、在Java基础类书、JVM的书、在Spring框架类的书中、以及各种各样的博客和推文中见过,我虽然看了又忘了,但总体还是有些了解,曾经自以为这不是什么大不了的过程。但时间总会教你做人,看得越多,越觉得以前理解不足。

此笔记记录,虚拟机中Java类加载的过程,各个过程发生的时机,具体做了什么事情,例如,在方法区或者堆分配了哪些内存,创建了哪些常量等。由于Java文件会预先编译,得到class文件,虚拟机的类加载,是对class文件进行的操作,所以不可避免的涉及到class文件的解读,只有知道class文件中有什么,虚拟机才能加载对应的内容。

一、class文件介绍

? 不可能完全解读class文件,《虚拟机规范第二版》花了一百多页写class文件,这是class的核心,如果要完全理解,可能还得去复习复习编译原理,词法分析语法分析代码生成之类的。

1.1 文件结构

文件结构定义:u1 = 1个字节,u2 = 2个字节,u4 = 4个字节,u8 = 8个字节;

ClassFile {
  u4                     magic;                                // 魔法数
  u2                     minor_version;                        // 副版本号
  u2                     major_version;                                // 主版本号
  u2                     constant_pool_count;                  // 常量池计数,从1开始计数
  cp_info                constant_pool[constant_pool_count-1]; // 常量池[常量数量]
  u2                                         access_flags;                                           // 访问标志
  u2                                         this_class;                                                     // 类索引
  u2                                         super_class;                                                    // 父类索引
  u2                                         interfaces_count;                                       // 接口计数器
  u2                     interfaces[interfaces_count]          // 接口表
  u2                                         fields_count;                                               // 字段计数器
  field_info             fields[fields_count];                               // 字段表
  u2                     methods_count;                                              // 方法计数器
  method_info            methods[methods_count];                             // 方法表
  u2                     attributes_count;                                       // 属性计数器
  attribute_info         attributes[attributes_count];               // 属性表
}

根据这个表,一个class文件的16进制文件就可以读取了。此处要注意几点

1.2 简单示例读取class文件

代码

/**
 * @Author: dhcao
 * @Version: 1.0
 */
public class ClassTest {

    public static final String abc = "ccc";

    private static String def = "fff";

    public String getAbcdef(){
      return abc + def;
    }
}

编译为ClassTest.class

直接使用subline或者其他软件打开此二进制文件
技术分享图片

解读:根据class文件结构解读

JDK版本号 Class版本号 10进制 16进制
1.1 45.0 00 00 00 2D
1.2 46.0 00 00 00 2E
1.3 47.0 00 00 00 2F
1.4 48.0 00 00 00 30
1.5 49.0 00 00 00 31
1.6 50.0 00 00 00 32
1.7 51.0 00 00 00 33
1.8 52.0 00 00 00 34

反编译该class文件javap -verbose ClassTest

第一部分:常量池部分

Constant pool:
   #1 = Methodref          #10.#27        // java/lang/Object."<init>":()V
   #2 = Class              #28            // java/lang/StringBuilder
   #3 = Methodref          #2.#27         // java/lang/StringBuilder."<init>":()V
   #4 = Class              #29            // org/relax/jvm/demo/ls/ClassTest
   #5 = String             #30            // ccc
   #6 = Methodref          #2.#31         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Fieldref           #4.#32         // org/relax/jvm/demo/ls/ClassTest.def:Ljava/lang/String;
   #8 = Methodref          #2.#33         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = String             #34            // fff
  #10 = Class              #35            // java/lang/Object
  #11 = Utf8               abc
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               ConstantValue
  #14 = Utf8               def
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lorg/relax/jvm/demo/ls/ClassTest;
  #22 = Utf8               getAbcdef
  #23 = Utf8               ()Ljava/lang/String;
  #24 = Utf8               <clinit>
  #25 = Utf8               SourceFile
  #26 = Utf8               ClassTest.java
  #27 = NameAndType        #15:#16        // "<init>":()V
  #28 = Utf8               java/lang/StringBuilder
  #29 = Utf8               org/relax/jvm/demo/ls/ClassTest
  #30 = Utf8               ccc
  #31 = NameAndType        #36:#37        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #32 = NameAndType        #14:#12        // def:Ljava/lang/String;
  #33 = NameAndType        #38:#23        // toString:()Ljava/lang/String;
  #34 = Utf8               fff
  #35 = Utf8               java/lang/Object
  #36 = Utf8               append
  #37 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #38 = Utf8               toString

二、类加载步骤

类文件(class文件)是Java编译器编译之后的结果,它遵循的是编译原理。类加载,是指JVM将class文件加载到虚拟机的过程。只有将class文件加载到虚拟机,才能够使用该class。

2.1 类加载过程

技术分享图片

将一个class文件(不管是文件还是二进制...只要是class格式)记载到虚拟机,最后移出虚拟机,通常认为有以上步骤,即类等声明周期。对于大多数时候,我们并不关注卸载过程,将“Using、使用”之前对类等处理,统称为“类加载”。所以也有描述类加载为3个主要过程(这也是虚拟机规范定义的过程):加载 --- 连接 --- 初始化

2.1.1 Loading、加载

2.1.2 Verifition、验证

? 在读入了二进制流之后,验证就开始了,验证的目的是保证Class文件的字节流包含的信息符合JVM的要求,并且不会危害JVM的安全。

? 那么需要验证哪些呢,在《虚拟机规范 Java SE 8》中,章节目录4.10详细讲解了JVM加载class文件需要进行的校验,根本目的还是保证class文件的正确性和安全性。

2.1.3 Preparation、准备

? 该阶段是非常重要的,在经过前面的阶段之后,一个Class文件已经加载到了JVM并验证了其正确性,那么接下来就需要对Class文件进行处理。

? 虚拟机规范规定:准备阶段的任务是创建类或者接口的静态字段,并用默认值初始化这些字段。这个阶段不会执行任何的虚拟机字节码指令。

? 过程

从开始接触Java我们就一直被一些看似简单实际有些意思的题目烦扰,例如:静态变量,静态块的执行顺序、父类子类的执行顺序、变量赋值时间、方法传递的是引用还是值等等乱七八糟的问题。

关于初始值:public static int value = 123; 在准备阶段,这段代码只会得到:value = 0,这是因为int型变量的初始值为0(引用类型初始值为null)。

但是:public static final int value = 123; 在准备阶段,这段代码会得到:value = 123,这是因为final定义常量,其值在编译时确定。

尝试分析,如何标记常量,以及为它赋值。依然是最上述的代码段

反编译该class文件javap -verbose ClassTest

第二部分:编译码

{
  public static final java.lang.String abc;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String ccc

  public org.relax.jvm.demo.ls.ClassTest();
    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 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/relax/jvm/demo/ls/ClassTest;
  
  ....
    .....
    ......
}

以上,为javap的反编译结果第二部分。我们看源代码中:

  public static final String abc = "ccc";

编译之后:
    public static final java.lang.String abc;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String ccc

重点在于ConstantValue: String ccc。

属性ConstantValue:如果同时使用了static 和 final,javac编译器在编译时就在字段上著明该属性,并在类加载的准备阶段,将该属性的值,自动赋值给静态变量。

在最开始定义class格式的时候写到,文件最后定义的是属性表,ConstantValue就是属性表中的属性。

  • 作用范围:使用在字段上。
  • 如果flags中含有ACC_STATIC和ACC_FINAL,那么ConstantValue的值将直接赋值给该字段。也就是ccc直接赋值给abc。
  • 强调:虚拟机规范只要求有ACC_STATIC就可以使用ConstantValue,是sun公司的javac编译器要求同时使用ACC_STATIC和ACC_FINAL
  • 只能限于基本类型和String使用

2.1.4 Resolution、解析

? 解析这个过程,并没有严格的规定在什么时候发生。解析的作用是将符号引用替换为直接引用的过程,只需要在使用符号之前替换这个符号就行。

? 以上描述还有些难以理解。说实话,我也不知道怎么解释了,举个例子描述(类方法解析):

 public String getAbcdef(){

        int a = 3;
        int c = a + 4;
        return abc + def + c;
    }

执行 javap -verbose ClassTest
  
  --------------------------------------------------------------------------------
  public java.lang.String getAbcdef();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_3
         1: istore_1
         2: iload_1
         3: iconst_4
         4: iadd
         5: istore_2
         6: new           #2                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
        13: ldc           #5                  // String ccc
        15: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        18: getstatic     #7                  // Field def:Ljava/lang/String;
        21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: iload_2
        25: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        28: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        31: areturn
      LineNumberTable:
        line 15: 0
        line 16: 2
        line 17: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  this   Lorg/relax/jvm/demo/ls/ClassTest;
            2      30     1     a   I
            6      26     2     c   I
    --------------------------------------------------------------------------------

在反编译中,一直出现的jvm指令:invokevirtual

解析的目的:

? 在上文 中,该指令调用的是StringBuilder的append方法(直接字符串相加),虚拟机要求,在执行该条指令(invokevirtual)之前,需要对他们所使用的符号进行解析,也就是对StringBuilder.append进行解析。解析的结果是,该指令能够正确的找到该方法的入口!

解析过程:(类方法解析)

  #6 = Methodref          #2.#31         // java/lang/StringBuilder.append:

? 它是一个Methodref(方法常量),这是由#2(java/lang/StringBuilder)和#31(append)组成的。

? 而#2:

#2 = Class              #28            // java/lang/StringBuilder

? 它是一个Class(类)。

解析的目的是将符号引用转为能用的直接引用。主要包含以下:

2.1.5 Initializaition、初始化

? Loading、加载阶段读入了Class文件

? Verifition、验证阶段校验了其正确性

? Preparation、准备阶段为其开辟了内存空间,并为static属性赋初始值

? Resolution、解析阶段将符号引用都转为了直接引用,使得Class中的定义都有了实际意义,不再是一串字面量

? Initializaition、初始化阶段,是类加载的最后一步

也是执行字节码的过程,也是执行<clinit>()方法的过程

三、总结

? 类的加载过程,主要流程如上,但是更多的细节,没有描述,例如更多的Class文件细节,更多的类加载的内容,更具体的栈与堆的数据结构和分配过程。在后面的笔记中将对这些进行补充。

熟悉的面试题,现在看来也显然易见!

class Parent {
   static {
       System.out.println("父类静态块");
   }

   Parent(){
       System.out.println("父类构造函数");
   }
}

class Sub extends Parent{
    static {
        System.out.println("子类静态块");
    }

    Sub(){
        System.out.println("子类构造函数");
    }
}

// 如何输出...
class Test{
    public static void main(String[] args) {
        new Sub();
    }
}

原文:https://www.cnblogs.com/dhcao/p/12019859.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!