什么叫类加载(classloader)?
类加载简单的说就是JVM通过类加载器ClassLoader,把.class文件中的信息,拼装成Class对象放入内存中。
.java---complier----.class---classloader--Class
类加载过程是什么?
类的加载主要有三步:加载->连接->初始化。连接过程又分为 验证->准备->解析
加载(Load)
指的是类加载,即class loading,虚拟机加载完成三件事情:
通过一个类的全限定名来获取定义此类的二进制字节流;
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
注意:类加载即可以由引导类加载器或自定义类加载器去完成加载。(可用开发人员控制)
连接(Linking)
连接分为,三大步骤分别为:验证、准备、解析
验证(Verification)
为了确保Class文件的字节流中包含的信息符合虚拟机要求,并且不会危害虚拟机,所以验证分为四大验证阶段,分别为:文件格式验证、元数据验证、字节码验证、符号引用验证。
注意:
一个类方法的字节码没有通过字节码验证,那肯定是有问题;
如果一个方法体通过字节码验证,也不能表示一定就是安全的;
因为程序去校验程序逻辑是无法做到绝对准确。
准备(Preparation)
目的:正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
以下参考:深入理解 java 虚拟机第三版
解析(Resolution)
解析目的:Java虚拟机将常量池内的符号引用替换为直接引用的过程,相关解析有:类或接口的解析、字段解析、方法解析、接口方法解析。
什么是符号引用?
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
什么是直接引用(Direct References)?
可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
区别于符合引用就是直接引用必须引用的目标已经在内存中存在。
初始化(Initialization)
初始化是类加载过程中最后一步,初始化目的:根据程序员程序编码制定的主观计划去初始化类变量和其他资源。
JAVA ClassLoader(类加载器)分类
Bootstrap Classloader(启动类加载器)
最顶层的加载类,由C或C 语言实现。主要加载核心类库,%JRE_HOME%lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。
Extension ClassLoader(扩展类装载器)
主要负责加载Java的扩展类库,加载目录%JRE_HOME%libext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
system classloader(系统类装载器)
也称为Appclass Loader 加载当前应用的classpath的所有类。
加载顺序?
Bootstrap Classloader->Extension ClassLoader->system classloader
代码实现
代码语言:javascript复制/**
* @author: csh
* @Date: 2020/12/28 18:35
* @Description:classLoader学习
*/
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
//获取拓展类加载器 sun.misc.Launcher$ExtClassLoader@14ae5a5
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
//获取根类加载器 null
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);
//用户自定类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2(使用系统加载器进行加载)
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);
//获取strng 类加载器 null(使用引导类加载器)java核心都是使用该种加载方式
ClassLoader stringClassLoader = String.class.getClassLoader();
System.out.println(stringClassLoader);
}
}
结果
代码语言:javascript复制sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@14ae5a5
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null
个人理解:引导类加载器和自定义类加载器,自定义类就像你自己的手机随时想用就用,或者你家人想用直接向你借就OK了,但是引导类就像某个大领导的个人手机,你跟你家人一般是无法直接借到的(基本不可能)。
全盘负责委托机制是什么?
全盘负责委托机制指的是:即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的所有 Class也由这个classloader负责载入,除非是显式的使用另外一个classloader载入;委托机制则是先让parent(父)类加载器 (而不是super,它与parent classloader类不是继承关系)寻找,只有在parent找不到的时候才从自己的类路径中去寻找。此外类加载还采用了cache机制,也就是如果 cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。
双亲委托是什么?
即在类加载的时候,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载。
注意:双亲为null有两种情况:第一,其双亲就是启动类加载器;第二,当前加载器就是启动类加载器。判断类是否加载时,应用类加载器会顺着双亲路径往上判断,直到启动类加载器。但是启动类加载器不会往下询问,这个委托是单向的。
代码语言:javascript复制protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
//检查指定类是否被当前类加载器加载过
Class c = findLoadedClass(name);
if( c == null ){//如果没被加载过,委派给父加载器加载
try{
if( parent != null )
c = parent.loadClass(name,resolve);
else
c = findBootstrapClassOrNull(name);
}catch ( ClassNotFoundException e ){
//如果父加载器无法加载
}
if( c == null ){//父类不能加载,由当前的类加载器加载
c = findClass(name);
}
}
if( resolve ){//如果要求立即链接,那么加载完类直接链接
resolveClass();
}
//将加载过这个类对象直接返回
return c;
}
双亲委托模型好处与弊端
好处:
避免重复加载:当父亲已经加载了该类,就没有必要ClassLoader再加载一次;
安全:避免自定义的类名与原生一致导致替代java核 心的api类型;
弊端:
顶层的classloader无法访问底层的classloader所加载的类。
最后
虚拟机加载过程及类的加载器相关知识的了解,有利于今后自定义一些jar包的时候可以做为基础知识进行深入。比如当了解自定义类加载器后可以在些基础上开发对应的动态jar包进行相关的动态加载。
参考文章:
https://blog.csdn.net/n20164206199/article/details/106769415
https://www.iteye.com/blog/chenzhou123520-1601267