JVM之我们的代码是如何被加载到JVM执行的?(一)

 2023-01-03
原文作者:金1998 原文地址:https://juejin.cn/post/6979869357266436103

我们的代码是如何被加载到JVM执行的?(一)

流程:1.xxx.java -> 2.javac编译 -> 3.xxx.class -> 4.ClassLoader -> 5.字节码解释器、JIT -> 6.执行引擎执行

202301011609028791.png

java文件编译成class文件

我们对java文件是熟悉的,但是对class文件是陌生的。JVM运行的是class文件,只要文件能够解释成class文件,那么都能在JVM上运行,将语言和执行代码分开(适配器模式思想),这也是“一次编译,到处运行”的由来。我们可以一起来了解下class文件的构成,对我们理解后续的过程是有帮助的。

202301011609034802.png

上面这个明显很难看,在IDEA中可以使用插件“jclasslib Bytecode Viewer”进行辅助查看。

202301011609040533.png

插件将描述被自动转成中文了,所以看下面的文件结构以及代表的含义:

ClassFileFormat(文件结构)

  • Magic Number -- 魔数:代表本文件是.class文件 -- cafe babe
  • Minor Version -- 次版本号,向下兼容
  • Major Version -- 主版本号,向下兼容
  • constant_pool_count -- 常量池个数
  • constant_pool(长度为constant_pool-conunt-1的表) -- 常量池
  • access_flags -- 访问标志(可进行位运算)
  • this_class -- 当前类
  • super_class -- 父类
  • interfaces_count -- 实现几个接口
  • interfaces -- 具体接口
  • fields_count -- 属性个数
  • fields -- 具体属性
  • methods_count -- 方法个数
  • methods -- 具体方法
  • attributes_count - u2 -- 附加属性有哪些
  • attributes -- 具体附加属性

想更深入的阅读,可看这篇文章:详解Class类文件的结构

以上是class文件的阅读,接下来将描述第四步ClassLoader的内容。

ClassLoader类加载器

列举常见的类加载器(级别由高到低):Bootstrap ClassLoader,Extension ClassLoader,App ClassLoader,Custom ClassLoader...

类加载器的工作步骤

202301011609045304.png

1. Loading -- 加载

类加载器的加载机制:双亲委派机制(如下图)。当加载一个class文件时,如果自定义类加载器(A)已经加载过则直接返回Class对象,如果没有则通过类加载器(A)的parent属性找父类加载器(B),如果父类加载器(B)加载过则返回Class对象,没有则继续访问父类加载器(B)的parent属性找父类加载器(C)有没有加载过,直到Bootstap(最顶级的)也没加载过时,则按照这个class文件的路径依次向下询问是否为该类加载器的加载范围,如果是则进行加载,如果都不是,则由自定义类加载器加载出来。

双亲委派机制的优点:避免类重复加载、核心类被篡改等安全情况,主要是为了安全考虑。

双亲委派机制的缺点:如果某个类已经被某个类加载器加载出来(特别是Bootstrap),其他任何类加载器都不能对其进行修改,也不会重复加载其他的接口实现,但是开发者又确实需要修改这个接口的实现,那么则需要使用“SPI( 服务提供者接口)”机制,实现策略模式和热插拔效果。

202301011609050715.png

    /**
     * 类加载器层级关系
     */
    public class JVMTest {
        public static void main(String[] args) {
            ClassLoader classLoader1 = JVMTest.class.getClassLoader();
            ClassLoader classLoader2 = classLoader1.getParent();
            ClassLoader classLoader3 = classLoader2.getParent();
    
            System.out.println("classLoader1 == " + classLoader1); // classLoader1 == sun.misc.Launcher$AppClassLoader@14dad5dc
            System.out.println("classLoader2 == " + classLoader2); // classLoader2 == sun.misc.Launcher$ExtClassLoader@19469ea2
            System.out.println("classLoader3 == " + classLoader3); // classLoader3 == null -->由于Bootstrap是由C++实现,java并没有该类,所以返回Null
        }
    }

ClassLoader源码解析

上面是理解ClassLoader是什么帮助大家,理解脉络。下面我们主要看下loadClass的源码,验证上面的脉络是否说的一致。

当要加载一个class时,入口方法是loadClass,下面是loadClass的伪代码。

    protected Class<?> loadClass(String name, boolean resolve){
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 首先,检查这个class是否已经被加载过
            Class<?> c = findLoadedClass(name);
            // 没有,则走if里面
            if (c == null) {
                // 先从父类加载器开始加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
               
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    // 如果父类加载器加载不出来则调用findClass查找,并将class对象生成出来
                    c = findClass(name);
                }
            }
            if (resolve) {
                // 如果resolve=true则用resolveClass()处理类
                resolveClass(c);
            }
            return c;
        }
    }

resolveClass():链接指定的类。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™规范中的Execution描述进行链接。

URLClassLoader类中的findClass()实现

    protected Class<?> findClass(final String name){
        final Class<?> result;
        result = AccessController.doPrivileged(
           new PrivilegedExceptionAction<Class<?>>() {
               public Class<?> run() throws ClassNotFoundException {
                   String path = name.replace('.', '/').concat(".class");
                   Resource res = ucp.getResource(path, false);
                   if (res != null) {
                       // 生成class对象
                       return defineClass(name, res);
                  } else {
                      return null;
                   }
               }
           }, acc);
        return result;
    }

2.Linking -- 链接

  1. Verification -- 验证 校验class文件,class文件需符合当前虚拟机的要求(例如上面class文件的开头“cafe babe”),保证class文件加载正确。
  2. Preparation -- 准备 静态变量赋默认值,对类中的静态字段分配内存空间,基础类型赋默认值(例如long=0L,int=0),引用类型默认为null。
  3. Resolution -- 解析

我们看class文件可知,类的很多信息的引用都放在了常量池里,如果使用的时候每次都去常量池里取,也比较低效,所以解析过程为类、接口、方法、成员变量的符号引用定位直接引用。

3.Initializing -- 初始化

静态变量赋值为初始值

下期预告