概述
上一篇可以不用掌握,但必须了解字节码文件的整体结构中提到了class文件其实就是二进制流,那么我们如何把二进制流加载到JVM中?今天来学一下类的加载
类加载基本分为3大步,loading(加载),linking(链接),initializing(初始化),linking又分为3步,分别是verification(验证),preparation(准备),resolution(解析)。基本流程如下图
Loading
定义:通过类的全限定名(路径+文件名)获取此类的二进制流(字节码文件),将二进制流转为方法区的运行时数据结构,同时创建class对象,作为此类的访问入口
加载工具:ClassLoader,有bootstrap,extention,app,custom加载器。
加载方式:parent delegating(双亲委派)
加载流程
(图片来源于网络)
流程简述
当加载类(假设为test)时,CustomClassLoader查询是否加载过test,是的话,直接返回,否则问父加载器AppClassLoader
AppClassLoader查询是否加载过test,是的话,直接返回,否则问父加载器ExtClassLoader
ExtClassLoader查询是否加载过test,是的话,直接返回,否则问父加载器BootstrapClassLoader
BootstrapClassLoader查询是否加载过test,是的话,直接返回,否则查看自己是否可以加载,是的话,加载并返回,否的话,则让ExtClassLoader去加载
ExtClassLoader查看自己是否可以加载,是的话,加载并返回,否的话,则让AppClassLoader去加载
AppClassLoader查看自己是否可以加载,是的话,加载并返回,否的话,则让CustomClassLoader去加载
CustomClassLoader查看自己是否可以加载,是的话,加载并返回,否的话,则抛出ClassNotFundException
BootstrapClassLoader:由C++实现,在Java中用null来指代,只加载最基础最重要的类,例如JRE的lib目录下jar包中的类以及有虚拟机参数-Xbootclasspath指定的类。
ExtClassLoader:其父加载器是BootstrapClassLoader,加载相对次要但通用的类,例如JRE的lib/ext目录下jar包的类,以及java.ext.dirs指定的类。(java 9 之后,改名为平台类加载器,加载更多类)
AppClassLoader:其父加载器是ExtClassLoader,加载应用程序路径下的类。
CustomClassLoader:自定义加载器,可实现特殊的加载方式,例如对class文件加密,加载时利用自定义的类加载器进行解密后加载。
双亲委派加载的源码
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 { //最顶级父类bootstrapClassLoader加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } //父类中既没有缓存,也无法加载,则当前classloader尝试加载 if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); //自定义classLoader只能重写findClass方法 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.lang.String.Class,在未加载的情况下,会默认先轮询到bootstrapClassLoader,而bootStrapClassLoader已经加载过String.class了,并不会加载篡改后的class。
如何打破双亲委派?
自定义类加载器只要重新loadClass方法,即可以打破双亲委派。一般自定义类加载器只重新findClass方法是不会打破双亲委派的。详细介绍可以看此文:如何打破双亲委派
类的加载时机
jvm加载类采用的是懒加载,几种常见的场景:
new对象,getStatic,putStatic时
反射
初始化子类时
虚拟机启动时,主类必须加载
LINKING
verification
验证class文件的字节流符合虚拟机要求,保证被加载类的正确性。
文件格式验证:class文件开头都是CAFE BABE
元数据验证
字节码验证
符号引用验证
preparation
为静态变量分配内存,并将静态变量设置为初始值(零值)
resolution
将常量池内的符号引用转为内存中的直接引用
INITIALIZING
执行clinit方法,将执行所有静态变量和静态代码块的语句(会按照语句在原文件中的顺序进行执行),将静态变量设置为初始值。
原文:https://juejin.cn/post/7098264829915299847