加载

JVM 是虚拟机的一种,它的指令集语言是字节码,字节码构成的文件是 class 文件。平常我们写的 Java 文件,需要编译为 class 文件才能交给 JVM 运行。可以这么说:C 语言代码——>二进制文件——>计算机硬件,就相当于 Java 代码——>字节码文件——>JVM。JVM 将指定的 class 文件读取到内存里,并运行该 class 文件里的 Java 程序的过程,就称之为类的加载;反之,将某个 class 文件的运行时数据从 JVM 中移除的过程,就称之为类的卸载。

class 文件的运行时数据就是 C++对象,也称为 kclass 对象,这些运行时数据在 JDK7 之前是放在永久代(PermGen),JDK8 之后则放在元空间(Metaspace)。

类加载器

类的加载是需要类加载器完成的,但是类加载器在 JVM 中的作用可不止这些。在 JVM 中,一个类的唯一性是需要这个类本身和类加载一起才能确定的,每个类加载器都有一个独立的命名空间。

类加载器在 JVM 中的作用有:

  • 将类的字节码文件从 JVM 外部加载到内存中
  • 确定一个类的唯一性
  • 提供隔离特性,为中间件开发者提供便利,例如 Tomcat

类的唯一性

不同的类加载器,即使是同一个类字节码文件,最后再 JVM 里的类对象也不是同一个,下面的代码展示了这个结论:

package jvm;
import java.io.InputStream;
import java.io.IOException;
public class ClassLoaderTest {
  public static void main(String[] args) {
    ClassLoader myLoader = new ClassLoader() {
      @Override
      public Class<?> loadClass(String name)
        throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
        InputStream inputStream = getClass().getResourceAsStream(fileName);
        if (inputStream == null) {
          return super.loadClass(name);
        }
        try {
          byte[] b = new byte[inputStream.available()];
          inputStream.read(b);
          return defineClass(name, b, 0, b.length);
        } catch (IOException e) {
          throw new ClassNotFoundException();
        }
      }
    };
    Object obj = myLoader.loadClass("jvm.ClassLoaderTest").newInstance();
    System.out.println(obj.getClass()); // class jvm.ClassLoaderTest
    System.out.println(obj instanceof jvm.ClassLoaderTest); // false
    ClassLoaderTest classLoaderTest = new ClassLoaderTest();
    System.out.println(classLoaderTest.getClass()); // class jvm.ClassLoaderTest
    System.out.printLn(classLoaderTest instanceof jvm.ClassLoaderTest); //true
  }
}Copy to clipboardErrorCopied

可以看出,代码中使用自定义类加载器(myLoader)加载的 jvm.ClassLoaderTest 类和通过应用程序类加载器加载的类不是同一个类。

加载流程

Java 类从被虚拟机加载开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段;其中验证、准备和解析又统称为连接(Linking)阶段。

类的生命周期

类文件结构

一个编译后的类文件包含下面的结构:

ClassFile {
    u4            magic;
    u2            minor_version;
    u2            major_version;
    u2            constant_pool_count;
    cp_info        contant_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];
}Copy to clipboardErrorCopied
magic, minor_version, major_version 类文件的版本信息和用于编译这个类的 JDK 版本
constant_pool 类似于符号表,尽管它包含更多数据。下面有更多的详细描述,
access_flags 提供这个类的描述符列表,
this_class 提供这个类全名的常量池(constant_pool)索引,比如 org/jamesdbloom/foo/Bar,
super_class 提供这个类的父类符号引用的常量池索引,
interfaces 指向常量池的索引数组,提供那些被实现的接口的符号引用,
fields 提供每个字段完整描述的常量池索引数组,
methods 指向 constant_pool 的索引数组,用于表示每个方法签名的完整描述。如果这个方法不是抽象方法也不是 native 方法,那么就会显示这个函数的字节码
attributes 不同值的数组,表示这个类的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解,

可以用 javap 查看编译后的 Java class 文件字节码。如果你编译下面这个简单的类:

package org.jvminternals;
public class SimpleClass {
    public void sayHello() {
        System.out.println("Hello");
    }
}Copy to clipboardErrorCopied

运行下面的命令,就可以得到下面的结果输出: javap -v -p -s -sysinfo -constants classes/org/jvminternals/SimpleClass.class。

public class org.jvminternals.SimpleClass
  SourceFile: "SimpleClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            //  "Hello"
   #4 = Methodref          #21.#22        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #23            //  org/jvminternals/SimpleClass
   #6 = Class              #24            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/jvminternals/SimpleClass;
  #14 = Utf8               sayHello
  #15 = Utf8               SourceFile
  #16 = Utf8               SimpleClass.java
  #17 = NameAndType        #7:#8          //  "<init>":()V
  #18 = Class              #25            //  java/lang/System
  #19 = NameAndType        #26:#27        //  out:Ljava/io/PrintStream;
  #20 = Utf8               Hello
  #21 = Class              #28            //  java/io/PrintStream
  #22 = NameAndType        #29:#30        //  println:(Ljava/lang/String;)V
  #23 = Utf8               org/jvminternals/SimpleClass
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/System
  #26 = Utf8               out
  #27 = Utf8               Ljava/io/PrintStream;
  #28 = Utf8               java/io/PrintStream
  #29 = Utf8               println
  #30 = Utf8               (Ljava/lang/String;)V
{
  public org.jvminternals.SimpleClass();
    Signature: ()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   Lorg/jvminternals/SimpleClass;
  public void sayHello();
    Signature: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
        0: getstatic      #2    // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc            #3    // String "Hello"
        5: invokevirtual  #4    // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      9      0    this   Lorg/jvminternals/SimpleClass;
}Copy to clipboardErrorCopied

这个 class 文件展示了三个主要部分:常量池、构造器方法和 sayHello 方法。

  • 常量池: 提供了通常由符号表提供的相同信息,详细描述见下文。
  • 方法: 每一个方法包含四个区域,签名和访问标签字节码 LineNumberTable:为调试器提供源码中的每一行对应的字节码信息。上面的例子中,Java 源码里的第 6 行与 sayHello 函数字节码序号 0 相关,第 7 行与字节码序号 8 相关。LocalVariableTable:列出了所有栈帧中的局部变量。上面两个例子中,唯一的局部变量就是 this。

这个 class 文件用到下面这些字节码操作符:

操作符 说明
aload0 这个操作码是 aload 格式操作码中的一个。它们用来把对象引用加载到操作码栈。表示正在被访问的局部变量数组的位置,但只能是 0、1、2、3 中的一个。还有一些其它类似的操作码用来载入非对象引用的数据,如 iload, lload, float 和 dload。其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double。局部变量数组位置大于 3 的局部变量可以用 iload, lload, float, dload 和 aload 载入。这些操作码都只需要一个操作数,即数组中的位置
ldc 这个操作码用来将常量从运行时常量池压栈到操作数栈
getstatic 这个操作码用来把一个静态变量从运行时常量池的静态变量列表中压栈到操作数栈
invokespecial, invokevirtual 这些操作码属于一组函数调用的操作码,包括:invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual。在这个 class 文件中,invokespecial 和 invokevirutal 两个指令都用到了,两者的区别是,invokevirutal 指令调用一个对象的实例方法,invokespecial 指令调用实例初始化方法、私有方法、父类方法,
return 这个操作码属于 ireturn、lreturn、freturn、dreturn、areturn 和 return 操作码组。每个操作码返回一种类型的返回值,其中 i 表示 int,l 表示 long,f 表示 float,d 表示 double,a 表示 对象引用。没有前缀类型字母的 return 表示返回 void

跟任何典型的字节码一样,操作数与局部变量、操作数栈、运行时常量池的主要交互如下所示。构造器函数包含两个指令。首先,this 变量被压栈到操作数栈,然后父类的构造器函数被调用,而这个构造器会消费 this,之后 this 被弹出操作数栈。

sayHello() 方法更加复杂,正如之前解释的那样,因为它需要用运行时常量池中的指向符号引用的真实引用。第一个操作码 getstatic 从 System 类中将 out 静态变量压到操作数栈。下一个操作码 ldc 把字符串 “Hello” 压栈到操作数栈。最后 invokevirtual 操作符会调用 System.out 变量的 println 方法,从操作数栈作弹出”Hello” 变量作为 println 的一个参数,并在当前线程开辟一个新栈帧。