JVM类加载机制

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

类的生命周期

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历 加载 (Loading)、 验证(Verification)、准备(Preparation)、解析(Resolution)、 初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段, 其中验证、准备、解析三个部分统称为连接(Linking) 如下图所示: 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的 ,类型的加载过程必须按照这种顺序按部就班地开始。 而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。 注意写的是按部就班地"开始",而不是按部就班地"进行"或按部就班地"完成",强调这点是因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。

什么时候进行类加载

《Java虚拟机规范》 则是严格规定了有且只有六种 情况必须立即对类进行"初始化"(而加载、验证、准备自然需要在此之前开始。

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始 化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
  • 使用new关键字实例化对象的时候。
  • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) 的时候。
  • 调用一个类型的静态方法的。
  1. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
  2. 当初始化类的时候,若发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  3. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  4. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  5. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

主动引用/被动应用

对于这六种会触发类型进行初始化的场景,《Java虚拟机规范》中使用了一个非常强烈的限定语 ------"有且只有"。 六种场景中的行为称为对一个类型进行主动引用 。 除此之外,所有引用类型的方式都不会触发初始化,称为被动引用

类加载时机验证

主动引用验证

java 复制代码
public class ClassLoadTest {

    static {
        System.out.println("ClassLoadTest class load...");
    }    

    public static class P {
        static {
            System.out.println("P class load...");
        }
    }

    public static class S extends P {
        static {
            System.out.println("S class load...");
        }

        public static int a = 0;
    }

    public static class SS extends S {
        static {
            System.out.println("SS class load....");
        }
    }

    public static void main(String[] args) {
        Class.forName("com.jvmtest.ClassLoadTest$SS");
    }

}

结果输出

java 复制代码
ClassLoadTest class load...
P class load...
S class load...
SS class load....

结论:验证1、2、3、4种情况 类SS中增加一个静态方法

java 复制代码
public static String hello(String s) {
return "hello:" + s;
}

修改main方法为:

java 复制代码
public static void main(String[] args) {
    // 定义方法入参、返回
    MethodType mt = MethodType.methodType(String.class, String.class);
    try {
        //查找方法句柄
        MethodHandle mh = MethodHandles.lookup().findStatic(SS.class, "hello", mt);
        //根据方法句柄调用方法----注意返回值必须强转
        String result = (String) mh.invokeExact("sss");
        System.out.println(result);
    }  catch (Throwable e) {
        e.printStackTrace();
    }
}

结果为:

java 复制代码
ClassLoadTest class load...
P class load...
S class load...
SS class load....
hello:sss

结论:验证第5种情况

JDK6之前我们会使用java反射来实现动态方法调用,多数框架用反射的比较多,例如mybatis、spring等。在JDK7中,新增了java.lang.invoke.MethodHandle(方法句柄),称之为"现代化反射"。其实反射和java.lang.invoke.MethodHandle都是间接调用方法的途径,但java.lang.invoke.MethodHandle比反射更简洁,用反射功能会写一大堆冗余代码

接口由于没有静态代码块验证,我们可以通过创建新的线程来验证,增加一个接口

java 复制代码
public interface D {

    Thread thread = new Thread() {
        {
            System.out.println("D class load...");
        }
    };

    default void test() {

    }
}

//类SS 实现该接口
// main 方法修改为
new SS();

输出

java 复制代码
ClassLoadTest class load...
P class load...
S class load...
D class load...
SS class load....

注释接口中默认方法后 输出

java 复制代码
ClassLoadTest class load...
P class load...
S class load...
SS class load....

结论:验证第6种情况

被动引用验证

修改main方法为:

java 复制代码
public static void main(String[] args) {
    System.out.println(SS.a);
}

结果输出

java 复制代码
ClassLoadTest class load...
P class load...
S class load...
0

结论:可以看到通过子类引用父类的静态字段,不会导致子类初始化。 给上面的静态变量加一个final,

java 复制代码
public final static int a = 0;

输出

java 复制代码
ClassLoadTest class load...
0

SS类中增加一个常量

java 复制代码
public final static String str = UUID.randomUUID().toString();

//main方法修改为
System.out.println(SS.str);

输出

java 复制代码
ClassLoadTest class load...
P class load...
S class load...
577b1353-12ec-4805-92ce-9a8aec7fbaa0

结论:编译期常量 在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化,运行期常量会在程序运行的时候才会确定,所以触发类的初始化。 修改main方法为:

java 复制代码
public static void main(String[] args) {
    SS[] ss = new SS[10];
}

输出

java 复制代码
ClassLoadTest class load...

结论:通过数组定义来引用类,不会触发此类的初始化

类加载过程

多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启动程序,这里首先需要通过类加载器 把主类加载到JVM。 主类在运行过程中如果使用到其它类,会逐步加载这些类。 注意,jar包里的类不是一次性全部加载的,是使用到时才加载。 类加载主要是这5个过程 加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)

加载

在加载阶段,Java虚拟机需要完成以下三件事情:

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

加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段

这时候进行内存分配的仅包括类变量,而不包括实例变量

分配的默认值

数据类型 int long short char byte boolean float double reference
默认值 0 0L (short)0 '\u0000' (byte)0 false 0.0f 0.0d null

然而:如果类字段的字段属性表中存在Constant Value属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,比如:

java 复制代码
public static final int value = 123;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据Con-stantValue的设置,将value赋值为123。

解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程

符号引用

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用

直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄

这是所谓的静态链接过程(类加载期间完成)。

初始化

初始化阶段就是执行类构造器<clinit>()方法的过程。

  1. <clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物
  2. <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
  3. <clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器。
  4. Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕,因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object
  5. <clinit>()方法对于类或接口来说并不是必需的。如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  6. Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行 这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法。

一条线程用休眠来模拟长时间操作,另外一条线程在阻塞等待:

java 复制代码
public static void main(String[] args) {
    Runnable script = new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread() + "start");
            DeadLoopClass dlc = new DeadLoopClass();
            System.out.println(Thread.currentThread() + " run over");
        }
    };
    Thread thread1 = new Thread(script);
    Thread thread2 = new Thread(script);
    thread1.start();
    thread2.start();
}

static class DeadLoopClass {
    static {
        System.out.println(Thread.currentThread() + "init DeadLoopClass");
        try {
            TimeUnit.SECONDS.sleep(10);
            System.out.println("init over。。。");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

将会看到2个线程都被阻塞10s后,才会输出init over,而且静态代码块只执行一次。

java 复制代码
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]init DeadLoopClass
init over。。。
Thread[Thread-1,5,main] run over
Thread[Thread-0,5,main] run over

类加载器

JVM类加载器初始化过程

  1. C++实现创建引导类加载器,引导类加载器实例会加载 jre/lib 目录下jar包中的java核心类
  2. C++创建JVM启动器实例 sun.misc.Launcher,Launcher创建方式才用了单例模式,保证一个JVM中只有一个Launcher实例
  3. Launcher实例在构造时会创建加载扩展类加载器sun.misc.Launcher.ExtClassLoader实例、应用程序类加载器sun.misc.Launcher.AppClassLoader示例
  4. JVM默认使用launcher.getClassLoader()返回的类加载器AppClassLoader的实例加载应用程序类

源码解析(JDK1.8):

单例 构造方法 系统默认classLoader ExtclassLoader加载目录 AppClassLoader加载目录

加载流程图

JVM类加载的双亲委派机制

原理

加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载路径下都找不到目标类,则在自己的类加载器中查找并载入目标类

过程

  1. 先检查指定名称的类自己是否已经加载,如果加载过就无需重复加载,直接返回
  2. 如果指定名称的类没有加载过,判断是否有父加载器,如果有父加载器则由父加载器加载(调用parent.loadClass(name, false)),如果没有父加载器则调用bootstrap类加载器来加载(其实就是引导类加载器)
  3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass()方法来完成类加载

源码解析(JDK8)

java 复制代码
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); //调用自己实现的findClass方法

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

设计原因

  1. 沙箱安全机制

核心类库只会由引导类加载器去加载,可以防止核心API库被随意篡改

  1. 避免类的重复加载

如果父加载器已经加载了目标类,子加载器就没必要重复加载,保证被加载类的唯一性

层级结构

示例

java 复制代码
public class ClassLoaderTest {


    public static void main(String[] args) {
        // null--BootClassLoader
        System.out.println(String.class.getClassLoader());
        // ExtClassLoader
        System.out.println(CKeyPairGenerator.class.getClassLoader());
        // AppClassLoader
        ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(appClassLoader);
        System.out.println("------------------------");
        System.out.println(appClassLoader.getParent());
        System.out.println(appClassLoader.getParent().getParent());

    }

}

输出

java 复制代码
null
sun.misc.Launcher$ExtClassLoader@29444d75
sun.misc.Launcher$AppClassLoader@18b4aac2
------------------------
sun.misc.Launcher$ExtClassLoader@29444d75
null

自定义类加载器

自定义类加载器,如果不需要打破双亲委派机制的话,只要实现findClass方法即可。方法默认抛出异常。

示例代码
java 复制代码
public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 根据文件名称读取文件
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = loadByte(name);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(e.getMessage());
        }
    }

    public static void main(String[] args) throws Throwable {
        MyClassLoader classLoader = new MyClassLoader("C:\\Users");
        Class<?> aClass = classLoader.loadClass("com.demo.JvmTest");
        System.out.println(aClass.getClassLoader());
        Object object = aClass.newInstance();
        Method method = aClass.getMethod("say", String.class);
        method.invoke(object, "world");
    }

}
java 复制代码
package com.demo;

public class JvmTest {

    public void say(String s) {
        System.out.println("hello:" + s);
    }

}

输出

如果你的类还在编辑器中的话

则会输出

java 复制代码
sun.misc.Launcher$AppClassLoader@18b4aac2
hello:world

发现自定义类加载器没有生效,类加载器是AppClassLoader,符合双亲委派机制。 因为我们定义的类加载器,默认传入的父类均是AppClassLoader。 把IDE中这个类干掉,输出

java 复制代码
com.demo.jvm.MyClassLoader@39a054a5
hello:world

自定义类加载器生效。

打破双亲委派机制

打破双亲委派机制,只需要重写loadClass方法。

示例代码
java 复制代码
@Override
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 (name.startsWith("com.demo.JvmTest")) {
                    c = findClass(name);
                } else {
                    c = this.getParent().loadClass(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
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

这样就可以了。

全盘负责委托机制

"全盘负责"是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。 我们把自定义加载的部分注释掉

java 复制代码
@Override
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 (name.startsWith("com.demo.JvmTest")) {
            //                        c = findClass(name);
            //                    } else {
            //                        c = this.getParent().loadClass(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
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

运行

java 复制代码
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
Caused by: java.lang.ClassNotFoundException: C:\Users\java\lang\Object.class (系统找不到指定的路径。)

上面观点验证。

验证沙箱安全机制

那假如我们在那个目录加入缺失的Class,是不是就可以加载Object.class 了? 结果报错

java 复制代码
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

JDK1.9及以上类加载器

Java 9仍然保留了三层类加载器结构,不过为了支持模块系统,对它们做了一些调整。扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。可以通过ClassLoader的新方法getPlatformClassLoader()来获取。Java 9中的内置类加载器如下所示。

  • 引导类加载器:定义核心Java SE和JDK模块。
  • 平台类加载器:定义部分Java SE和JDK模块。
  • 应用或系统类加载器:定义CLASSPATH上的类和模块路径中的模块。

结构图

继承关系

都继承于BuiltinClassLoader

代码验证

java 复制代码
public class ClassLoaderTest {


    public static void main(String[] args) {
        // null--BootClassLoader
        System.out.println(String.class.getClassLoader());
        // AppClassLoader
        ClassLoader appClassLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(appClassLoader);
        System.out.println("------------------------");
        System.out.println(appClassLoader.getParent());
        System.out.println(appClassLoader.getParent().getParent());

    }

}

输出

java 复制代码
null
jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
------------------------
jdk.internal.loader.ClassLoaders$PlatformClassLoader@2c13da15
null

jdk为了兼容之前的版本,所以获取BootClassLoader还是null

类加载模块

我们看看这些类加载器都加载了哪些模块(JDK17)

BootClassLoader

PlatformClassLoader

AppClassLoader

自定义类加载器

java 复制代码
public class MyClassLoader extends ClassLoader{

    private final String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 根据文件名称读取文件
     */
    private byte[] loadByte(String name) throws IOException {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = loadByte(name);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(e.getMessage());
        }
    }

    @Override
    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 (name.startsWith("com.demo.JvmTest")) {
                        c = findClass(name);
                    } else {
                        c = this.getParent().loadClass(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
                    //                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    //                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    //                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    public static void main(String[] args) throws Throwable {
        MyClassLoader classLoader = new MyClassLoader("C:\\Users");
        Class<?> aClass = classLoader.loadClass("com.demo.JvmTest");
        System.out.println(aClass.getClassLoader());
        //        Object object = aClass.newInstance();
        Object object =  aClass.getConstructor().newInstance();
        Method method = aClass.getMethod("say", String.class);
        method.invoke(object, "world");
    }


}

和之前版本差不多,class获取实例的方法已过期,新的模式通过先获取构造器再创建实例。

相关推荐
yuanbenshidiaos12 小时前
c++---------数据类型
java·jvm·c++
java1234_小锋15 小时前
JVM对象分配内存如何保证线程安全?
jvm
40岁的系统架构师19 小时前
1 JVM JDK JRE之间的区别以及使用字节码的好处
java·jvm·python
寻找沙漠的人19 小时前
理解JVM
java·jvm·java-ee
我叫啥都行19 小时前
计算机基础复习12.22
java·jvm·redis·后端·mysql
bufanjun0011 天前
JUC并发工具---ThreadLocal
java·jvm·面试·并发·并发基础
东阳马生架构1 天前
JVM简介—1.Java内存区域
jvm
工程师老罗1 天前
Android笔试面试题AI答之SQLite(2)
android·jvm·sqlite
Qzer_4072 天前
jvm字节码中方法的结构
jvm
奇偶变不变2 天前
RTOS之事件集
java·linux·jvm·单片机·算法