类加载机制详细说明

10/27/2021 class

作为java开发,我们知道java的运行方式有多种,比如:在开发工具中运行、执行jar文件运行、又或者在命令行中执行...但是这些都离不开jre,即为:java运行时环境。

jre包含:jvm以及java的核心类库;我们一般jdk中即包含了jre还有一系列的工具和诊断工具等等;

那么java运行的过程是怎样的呢?

首先,我们按照java语法编写的代码.java经过jvm编译器编译为.class文件。

然后jvm再将.class文件加载到jvm中。最后才是使用,使用完成之后卸载。

那么接下来我们详细说明一下这个加载机制。

# 加载机制

类加载机制是jvm把class文件加载到内存,并对数据进行校验、准备、解析、初始化,最终形成jvm可以直接使用的Java类型的过程。

# 加载

将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。

# 链接

将Java类的二进制代码合并到jvm的运行状态之中。

# 验证

确保加载的类信息符合jvm规范,没有安全方面的问题。

# 准备

正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意此时的设置初始值为默认值(除非是带有final修饰,在这里已经被赋值),具体赋值在初始化阶段完成。

# 解析

虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java (opens new window)中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用: 直接引用可以是

(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

(3)一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

# 初始化

初始化阶段是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中的所有类变量的 赋值 动作和 **静态语句块(static块) **中的语句合并产生的。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
  • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

我们用一个示例说明一下初始化顺序:

public class Parent {

    static String p = "父类静态变量";

    String m = "父类非静态变量";

    static {
        System.out.println("父类静态代码块");
    }

    {
        System.out.println("父类非静态代码块");
    }

    public Parent() {
        System.out.println("执行了父类的构造方法");
    }
}

public class Child extends Parent {

    static String p = "子类静态变量";

    String m = "子类非静态变量";

    static {
        System.out.println("子类静态代码块");
    }

    {
        System.out.println("子类非静态代码块");
    }

    public Child() {
        System.out.println("执行了子类的构造方法");
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
    }
}


//执行结果
父类静态代码块
子类静态代码块
父类非静态代码块
执行了父类的构造方法
子类非静态代码块
执行了子类的构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

可以看到 类初始化顺序为: 静态成员和静态代码块>普通成员和普通代码块>构造函数;静态初始化>父类初始化>子类初始化;

# 类加载器

引导类加载器(bootstrap class loader) (1)它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路径下的内容),是用原生代码(C语言)来实现的,并不继承自 java.lang.ClassLoader。 (2)加载扩展类和应用程序类加载器。并指定他们的父类加载器。

扩展类加载器(extensions class loader) (1)用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路径下的内容) 。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类。 (2)由sun.misc.Launcher$ExtClassLoader实现。

应用程序类加载器(application class loader) (1)它根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。 (2)由sun.misc.Launcher$AppClassLoader实现。

自定义类加载器 (1)开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

package com.example.demo;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * @author : zhanghuang
 * @date : 2021-10-27 5:19 下午
 */
public class MyClassLoader extends ClassLoader {

    //指定路径
    private String path;


    public MyClassLoader(String classPath) {
        path = classPath;
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class log = null;
        byte[] classData = getData();
        if (classData != null) {
            log = defineClass(name, classData, 0, classData.length);
        }
        return log;
    }

    /**
     * 将class文件转化为字节码数组
     *
     * @return
     */
    private byte[] getData() {
        File file = new File(path);
        if (file.exists()) {
            FileInputStream in = null;
            ByteArrayOutputStream out = null;
            try {
                in = new FileInputStream(file);
                out = new ByteArrayOutputStream();

                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = in.read(buffer)) != -1) {
                    out.write(buffer, 0, size);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    assert in != null;
                    in.close();
                } catch (IOException e) {

                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        } else {
            return null;
        }
    }

}


package com.example.demo;

/**
 * @author : zhanghuang
 * @date : 2021-10-27 5:22 下午
 */
public class Log {
    public static void main(String[] args) {
        System.out.println("load Log class successfully");
    }
}


package com.example.demo;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author : zhanghuang
 * @date : 2021-10-27 5:04 下午
 */
public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {

        String path = "/Users/zhanghuang/servyou/demo/src/main/java/com/example/demo/Log.class";
        MyClassLoader myClassLoader = new MyClassLoader(path);
        //类的全称
        String packageNamePath = "com.example.demo.Log";
        Class<?> log = myClassLoader.findClass(packageNamePath);
        System.out.println("类加载器是:" + log.getClassLoader());
        Method main = log.getDeclaredMethod("main", String[].class);
        Object o = log.getDeclaredConstructor().newInstance();
        String[] arg = {"ad"};
        main.invoke(o, (Object) arg);

    }
}

输出:
类加载器是:com.example.demo.MyClassLoader@6108b2d7
load Log class successfully

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

# java.class.ClassLoader类

(1)作用:

  • java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。
  • ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

(2)常用方法:

  • getParent() 返回该类加载器的父类加载器。
  • loadClass(String name) 加载名称为 name的类,返回的结果是java.lang.Class类的实例。 此方法负责加载指定名字的类,首先会从已加载的类中去寻找,如果没有找到;从parent ClassLoader[ExtClassLoader]中加载;如果没有加载到,则从Bootstrap ClassLoader中尝试加载(findBootstrapClassOrNull方法), 如果还是加载失败,则自己加载。如果还不能加载,则抛出异常ClassNotFoundException。
  • findClass(String name) 查找名称为 name的类,返回的结果是java.lang.Class类的实例。
  • findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
  • defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实例。这个方法被声明为 final的。
  • resolveClass(Class<?> c) 链接指定的 Java 类。

简而言之,类加载器的原理:双亲委托机制,向上委托,向下加载;

通常当你需要动态加载资源的时候 , 你至少有三个 ClassLoader 可以选择 : 1.系统类加载器或叫作应用类加载器 (system classloader or application classloader) 2.当前类加载器 3.当前线程类加载器

当前线程类加载器是为了抛弃双亲委派加载链模式。 每个线程都有一个关联的上下文类加载器。如果你使用new Thread()方式生成新的线程,新线程将继承其父线程的上下文类加载器。如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上 下文类加载器。Thread.currentThread().getContextClassLoader()

Tomcat服务器的类加载器

每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式(不同于前面说的双亲委托机制),所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。

最后更新时间: 11/24/2022, 9:44:46 AM