JVM学习笔记 - 01类的加载过程

471 阅读5分钟

Java从编码到执行: 已知有个java文件叫Demo.java,执行javac命令后生成Demo.class。再执行java命令,就会使用ClassLoader加载到内存,同时Java相关类库也会加载到内存。然后调用字节码解释器或即时编译器进行编译。最后调用执行引擎进行程序执行。

关于javac的过程,会专门写一篇JVM学习笔记 - 00 类编译来描述javac过程。

类加载过程主要有三个步骤Loading -> Linking -> Initializing,其中Linking又包含了Verification -> Preparation -> Resolution三个步骤,如下图:

Loading

双亲委派

BootstrapClassLoader加载lib/rt.jarlib/charset.jar等核心类;

ExtClassLoader加载扩展包jre/lib/ext/*.jar,或由-Djava.ext.dirs指定;

AppClassLoader加载classpath指定内容;

Costom Classloader为自定义类加载器。

加载一个类的过程如下:

  • 从自定义ClassLoader中查找该类是否加载,已加载则停止;未加载则去AppClassLoader,查找;
  • AppClassLoader中查找该类是否加载,已加载则停止;未加载则去ExtClassLoader,查找;
  • ExtClassLoader中查找该类是否加载,已加载则停止;未加载则去BootstrapClassLoader,查找;
  • BootstrapClassLoader中查找该类是否加载,已加载则停止;未加载则判断该类是否由该加载器加载,若是则直接加载,若不是则去ExtClassLoader进一步判断;
  • 判断该类是否由ExtClassLoader加载,若是则加载,若不是则去AppClassLoader进一步判断;
  • 判断该类是否由AppClassLoader加载,若是则加载,若不是则去自定义ClassLoader进一步判断;
  • 判断该类是否由ClassLoader加载,若是则加载,若不是则抛出ClassNotFoundException异常;

注意: 这几个类加载其互相不为继承关系,每个ClassLoader都有一个private final ClassLoader parent;属性,表示父ClassLoader

ClassLoader源码

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                   ···
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

在上述步骤中,查找类是否被加载由方法findLoadedClass()来处理;去父加载器加载由parent.loadClass()来处理;当加载器为BootstrapClassLoader时,parentnull,直接执行findBootstrapClassOrNull()方法;具体的加载过程由findClass()来处理。

自定义类加载器 // TODO

自定义类加载器只需要继承ClassLoader,并重写findClass()方法即可,一个简单的例子如下:

public class TestClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File f = new File("test/", name.replace(".", "/").concat(".class"));
        try {
            FileInputStream fis = new FileInputStream(f);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b;
            while ((b = fis.read()) != 0) {
                baos.write(b);
            }
            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
}

Hello World如下:

public class HelloWorld {
    public void print() {
        System.out.println("Hello World");
    }
}

测试代码如下:

    public static void main(String[] args) throws Exception {
        ClassLoader l = new TestClassLoader();
        Class clazz = l.loadClass("com.wzy.test.HelloWorld");

        HelloWorld helloWorld = (HelloWorld) clazz.newInstance();
        helloWorld.print();
    }

混合执行 编译执行 解释执行

Java默认使用混合模式,也就是混合使用解释器(bytecode intepreter)和热点代码编译(JIT, Just In-Time Compiler)。起始节点采用解释执行,然后进行热点代码检测。多测被调用的方法以及多次被调用的循环会被进行编译。

参数设定:

-Xmixed - 混合模式

-Xcomp - 编译模式

-Xint - 解释模式

Linking

Verification

验证文件是否符合JVM规定,详细可以查看文章JVM学习笔记 - 00 类编译 // TODO

Preparation

静态成员变量赋默认值,例如:int为0,double为0.0,引用类型为null等。但是,被final修饰的static变量,例如public static int value = 123;会直接赋值为123。

Resolution

将类、方法、属性等符号引用解析为直接引用,常量池中的各种符号引用解析为指针、偏移量等内存地址的直接引用。

符号引用: 与虚拟机布局无关,由于java类编译成class文件时,虚拟机还不知道引用类的地址,所以就以无歧义的符号来描述引用目标。这里有个简单的例子:

public class Test {
    private static int i_static_1;
    private static int i_static_2 = 5;
    private int i1;
    private int i2 = 10;
}

反编译后

public class com.wzy.springboot.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref          #5.#18         // java/lang/Object."<init>":()V
#2 = Fieldref           #4.#19         // com/wzy/springboot/Test.i2:I
#3 = Fieldref           #4.#20         // com/wzy/springboot/Test.i_static_2:I
#4 = Class              #21            // com/wzy/springboot/Test
#5 = Class              #22            // java/lang/Object
#6 = Utf8               i_static_1
#7 = Utf8               I
#8 = Utf8               i_static_2
#9 = Utf8               i1
#10 = Utf8               i2
#11 = Utf8               <init>
#12 = Utf8               ()V
#13 = Utf8               Code
#14 = Utf8               LineNumberTable
#15 = Utf8               <clinit>
#16 = Utf8               SourceFile
#17 = Utf8               Test.java
#18 = NameAndType        #11:#12        // "<init>":()V
#19 = NameAndType        #10:#7         // i2:I
#20 = NameAndType        #8:#7          // i_static_2:I
#21 = Utf8               com/wzy/springboot/Test
#22 = Utf8               java/lang/Object

常量池中带有Utf8的其实就是符号引用。

直接引用: 和虚拟机布局相关,不同虚拟机对于相同符号引用翻译出来的直接引用不同。如果有了直接引用,那目标一定被加载到了内存中。 直接引用可以是:

  • 直接指向类、方法和属性的指针;
  • 相对偏移量(指向实例变量、实例方法的直接引用都是偏移量);
  • 一个间接定位到对象的句柄。

Initializing

调用初始化代码,即类构造器<clinit>,给静态成员变量赋初始值。举个无聊的例子:

public class Test {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T {
    public static T t = new T(); // null
	public static int count = 2; // 0
    
    private T() {
        count ++;
        //System.out.println("--" + count);
    }
}  

这段代码打印结果为2。Preparation阶段,tnullcount为0。Initializing阶段,new T()调用头早方法,count变为1,紧接着count被赋初始值,又变回了2。