JVM 技术内幕——HotSpot VM 类加载机制

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。实现语言无关性的基础是 VM 和字节码存储格式,JVM 不和包括 Java 在内的任何语言绑定,它只与 “Class 文件” 这种特定的二进制文件格式所关联,Class 文件包含了 JVM 指令集和符号表以及若干其他辅助信息。Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的。

HotSpot VM 运行时数据区

查看字节码命令:javap -verbose ClassName.class

1、一次编译,处处运行如何实现?

Java 源码首先被编译成字节码,再由不同平台的 JVM 进行解析,转换成具体平台上的机器指令。

2、为什么 JVM 不直接将源码解析成机器码去执行?

直接将源码解析成机器码,每次执行都需要各种检查,影响性能,所以这里引入了中间字节码,多次执行程序是不需要各种校验和补全的。同时也可以将别的语言解析成字节码,增加平台的兼容扩展能力。

3、JVM 如何加载 .class 文件?

JVM 的 Class Loader 依据特定格式,加载 .class 文件到内存,然后 Execution Engine(执行引擎) 对命令进行解析,解析完成就提交到操作系统中去执行了。

1.ClassLoader

类从编译到执行的过程:

  • 编译器将 Person.java 编译成 Person.class 字节码文件;
  • ClassLoader 将字节码转换为 JVM 中的 Class 对象;
  • JVM 利用 Class 对象实例化为 Person 对象。

什么是 ClassLoader?

所有的 Class 都是由 ClassLoader 进行加载的,ClassLoader 获取 Class 文件里的二进制数据流装载进系统,然后交给 JVM 进行连接、初始化等操作。

通过看 ClassLoader 源码得知,ClassLoader 是一个抽象类,ClassLoader 提供了一些重要的接口:

private final ClassLoader parent;
// 加载类
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 避免多个线程调用同一个 ClassLoader 加载同一个类
    synchronized (getClassLoadingLock(name)) {
        // 检查类是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) { // 没有加载过的逻辑
            long t0 = System.nanoTime();
            try {
            	// 自定义ClassLoader的parent是AppClassLoader,AppClassLoader的parent是ExtClassLoader
            	// ExtClassLoader的parent是null, 因为BootstrapClassLoader是C++写的, 获取不到
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else { // BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) { resolveClass(c); }
        return c;
    }
}
private Class<?> findBootstrapClassOrNull(String name) {
    if (!checkName(name)) return null;
    return findBootstrapClass(name);
}
private native Class<?> findBootstrapClass(String name);

ClassLoader 的种类:

BootstrapClassLoader:C++ 编写,加载核心库 java.*
ExtClassLoader:Java 编写,加载扩展库 javax.*
AppClassLoader:Java 编写,加载程序所在目录
自定义ClassLoader:Java 编写,定制化加载

ClassLoader 的双亲委派机制:

检查类是否已经加载的顺序:

  1. 自定义ClassLoader;
  2. AppClassLoader;
  3. ExtClassLoader;
  4. BootstrapClassLoader。

尝试加载类的顺序正好是反的:

  1. Load jre\lib\rt.jar 或者 -Xbootclasspath选项指定的jar包;
  2. Load jre\lib\ext*.jar 或者 -Djava.ext.dirs 指定目录下的 jar 包;
  3. Load classpath 或者 -Djava.class.path 所指定的目录下的类和 jar 包;
  4. 通过 java.lang.ClassLoader 的子类自定义加载 class。

为什么要使用双亲委派机制去加载类?

  • 避免多份同样字节码的加载,内存是宝贵的,没必要保存两份相同的 Class 对象;

类的加载方式:

隐式加载:new
显式加载:ClassLoader 的 loadClass() 方法、Class 的 forName() 方法 等

ClassLoader cl = Person.class.getClassLoader();
Class cls = Class.forName("com.example.javabasic.Person");

loadClass 和 forName 的区别?

Class 的 forName() 得到的 class 是已经初始化完成的。
ClassLoader 的 loadClass() 得到的 class 是还没有链接的。

应用案例:

使用 Class.forName(“com.mysql.jdbc.Driver”) 创建数据库驱动,Driver 类中是有一段静态的代码段的,因此需要调用 forName 才能执行它,进而生成 Driver 对象。

Spring IOC 为了加快初始化速度,大量使用类延时加载技术,使用 ClassLoader 不需要执行链接和初始化步骤,这样可以加快加载速度,把类的初始化工作留到实际使用到这个类的时候才去做。

类的装载过程:

1、加载

通过 ClassLoader 的 loadClass() 方法把 class 文件字节码加载到内存中,并将这些静态数据转换成运行时数据区方法区的类型数据,在运行时,在数据区堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区类数据的访问入口。

2、链接,分为校验、准备、解析三步:

校验:检查加载的 class 的正确性和安全性;
准备:为类变量分配存储空间并设置类变量初始值;
解析:JVM 将常量池内的符号引用转换为直接引用。

3、初始化

执行类变量赋值和静态代码块。

1.类加载机制

Class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件之中,中间没有任何分隔符,这使得 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用 8 个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个 8 位字节进行存储。

在 Class 文件中描述的各种信息,最终都是需要加载到 JVM 中之后才能运行和使用。

JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型,这就是 JVM 的类加载机制。

类的生命周期:加载 > 验证 > 准备 > 解析 > 初始化 > 使用 > 卸载

加载、验证、准备、解析、卸载这 5 个的顺序是确定的,而解析则不一定,在某些情况是 初始化 > 解析,这是为了支持 Java 的运行时绑定。

类加载过程包括加载、验证、准备、解析、初始化 5 个阶段。VM 设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到 JVM 外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块叫做"类加载器"。

1.加载

  1. 通过一个类的全限定名来获取定义此类的二进制流;
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

2.验证

如果验证到输入的字节流不符合 Class 文件格式的约束,VM 就应抛出一个 java.lang.VerifyError 异常或其子类异常。验证阶段大致会有文件格式验证、元数据验证、字节码验证、符号引用验证 4 个阶段。

3.准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产品混淆的概念需要强调一下:

  1. 这时候进行内存分配的仅包含类变量(被 static 修饰的变量),而不包含实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中;
  2. 这里所说的初始值"通常情况"下是数据类型的零值,假设一个类变量的定义为 public static int value = 123; 那变量 value 在准备阶段过后的初始值是0而不是123,因为这个时候尚未开始执行任何 Java 方法,value 赋值为 123 的动作将在初始化阶段才会执行。 还有一种情况是定义为 public static final int value = 123; 这种情况再准备阶段 VM 就会将 value 赋值为 123。

4.解析

解析过程是 VM 将常量池中的符号引用替换为直接引用的过程。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和 VM 实现的内存布局相关的,同一个符号引用在不同 VM 实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

5.初始化

初始化是类加载过程的最后一步。对于初始化阶段,VM 规范严格规定了有且只有 5 种情况必须立即对类进行初始化:

  1. 遇到 new、getstatic、putstatic、invokestatic 这 4 个字节码指令时,场景有使用 new 实例化对象的时候;读取或设置一个类的静态字段时(被 final 修饰,已在编译期把结果放入常量池的静态字段除外)的时候;调用一个类的静态方法的时候;
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候;
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
  4. 当 VM 启动的时候,用户需要指定一个要执行的主类(包含 main 方法),VM 会先初始化这个主类;
  5. 当使用 JDK1.7 的动态语言支持时。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页