solitaryclown

Class文件分析与JVM的类加载

2022-01-25
solitaryclown

1. class文件结构

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    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];
}

2. 分析class文件

JDK提供了javap程序帮助我们“翻译”class文件,以更可读的方式将class结构打印出来。

2.1. <clinit>()V

对于class的static变量赋值和static代码块,JVM编译时会将这些代码组合成一个方法,在类加载的初始阶段执行这个方法。

2.2. <init>()V

对于class中的变量声明、普通代码块和构造方法,JVM编译时会将这些代码组合成一个方法,且不管源代码的顺序如何,构造方法都会被放在方法最后。在构造对象实例时调用。

2.3. 多态原理

2.4. 异常捕获(try-catch-finally)

在class文件中会有一个 Exception table,字节码执行时根据捕获的异常决定跳到哪一行代码执行。

2.4.1. finally原理

finally内部的代码会被复制若干份放到try和多个catch块的后面。 例子:

public class TestTryCatch {
    public static void main(String[] args) {
        int a=0;
        try {
            a=10;
        }catch (Exception e){
            a=20;
        }finally {
            a=30;
        }
    }
}

字节码:

Classfile /C:/Users/Administrator/Desktop/test_java_code/TestTryCatch.class
  Last modified 2022-1-26; size 481 bytes
  MD5 checksum ed3a4463d3f8be8cec1502fe277883dc
  Compiled from "TestTryCatch.java"
public class TestTryCatch
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // java/lang/Exception
   #3 = Class              #19            // TestTryCatch
   #4 = Class              #20            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               StackMapTable
  #12 = Class              #21            // "[Ljava/lang/String;"
  #13 = Class              #18            // java/lang/Exception
  #14 = Class              #22            // java/lang/Throwable
  #15 = Utf8               SourceFile
  #16 = Utf8               TestTryCatch.java
  #17 = NameAndType        #5:#6          // "<init>":()V
  #18 = Utf8               java/lang/Exception
  #19 = Utf8               TestTryCatch
  #20 = Utf8               java/lang/Object
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               java/lang/Throwable
{
  public TestTryCatch();
    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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: bipush        30
         7: istore_1
         8: goto          27
        11: astore_2
        12: bipush        20
        14: istore_1
        15: bipush        30
        17: istore_1
        18: goto          27
        21: astore_3
        22: bipush        30
        24: istore_1
        25: aload_3
        26: athrow
        27: return
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any
            11    15    21   any
      LineNumberTable:
        line 5: 0
        line 7: 2
        line 11: 5
        line 12: 8
        line 8: 11
        line 9: 12
        line 11: 15
        line 12: 18
        line 11: 21
        line 12: 25
        line 13: 27
      StackMapTable: number_of_entries = 3
        frame_type = 255 /* full_frame */
          offset_delta = 11
          locals = [ class "[Ljava/lang/String;", int ]
          stack = [ class java/lang/Exception ]
        frame_type = 73 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 5 /* same */
}
SourceFile: "TestTryCatch.java"

main()方法的字节码中,有三段bipush 30;istore_1代码,这就是源代码finally块中i=30的字节码。

2.5. try-finally返回值问题

  • try块里面有return而finally没有

    try块在将返回值返回之前会暂存到一个局部变量,然后执行finally中的代码,最终返回暂存的返回值,因此如果finally对try里面的返回值做了修改,是无效的,对于基本数据类型,返回值不会变,对于引用数据类型,引用的值不会变。 7OG2Yq.md.png 7OGRf0.md.png

  • try和finally都有return语句

    1. try中的return不会执行,只会执行finally中的return
    2. 如果try发生异常且没有catch语句,finally如果有return不会有异常被抛出

2.6. synchronized在字节码中的体现

JVM指令monitorentermoniterexit实现加锁、解锁。 当synchronized代码块发生异常,根据异常控制表,会执行moniterexit用来释放锁。

public class TestSynchronized {
    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock){
            lock.toString();
        }
    }
}

字节码:
7OUBZD.md.png

2.7. Java语法糖

2.7.1. 默认无参构造

如果一个class没有定义任何构造方法,JVM会默认给一个无参构造。

2.7.2. 自动拆装箱

对于普通数据类型的包装类型,JVM编译时会生成拆箱、装箱的字节码。

2.7.3. 泛型(Generics)

在许多的Java类中都使用了泛型机制,常用的比如List、Map等集合类,泛型作为一种语言风格,允许在强类型语言中编写代码时使用一些以后才指定的类型,在实例化时将类型作为参数。
在Java中,泛型将运行期遇到的问题转移到了编译期,也省去了在源代码中进行类型强转的麻烦。
比如:

   List list=new ArrayList();
   list.add(new Student())
   Student stu=(Student)list.get(0);

上面的代码没有使用泛型,每次从集合中取出元素都要手动进行强制类型转换。 如果使用泛型,不需要手动强转,且如果使用了与泛型类型无法转换的类型,编译器进行类型检查时会报错。

type erasure原理

  1. Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods. 1. Insert type casts if necessary to preserve type safety. 2. Generate bridge methods to preserve polymorphism in extended generic types.
   public class TestGeneric {
      public static void main(String[] args) {
          List<String> list= new ArrayList<String>();
          list.add("hello");
          String s = list.get(0);
      }
   }

编译后:
泛型擦除

参考:https://docs.oracle.com/javase/tutorial/java/generics/genTypes.html

2.7.4. switch参数为String和enum

2.7.5. try-with-resources

Java7提供try-with-resources语法,形如

try(创建资源){
  ...
}catch(){
  ...
}

使用上面形式的try-catch,只要资源类实现了AutoCloseable接口,就可以实现资源自动关闭。编译时JVM会在字节码中生成finally代码块,在其中实现资源释放。

2.8. defining loader和initiating loader

对于一个class或者interface C,对于一个classloader L,如果L直接创建C,则L是C的defining loader;如果L直接创建C或者通过委托创建C,L是C的initiating loader

在运行时,一个class或者interface不是仅仅由它的名字决定,而是由一个组合决定:二进制名字和它的defining loader

3. 类加载阶段

3.1. 第一阶段:加载

这一阶段的工作是将二进制字节码载入方法区,方法区内部采用C++写的一个类instanceKlass描述某个Java类的元数据。 它的重要属性包括:

  • _java_mirror:指向某个类在堆中的Class对象
  • _class_loader:加载这个类的类加载器
  • _super:父类
  • _fields:成员变量
  • _methods:方法
  • _constants:常量池
  • _vtable:虚方法表

注意:

  1. 如果某个类有父类,先加载父类
  2. 加载和连接可能是交替执行的。

3.2. 第二阶段:连接

3.2.1. 验证

验证字节码格式是否符合规范,比如魔术、版本号等等。

3.2.2. 准备

为static变量分配内存空间,设置默认值。 注意:
分配内存空间和赋值是两个分开的操作,分配在准备阶段,赋值在类的初始化阶段。

  • 如果static变量有final修饰
    • 基本数据类型或者String常量,那么赋值在准备阶段就可完成,不必等到初始化阶段完成。
    • 引用数据类型,那么赋值必须在类的初始化阶段完成。

      3.2.3. 解析

      将常量池中的符号引用解析为直接引用

3.3. 初始化

JVM调用<clinit>()V。 类的初始化是懒惰的:

  • main方法所在的类会首先被初始化
  • 首次访问某个类的static变量或static方法,会对这个类初始化
  • 子类初始化时如果父类还没有初始化,父类初始化会被触发
  • Class.forName()会进行初始化
  • new创建实例会进行初始化

不会触发初始化的情况:

  • 访问static final属性(基本类型和字符串)
  • 类.class不会触发初始化
  • 创建某个类的数组不会初始化
  • ClassLoader的loadClass()方法不会初始化
  • Class.forName()的参数boolean initialize为false

4. 类加载器

JVM规范规定有2种类加载器:

  1. bootstrap class loader
  2. user-defined class loader 每个用户user-defined class loader都是抽象类ClassLoader的子类的一个实例。

但在jdk中,定义了三种类加载器:

  • Bootstrap class loader
  • Ext class loader
  • App Class loader

  • Bootstrap class loader是C++语言实现的。
  • Ext和App类加载器是ClassLoader的子类(不是直接子类),根据JVM规范属于user-defined类加载器。 Ext和App是sun.misc.Launcher类中的内部类,层次关系如下:
    Class Loader

4.1. 双亲委派

所谓的双亲委派是指不同的类加载器存在parent-son的关系,并不是extends的关系,在ClassLoader抽象类中有一个parent来维护这种关系。

类加载:

  1. 检查该类是否已经加载
  2. 如果类还未被加载,尝试让上级加载这个类
  3. 如果上级为null,让bootstrap加载器加载这个类。

4.2. 自定义类加载器

场景:

  • 需要加载非classpath路径中的类
  • 希望通过接口实现解耦
  • 同名类希望都可以加载并隔离(tomcat不同每个webapp都可能有同名类,但不会冲突。)

4.2.1. 步骤

  1. 自定义类加载器类extends ClassLoader
  2. 重写findClass方法(如果不需要遵守双亲委派机制,重写loadClass方法)
  3. 读取类的字节码
  4. 调用defineClass()加载类 在使用自定义类加载器时,调用loadClass()方法实现类加载。

下一篇 Java泛型

Comments

Content