JVM|类加载机制


前言

很早前就看了很多类加载机制的文章,但都零零碎碎的,此次借着阅读深入理解Java虚拟机一书的契机,归纳一下碎片化的知识。


正文

类加载机制

所谓的“类加载机制”并不单单指“加载”这一过程。我们的类在编译后会形成.class文件,在运行时,JVM先要将.class文件加载进虚拟机内存。这个过程,其实就是虚拟机将文件以一串二进制流的形式读到自己空间中来。

上面表述可能不太正确,Java虚拟机规范并没有指出以哪种方式读取文件,可能是一串二进制流,有可能是其他方式,这个取决于JVM的实现。

具体过程如下,其中验证、准备、解析三个阶段又叫做连接,关于这个连接的理解,我个人觉得就是虚拟机真正访问class文件流里内容的过程。而为了防止class文件中有破坏虚拟机正常执行的指令,所以在加载后,才需要验证这一步骤。怎样验证呢?如文件格式验证:

  1. 检测是否以魔数开头
  2. 主次版本号是否合理
  3. 常量池中是否有不被支持的理性

在这里插入图片描述
又如元数据验证,这个阶段的验证应该是对Java语义的验证:

  1. 这个类是否有父类?
  2. 是否有类继承了final修改的类?

此外,还有字节码验证符号引用验证,其中,字节码验证为了确保没有危害虚拟机安全的指令,比如一个跳转指令是否会其他方法体的字节码上。符号引用验证则验证类自身以外的信息,比如,通过编译存放于常量池用来描述某个类文件的全限定名符号,来判断是否可以正常访问到该类。符号引用验证是为了保证解后边的解析阶段的正常执行。

验证之后是准备阶段,在这个阶段JVM为(static修饰的)变量分配内存初始值,注意这里不包括实例变量,实例变量会随着对象的初始化,一起在堆中分配内存。另外,这里的初始化应该也是属于半初始化阶段,例如下面代码,此时这个阶段是int的默认0值而不是100,关于100的赋值指令,在编译后,存放到构造方法内了,需要在初始化阶段才会真正的赋值:

private static int a = 100;

关于private static final int a = 100;则有所不同,final修饰的变量,在编译阶段,会将a字段属性会被表示为ConstantValue这样,在准备阶段a就能被赋值为100了,这也恰好可以解释如下代码。

byte a = 12;
byte b = 6;
byte c = a + b;(编译报错)
byte c = (byte)(a + b)(编译通过)

加了final 修饰的a、b在编译阶段就正常赋值了,此时可以直接得出18,即使相加运算会转换成默默向上装int类型,但18属于byte的范围,所以byte c = a + b此时不需要强转也不会报错。

 final byte a = 12;
 final byte b = 6;
byte c = a + b;(编译通过)

这里截取部分字节码做对比,不加final:

扫描二维码关注公众号,回复: 12665109 查看本文章
public static void main(java.lang.String[]);
    Code:
       0: bipush        12
       2: istore_1
       3: bipush        6
       5: istore_2

加了final之后

  public static void main(java.lang.String[]);
    Code:
       0: bipush        18
       2: istore_3
       3: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: iload_3
       7: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
      10: return
}

准备之后是解析阶段了,在这个阶段符号引用为被替代为直接引用,所谓的符号引用,个人理解为引用某个目标(可能会使用的意思)的符号,这个目标这个阶段只代表可能使用,此时可能还没有在内存中,而将符号引用转为直接引用,可以表示目标一定存在于内存中了。

注意,解析的运行时机不是确定的,它可能也在初始化之后才发生,譬如通过反射进行运行时的动态绑定。

最后是,初始化阶段,在上面有提到过“半初始化”,那时的静态变量a的初始值并不是我们预先设置的100,而是0,这个阶段的初始化,则会将100赋值给a。这个阶段会真正执行我们编译后的字节码,注意,这里的初始化应该还是指的类层次的初始化,或者说<cinit>的初始化过程。而<init>或者普通实例对象的内存分配,应该是在使用时才初始化和分配的。

cinit为静态类构造函数,编译器自动帮我们生成的,init为普通构造函数的指令。

类加载时机

何时会进行类加载机制的第一步“加载”呢?java虚拟机规范指出的我们主动引用一个类的时候,一定会加载的,其他情况并没有强制固定。

这个主动引用,大致分为如下情况:

  1. new一个类,还有遇到getstatic(获取静态字段)、putstatic(设置静态字段)、invokestatic(执行静态字段)字节码指令时。
  2. 反射调用
  3. 加载子类时,父类如果没有加载,会先进行加载
  4. 用户指定一个要执行的主类时
  5. 第五点需要配合实际代码才好理解,如下。

JDK1.7引入了MethodHandle类,这个Method Handles的引入和java.lang.reflect API相配合。当我们解析后得到的是一个静态的方法时,如果该方法对应的类还没初始化,此时会进行初始化。

  // 获取方法类型 
  // 参数为:1.返回值类型,2方法中参数类型
MethodType mt = MethodType.methodType(String.class, String.class);
   MethodHandle mh = null;
   try {
    
    
      mh = MethodHandles.lookup().findVirtual(MHTest.class, "toString", mt);  //查找方法句柄
        } catch (NoSuchMethodException | IllegalAccessException e) {
    
    
            e.printStackTrace();
        }
        return mh;

附(几个字节码的含义)

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/legendaryhaha/article/details/106455061