继续接着上一篇的:https://blog.csdn.net/xiao1_1bing/article/details/81120787
1、类加载器
寻找类加载器,先来一个小例子
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
//项目src目录下的appClassLoader
System.out.println(loader);
//JDK\jre\lib\ext目录下的ExtClassLoader
System.out.println(loader.getParent());
//JDK\jre\lib目录下的BootStrapClassLoader
System.out.println(loader.getParent().getParent());
}
}
运行后,输出结果:
sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null
从上面的结果可以看出,并没有获取到ExtClassLoader
的父Loader,原因是Bootstrap Loader
(引导类加载器)是用C语言实现的(native方法),找不到一个确定的返回父Loader的方式,于是就返回null。
这几种类加载器的层次关系如下图所示:
注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。
站在Java虚拟机的角度来讲,只存在两种不同的类加载器:启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;所有其它的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader
,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
启动类加载器:Bootstrap ClassLoader
,负责加载存放在JDK\jre\lib
(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath
参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader
加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器:Extension ClassLoader
,该加载器由sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext
目录中,或者由java.ext.dirs
系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader
,该类加载器由sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)所指定的类(项目目录下的),开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的appClassLoader类加载器(自己写的类最主要的加载器)。
第一个图是没有编译的Main.java文件,第二个图是编译之后,会出现一个单独的out包,会在out下的项目包中生成Main.class文件。所以说自己写的类在项目目录下就会通过AppClassLoader进行加载,而在上面import导入的以java开头的为BootstrapClassLoader和以javaw开头的为ExtClassLoader,还有一种就是xx.class虽然是自己写的类但是不在项目目录下,就需要用到自定义加载器,而它需要继承ClassLoader
类,下面会详细讲到。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
- 1、在执行非置信代码之前,自动验证数字签名。
- 2、动态地创建符合用户特定需要的定制化构建类。
- 3、从特定的场所取得java class,例如数据库中和网络中。
JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效,因为缓存中是老的版本。
2、类的加载
类加载有三种方式:
- 1、命令行启动应用时候由JVM初始化加载
- 2、通过Class.forName()方法动态加载
- 3、通过ClassLoader.loadClass()方法动态加载
1的例子
public class Main01 {
public static void main(String arg[]){
}
}
public class Main02 {
String ss;
}
主函数main什么也不做,没编译之前目录文件如图一,编译之后如图二,虽然Main01由于有main函数会被主动加载,但是Main02没有东西调用,它还是被加载到内存中,这些都是JVM进行的预加载。
2,3,的例子:
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = loaderTest.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行静态初始化块
loader.loadClass("Test2");
//使用Class.forName()来加载类,默认会执行静态初始化块
//Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
//Class.forName("Test2", false, loader);
}
}
demo类
public class Test2 {
static {
System.out.println("静态初始化块执行了!");
}
}
分别切换加载方式,会有不同的输出结果。
Class.forName()和ClassLoader.loadClass()区别
Class.forName()
:将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;ClassLoader.loadClass()
:只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。Class.forName(name, initialize, loader)
带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
3、双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制:
- 1、当
AppClassLoader
加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader
去完成。 - 2、当
ExtClassLoader
加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader“`去完成。 - 3、如果
BootStrapClassLoader
加载失败(例如在$JAVA_HOME/jre/lib
里未查找到该class),会使用ExtClassLoader
来尝试加载; - 4、若ExtClassLoader也加载失败,则会使用
AppClassLoader
来加载,如果AppClassLoader
也加载失败,则会报出异常ClassNotFoundException
。
ClassLoader源码分析:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{ //线程安全
synchronized (getClassLoadingLock(name)) {
// 首先判断该类型是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载,进行递归。
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,并通过该方法调用本地方法native Class findBootstrapClass(String name)方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派模型意义:
- 系统类防止内存中出现多份同样的字节码(一个类同时被多个类加载器加载),保证了只出现一个
- 保证Java程序安全稳定运行
6、自定义类加载器
通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。当我们在使用中有时要使用一个新类,知道它的文件路径和它的文件名字,我们要把它调入系统并使用它或者说一个类文件已经被加密处理,类文件里面的内容是我们加密后的密文,不能直接使用,只能是对文件内容解密后才能使用,就可以用类库加载器ClassLoader,把类文件当做数据流读入到一个byte[]中,对这个byte[]进行解密处理后(没加密当然就不用做这步了),再通过 byte[] 生成一个类,并加载到系统中(内存中)。
自定义类加载器一般都是继承自ClassLoader
类,从上面对loadClass
方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:
将上面生成的Main.class拷贝到D盘目录下,将之前的所有全部删除,包括out包,这个时候新建一个Java文件,文本目录是图一。编译之后如图二,但是它会去外部目录加载D:\Main.calss,项目目录就不会出现该Class。
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//defineClass将byte[]转化为指定类名的类并进行类加载
return defineClass(name, classData, 0, classData.length);
}
}
//将Class文件转化为二进制数据
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
//主函数
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("D:\\");
Class<?> testClass = null;
try {
//loadClass()会调用findClass(),这个是继承自ClassLoader类调用的
testClass = classLoader.loadClass("Main");
//Class实例化为Object
Object object = testClass.newInstance();
ClassLoader loader=object.getClass().getClassLoader();
//自定义ClassLoader
System.out.println(loader);
//AppClassLoader
System.out.println(loader.getParent());
//ExtClassLoader
System.out.println(loader.getParent().getParent());
//BootstrapClassLoader
System.out.println(loader.getParent().getParent().getParent());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
输出:
MyClassLoader@1540e19d
sun.misc.Launcher$AppClassLoader@75b84c92
sun.misc.Launcher$ExtClassLoader@14ae5a5
null
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:
- 1、这里传递的文件名需要是类的全限定性名称(即
com.paddx.test.classloading.Test
格式的)因为 defineClass 方法是按这种格式进行处理的。 - 2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
- 3、这类Test 类本身可以被
AppClassLoader
类加载,因此我们不能把com/paddx/test/classloading/Test.class
放在项目路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader
加载,而不会通过我们自定义类加载器来加载。