JVM系列(一)类加载机制

类加载过程

2020年真是一个灾祸重生的一年,开年的春节假期因为肺炎疫情一直延续,弄得人们都人心惶惶,只能憋在家里写写博客打发时间,还是希望疫情早点结束,武汉加油!中国加油!
下面言归正传,本系列jvm文章主要从类加载过程、jvm内存模型、jvm垃圾收集、jvm优化等讲解本人对于jvm一些了解,希望能帮助到大家,若有不对之处欢迎,留言一起讨论。
在这里插入图片描述
上面是JVM虚拟机的一个概览图,本篇博客主要讲解类装载子系统方面的知识。
在这里插入图片描述
上图是类的加载过程
多个java文件经过编译打包生成可运行的jar包(例如S pring Boot项目),最终由java命令运行某个主要的main函数启动程序,这里首先需要通过 类加载器主类 加载到JVM。主类在运行过程中如果使用到其他类,会逐步加载这些类。
注意 jar包中的类不是一次性全部加载的,是使用到才加载的。

类加载到jvm的步骤

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载
    加载 : 在硬盘上查找并通过IO流读入字节码文件,使用到类时才会加载,例如 new 对象等等。
    验证: 校验字节码文件的正确性。
    准备: 给类的静态变量分配内存,并赋予默认值
    解析: 将符号引用替换为直接引用,该阶段会把一些静态方法替换为指向数据所存内存的指针或句柄(直接引用),这里所谓的静态链接过程(类加载期间完成),动态链接过程是在程序运行期间完成的将符号引用替换为直接引用。
    初始化: 对类的 静态变量初始化设定的值,执行静态代码块。

相信单纯的讲一下这个过程还是不是很容易理解,下面对每个步骤在进行详细的说明一下。
加载就不多说了,着重说明一下验证 步骤,验证的是字节码文件(.class文件)的正确性,那么什么是class文件?具体要验证什么呢?首先我们要明白一下几个概念。

1.什么是Class文件

Java字节码类文件(.class)是Java编译器编译Java源文件(.java)产生的“目标文件”。它是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。
class文件中的信息是一项一项排列的, 每项数据都有它的固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或8个字节, 数据项的不同长度分别用u1, u2, u4, u8表示, 分别表示一种数据项在class文件中占据一个字节, 两个字节, 4个字节和8个字节。 可以把u1, u2, u3, u4看做class文件数据项的“类型” 。

2.Class文件结构

下面就是我们的Class文件通过HEX-Editor插件编码后的结果
在这里插入图片描述
在这里插入图片描述

链接: .class文件具体解释说明参考
理解了什么是class文件,我们继续来说 验证 步骤到底验证什么。

  • 文件格式的验证
    • 是否以ca fe ba be开头
    • 版本号是否合理(编译版本和运行版本是否一致)
  • 元数据的验证
    • 是否有父类
    • 是否继承final类
    • 非抽象类实现了所有的抽象方法
  • 字节码验证
    • 运行检查
    • 栈数据类型和操作码数据参数吻合
    • 跳转指令指定到合理位置
  • 符号引用验证
    • 常量池中描述类是否存在
    • 访问的方法或字段是否存在且有足够的权限

讲完了验证步骤,准备和初始化步骤都是操作的静态变量,举个例子就很好明白了。

public class Test{
	private static User u = new User();
	private static int tem = 100;
	private static final int v = 1;
}

准备阶段就是将u设置成初始值null,将tem设置成初始值0。
初始化阶段将u 在初始化成 new User(),将tem设置成100。
对于static final类型,在准备阶段就会被赋上正确的值。

什么是类加载器

类的加载过程我们知道,是通过类加载器 把主类加载到jvm中的,那么到底什么是类加载器呢?
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。
虚拟机加载类有两种方式,一种方式ClassLoader.loadClass()方法,另一种是使用反射API,Class.forName()方法,其实Class.forName()方法内部也是使用的ClassLoader。

类加载器间的关系

在这里插入图片描述
由上图可知:
一个类的加载顺序是:自顶向下
一个类的检查顺序是:自底向上
这几种加载器不是继承关系,而是委托关系。那么类加载器是如何工作的呢?下面我们就从ClassLoader源码的loadClass()方法来进行解析。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //首先检查这个classsh是否已经加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果父类的加载器为空 则说明递归到bootStrapClassloader了
                        //bootStrapClassloader比较特殊无法通过get获取
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果bootstrapClassLoader 仍然加载完成后没有找到此类,则递归回来,尝试下级加载器调用findClass()方法去加载
                    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;
        }
    }

	private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }
	
	// return null if not found
    private native Class<?> findBootstrapClass(String name);

从ClassLoader源码的loadClass()方法中,我们可以看出,类加载器优先调用父类的loadClass()方法,优先使用父类进行加载,父类加载不到在调用子类进行加载。
这里的类加载其实就是一种双亲委派机制,加载某个类时会先委托父类加载器寻找目标类,找不到在委托上层父类加载器加载,如果所有父类加载器在自己的路径下都找不到目标类,则在自己的类路径下查找并载入目标类。

如何实现自定义类加载器

实现自定义的类加载器只需要继承ClassLoader类,并重写findClass方法即可。

扫描二维码关注公众号,回复: 9257807 查看本文章
public class CustomClassLoader extends ClassLoader {

	@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException 	{
        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException(name);
            } else {
                // defineClass方法将字节码转化为类
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new ClassNotFoundException(name);
    }

	public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader("D:/ClassLoader");
        try {
            Class<?> clazz = customClassLoader.loadClass("cn.com.study.classLoader.Log");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.invoke(obj,null);
            System.out.println(clazz.getClassLoader().getClass().getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果在这里插入图片描述
这里虽然是自定义了类加载器,但是真正加载Log类使用的加载器是AppClassLoader,因为Log类在classPath下也是存在在,此时调用的loadClass()方法是父类的,所以类加载是存在双亲委派机制的,所以AppClassLoader是优先加载的。
那么如何能实现不通过AppClassLoader加载而使用自己的类加载器加载,这就需要打破双亲委派机制,需要重写loadClass()方法

@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) {
                // 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;
        }
    }

总结:实现自定义类加载器,如果不需要打破双亲委派机制,只需要重写findClass方法,若需要打破双亲委派机制还需要重写loadClass方法。

JVM为什么要实现双亲委派机制

1.沙箱安全机制: 自己写的java.lang.String.class类是不会被加载的,这样可以防止核心API库被随意篡改
2.避免类的重复加载: 当父类已经加载了该类后,就没有必要子ClassLoader再加载一次,保证被加载的类只被加载一次。

发布了12 篇原创文章 · 获赞 0 · 访问量 349

猜你喜欢

转载自blog.csdn.net/fd135/article/details/104225461