Java类加载器( 死磕3)
文章很长,而且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《尼恩Java面试宝典 最新版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
【正文】Java类加载器( CLassLoader ) 死磕3:
揭秘 ClassLoader抽象基类
本小节目录
3.1. 类的加载分类:隐式加载和显示加载
3.2. 加载一个类的五步工作
3.3. 如何获取类的加载器
3.4 解刨加载器——ClassLoader抽象基类揭秘
3.5. loadClass 关键源代码分析
3.1. 揭秘ClassLoader抽象基类
3.1.1. 类的加载分类:隐式加载和显示加载
java中类是动态加载的,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。一是加快启动的速度,二是节约内存。如果一次性加载全部jar包的所有class,速度会很慢。
动态载入一个class类,有两种方式:
(1) implicit隐式加载
即通过实例化才载入的特性来动态载入class。比如:
IPet dog=new Dog();
在新建对象时,如果Dog.class类没有加载,则JVM 会在背后调用当前的AppClassLoader,加载Dog.class,并且初始化。
(2) explicit显式加载
explicit显式方式,又分两种方式:
一是:java.lang.Class的forName()方法。 比如:
Class<?> aClass = Class.forName(className);
二是:java.lang.ClassLoader的loadClass()方法。 比如:
FileClassLoader classLoader = new FileClassLoader(null,baseDir);
Class<?> aClass = classLoader.loadClass(className);
下面有两个问题:
两种显示加载的方式,有何区别呢?
两种显示加载的方式,有何联系呢?
花开两朵,各表一枝。 现在先介绍通过第二种ClassLoader显式加载方法 ,其类的加载过程。然后再进行两种方式的比对。
在介绍ClassLoader显式加载前,先回顾一下类的加载过程。
3.1.2. 加载一个类的五步工作
在疯狂创客圈的《死磕java》工程源码中,有一个常用的系统属性的配置类——SystemConfig 。下面以次为例,展示一下类的加载过程。
源代码如下:
package com.crazymakercircle.config;
.....................
@ConfigFileAnno(file = "/system.properties")
public class SystemConfig extends ConfigProperties{
static
{
Logger.info("开始加载配置文件到SystemConfig");
//依照注解装载配置项
loadAnnotations(SystemConfig.class);
}
....................................
@ConfigFieldAnno(proterty = "debug")
public static boolean debug;
/**
* 宠物工厂类的名称
*/
@ConfigFieldAnno(proterty = "pet.factory.class")
public static String PET_FACTORY_CLASS;
/**
* 宠物模块的类路径
*/
@ConfigFieldAnno(proterty = "pet.lib.path")
public static String PET_LIB_PATH;
}
这个类的主要功能是,从配置文件中加载配置项,方便编程时使用。
编译完成后,这个”.java”文件经过Java编译器编译成拓展名为”.class”文件——SystemConfig.class,这个”.class”文件中保存着Java代码经转换后的虚拟机指令。
当需要使用这个类时,虚拟机将会加载它的SystemConfig.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这个就是类加载的过程。
类加载的过程分为五步:加载、验证、准备、解析、初始化。
一、加载:
通过一个类的完全限定名称,查找此类字节码”.class”文件,读入内存形成字节码流。并创建一个Class对象。
二、验证
检查字节流中包含信息符合虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
三、准备
主要的工作是,在方法区分配静态变量的内存,并且进行内存的初始化,设置初始值即0。
四、解析
主要将常量池中的符号引用进行翻译,翻译为直接引用,也就是在内存中的地址。
五、初始化
首先,如果类的存在静态变量需要进行赋值,在这个阶段完成。
其次,如果有static 静态块,在这个阶段执行。
再次,若该类具有超类,则对其进行初始化。
实例中,通过这一步,完成执行SystemConfig 类的static块的执行,并且为每一个配置项赋值,因为都是静态的。
通过这个五步,一个类的全限定名的”.class”文件,完成了转换为一个与目标类对应的java.lang.Class对象实例的工作。 实际的工作,远远比上面的陈述的负责。为了方便理解和记忆,上面进行了大大的简化,只是提取出主要的特征。
以上五步的中间3个步骤,验证、准备、解析,合起来有一个统称,叫类的链接。
3.1.3. 如何获取类的加载器
使用 getClassLoader() 方法,可以取得类的加载器。如果是应用程序的class path下的类,加载器一般为AppClassLoader 类型。当前,并不是绝对的,这个后面讲到如何去定制和修改。
查看一下当前实例的应用程序类所属的classLoader,代码如下:
public static void showCurrentClassLoader()
{
Logger.info("");
Logger.info("显示当前类的ClassLoader::");
Logger.info("class=" + ClassLoaderDemo.class.getCanonicalName());
ClassLoader loader = ClassLoaderDemo.class.getClassLoader();
Logger.info("Loader=" + loader.toString());
}
结果如下:
showCurrentClassLoader |> 显示当前类的ClassLoader::
showCurrentClassLoader |> class=com.crazymakercircle.classLoaderDemo.base.ClassLoaderDemo
showCurrentClassLoader |> Loader=sun.misc.Launcher$AppClassLoader@18b4aac2
在显示加载场景中,A加载B,一般情况下,B的加载器就是A的加载器。演示类ClassLoaderDemo加载了SystemConfig类,看看后者的加载器是啥?
代码如下:
public static void showAppLoader()
{
String className = "com.crazymakercircle.config.SystemConfig";
Class<span style="color: rgb(0, 0, 255);"><?</span>> target = null;
try
{
//根据类名 显示加载加载类
target = Class.forName(className);
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
Logger.info("显示加载的类的ClassLoader:");
Logger.info("class=" + target.getCanonicalName());
ClassLoader loader = target.getClassLoader();
Logger.info("Loader=" + loader.toString());
}
结果如下:
showAppLoader |> 显示加载的类的ClassLoader:
showAppLoader |> class=com.crazymakercircle.config.SystemConfig
showAppLoader |> Loader=sun.misc.Launcher$AppClassLoader@18b4aac2
%java.ext.dirs% 下jar中类的加载器,类型一定为Extention ClassLoader。比方说,DNSNameService 域名服务类是一个典型的 lib\ext 中的dnsns.jar包中的类,我们来看下他的加载器。代码如下:
public static void showExtClassLoader()
{
ClassLoader loader = DNSNameService.class.getClassLoader();
Logger.info("");
Logger.info("%java.ext.dirs% 下类的ClassLoader:");
Logger.info("class=" + DNSNameService.class.getCanonicalName());
Logger.info("Loader=" + loader.toString());
}
结果如下:
showExtClassLoader |> %java.ext.dirs% 下类的ClassLoader:
showExtClassLoader |> class=sun.net.spi.nameservice.dns.DNSNameService
showExtClassLoader |> Loader=sun.misc.Launcher$ExtClassLoader@12a3a380
%java.home% 下jar中类的加载器,我们可以理所当然的认为,一定是Bootstrap ClassLoader 加载器类型。 比方说,String 类是一个典型的系统属性%java.home% 目录下 rt.jar 中的类,我们来看下他的加载器。代码如下:
public static void showBootstrapClassLoader()
{
ClassLoader loader = String.class.getClassLoader();
Logger.info("");
Logger.info("%java.home%下类的ClassLoader:");
Logger.info("class=" + String.class.getCanonicalName());
Logger.info("Loader=" + loader);
}
遗憾的时,结果与前面两个加载器,大不一样。
结果如下:
showBootstrapClassLoader |> %java.home%下类的ClassLoader:
showBootstrapClassLoader |> class=java.lang.String
showBootstrapClassLoader |> Loader=null
1.1.4. 解刨加载器——揭秘ClassLoader抽象基类
ClassLoader类是Java 中的 所有ClassLoader 的基类,在java.lang包中。这是一个抽象类。
ClassLoader类,包含了所有类加载器的三个重要组成部分:
(1)加载成功的Class 对象的缓存;
(2)类的查找路径
(3)loadClass(String name)
ClassLoader加载器将加载成功的Class 对象,加入一个Vector 动态数组中,避免重复加载。 这个Vector 动态数组,也就是加载成功的classes的缓存。
源码如下:
// The classes loaded by this class loader. The only purpose of this table
// is to keep the classes from being GC'ed until the loader is GC'ed.
private final Vector<Class<?>> classes = new Vector<>();
前面提到,每一个加载器,都有一个自己的查找范围,是一系列的jar包或者类路径,称之为查找路径。形象的说,这个查找路径,就像是ClassLoader加载类的专属的地盘。
其源码如下:
// The paths searched for libraries
private static String usr_paths[];
private static String sys_paths[];
讲了这么多,终于到了关键点——ClassLoader加载一个类的秘密在哪里?。
加载类的方法是:
ClassLoader的loadClass(String name)方法,是类加载器中一个比较重要的方法。其作用是,用于加载一个类。
这个方法,比较理想的流程,大致如下面所示:
(1)首先在缓存中找,是否已经加载。如果找到就返回。
(2)如果找不到,就去查找路径查找文件。如果找出类的.class字节码,则去完成加载一个类的五步工作,放入自己的缓存中,然后返回给调用者。
loadClass(String name)方法,充分将前面的 classes 缓存和查找路径利用起来,将他们串在了一起。
实际上,这仅仅只是一个理想化的结果。
实际的类加载流程,远远比以上的假想流程,复杂得多。
直接来看 loadClass 方法的源代码吧,这样反而简单、粗暴、直接。
1.1.5. loadClass 关键源代码分析
loadClass 方法的源代码如下:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//在加载器的缓存中,按照类名,查找已经加载好的现成类
Class<span style="color: rgb(0, 0, 255);"><?</span>> c = findLoadedClass(name);
//如果加载成功,就直接返回了
//如果还没有找到,尴尬了
if (c == null) {
....
try {
if (parent != null) {
//优先让父加载器去加载,若父加载器不为空的话
c = parent.loadClass(name, false);
} else {
//若父加载器为空,则通过Bootstrap Classloader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
....
}
if (c == null) {
// 如果父加载器、启动类加载器,都没有找到
// 才调用findclass,从自己的地盘,类路径加载
c = findClass(name);
....
}
}
.......
return c;
}
}
}
上面的代码中,与流程无关的代码直接省略了。
概况一下,loadClass的大致流程如下:
-
执行findLoadedClass(String)去缓存检测这个class是不是已经加载过了。 在加载器的缓存中,按照类名,查找已经加载好的现成类。如果找到,直接返回了。
-
如果没有找到,执行parent父加载器的loadClass方法。通过父加载器加载。注意:这里不是去自己的地盘查找class文件,而是优先通过父加载器加载。这点,非常重要。具体原因,后面会讲到。
-
若父加载器为空,则通过Bootstrap Classloader 加载。
-
如果父加载器、启动类加载器,都没有找到,才调用findclass,从自己的地盘,自己的类路径去查找字节码文件,通过findClass(String)查找。
抽丝剥茧之后,loadClass 方法的源代码其实也不过如此,比较简单。
但是,这里已经有两个疑问:
(1)一个加载器的parent是谁?
(2)为什么优先从parent加载,而不是从自己的地盘加载?
欲知后事如何,请看下回分解。
源码:
代码工程: classLoaderDemo.zip
下载地址:下面链接获取