深入理解类加载器
类加载的过程与作用已经在上面阐述了
类缓存
在介绍类加载器之前,先来说说 JVM
的缓存机制。
我们这里所讨论的不是某个方法中所需要用到的变量缓存,而是类缓存。我们知道标准的 JAVASE
类加载器可以按照要求来查找类,一旦某个类被加载到类加载器中,它将维持加载(也就是缓存)一段时间。在有方法被调用的时候,一般都是直接在缓存中直接取出该类并创建它的一个副本引用。
需要注意的是,虽然有类缓存的存在可以使性能提高,但 JVM
中的垃圾回收机制还是会回收这些 Class
对象的。
<font color=#9F79EE>一般来说,类都只被加载了一次!</font>
一个简单的测试 demo
public class demo01 {
public static void main(String[] args) throws ClassNotFoundException {
// 当A对象被创建的时候 先会执行静态代码块,再实执行A的构造方法
A a = new A();
System.out.println(a.width);
// 输出顺序为 创建初始化类A--> 创建A对象 --> width = 300
A a2 = new A();
// 只会打印 创建A对象,不会再加载第二次
}
}
class A {
public static int width = 100;
public static final int MAX = 200;
static {
System.out.println("静态初始化类A");
width = 300;
}
public A(){
System.out.println("创建A对象");
}
}
java.class.ClassLoader类介绍
一句话简单介绍一下 ClassLoader 的具体作用:就是将 .class
文件加载到jvm虚拟机中去。
类加载器的层次结构(树状结构)
先介绍一下 JAVA
中的四个重要的类加载器,加载器按照父级到子级的顺序进行排序。可以通过 getParent()
获取其父类的加载器,这个在下面会详细介绍。
引导类加载器(bootstrap class loader)
- 它用来加载
Java
的核心库( <font color=#B3B3B3>Java_Home/jre/lib/rt.jar</font> ,或者 sun.boot.class.path
路径下的内容),使用原生代码写的 --> C++
,并不继承 java.lang.ClassLoader
,它本身就是 JVM
虚拟机的一本分,它并不属于一个 JAVA
类,所以无法在 JAVA
代码中获取到它的引用。
- 引导类加载器的主要功能:加载扩展类和应用程序类加载器,并制定它们的父类加载器。
扩展类加载器(extensions class loader)
- 用来加载
JAVA
的扩展库(Java_Home/jre/ext/*.jar
,或者 java.ext.dirs
路径下的内容)。如果程序中没有指定该系统属性(-Djava.ext.dirs = sss/lib)
, JAVA
虚拟机的实现会提供一个扩展库目录(默认是 $JAVA_HOME/lib/ext
)。该类加载器在此目录里面查找并加载 JAVA
类
- 由
sun.misc.Launcher$ExtClassLoader
实现
来看一下系统默认指定的 java.ext.dirs
路径
public class testLoader
{
public static void main(String[] args)
{
System.out.println(System.getProperty("java.ext.dirs"));
}
}
应用程序类加载器(appliaction class loader)
-
它根据 JAVA
应用的类路径(classpath
java.class.path
)来加载。
一般来说 Java
应用的类都是由它来完成加载的(自己编写的 JAVA
类)。
-
由sun.misc.Launcher$AppClassLoader
实现
public static void main(String[] args)
{
System.out.println(ClassLoader.getSystemClassLoader());
// 执行结果为: sun.misc.Launcher$AppClassLoader@addbf1
}
自定义类加载器
-
开发人员可以通过继承 java.lang.ClassLoader
类的方式实现自己的类加载器,以满足一些特殊的要求。
在自定义类加载器中,开发人员应该注意要实现 双亲委派机制 ,在下面会详解。我们先来看一下代码怎么写
// 委托给父类加载器 这时候进入系统的加载器中,会一层一层的往上送
ClassLoader parent = this.getParent();
c = parent.loadClass(name);
关于层次结构说明
- 引导类加载器是有底层代码
C++
来实现的,它没有继承 java.lang.ClassLoader
接口
<font color=#9F79EE>当使用引导类加载器的时候,因为这是所有加载器的父类,为了安全所以,是无法打印该类的名称的,返回的是 null 。</font>
- 扩展类加载器(
`extensions class loader
),应用程序类加载器( appliaction class loader
),自定义类加载器 均是用 JAVA
实现的,都需要继承 java.lang.ClassLoader
接口
- 子父类关系 自定义加载器 --> 应用程序类加载器(
appliaction class loader
) --> 扩展类加载器( extensions class loader
) --> 引导类加载器( bootstrap class loader
)
类加载器的代{过}{滤}理模式
为什么这里的标题是 类加载器的代{过}{滤}理模式 呢,其实这里只是一个笼统的概括,在 JVM
的类加载器设计模式中,大多的都是 代{过}{滤}理模式
双亲委托机制就是代{过}{滤}理模式中的一种
双亲委托机制
- 就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最高的爷爷辈的,如果父类加载器可以完成类的加载任务,则成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
自己拿到东西后看都不看就往父级丢(典型的啃老,老一辈的有了先花掉)。
- 双亲委托机制是为了保证
JAVA
核心库的类型安全。[这种机制保证了不会出现用户能自定义 java.lang.Object
类的情况]。 保证核心类无法被用户定义!
- 类加载器除了用于加载类,也是安全的最基本的屏障。
双亲委托机制只是代{过}{滤}理模式中的一种
自定义类加载器
import java.io.*;
/**
* 自定义文件系统类加载器
* Created by BF on 2017/9/18.
*/
public class FileSystemClassLoader extends ClassLoader {
// 传进一个目录
// d://myjava/com/wiceflow/JVM/demo01.class --> com.wiceflow.JVM.demo01
// 指定根目录
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
// 重写父类方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
Class<?> c = findLoadedClass(name);
// 应该先查询有没有加载过这个类。如果有加载过,则直接返回加载好的类,如果没有,则加载新的内容
if (c!=null){
return c;
}else {
// 交给父类去加载,获得父类加载器
// 因为自定义加载器的上一层次是app类加载器,所以这里获得到的应用程序类加载器appliaction class loader
try{
// 委托给父类加载器 这时候进入系统的加载器中,会一层一层的往上送
ClassLoader parent = this.getParent();
c = parent.loadClass(name);
}catch (Exception e){
System.out.println("父级异常什么都不做!");
}
if (c!=null){
return c;
}else{
// 自定义IO流读取
byte[] classData = getClassData(name);
if (classData == null){
// 如果还是找不到,就手动抛出异常
throw new ClassNotFoundException();
}else{
c = defineClass(name,classData,0,classData.length);
return c;
}
}
}
}
/**
* 自定义字节流读取器
* @param name com.wiceflow.JVM.demo01 --> d://myjava/com/wiceflow/JVM/demo01.class
* @return
*/
private byte[] getClassData(String name){
// 转换路径
String path = rootDir + "/" + name.replace('.','/') + ".class";
// 字节输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 文件流
InputStream is = null;
try {
is = new FileInputStream(path);
// 定义接收数组
byte[] buffer = new byte[1024];
int temp = 0;
while ((temp=is.read(buffer))!=-1){
baos.write(buffer,0,temp);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return null;
}finally {
if (is!=null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
双亲委派机制请求过程
- 首先检查请求请求的类型是否已经被这个类装载其装载到命名空间中了,如果已经装载,直接返回。
- 委派类加载请求给父类加载器,如果父类加载器能够完成加载,则返回父类加载器加载的Class实例。
这里委派个父级处理的时候,如果父级加载器一层一层的往上找都没有对应的,则会在其自身抛出异常终止程序运行,而不是自定义加载器抛出异常,所以我们要进行 try/catch
操作,防止父类抛异常终止操作。
- 调用本类加载器的
findClass(...)
方法,试图获取对应的字节码,如果获取到,则调用defindClass(...)
导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(...)
,loadClass(...)
转抛异常,终止加载过程
-
<font color = red>注意: 被两个类加载器的同一个类,JVM不认为是相同的类,它们的HashCode不同!</font>
FileSystemClassLoader loader1 = new FileSystemClassLoader("D:/myjava");
FileSystemClassLoader loader2 = new FileSystemClassLoader("D:/myjava");
Class<?> c1 = loader1.loadClass("com.wiceflow.HelloWord");
Class<?> c2 = loader1.loadClass("com.wiceflow.HelloWord");
Class<?> c3 = loader2.loadClass("com.wiceflow.HelloWord");
System.out.println(c1.hashCode()==c2.hashCode()); // true
System.out.println(c1.hashCode()==c3.hashCode()); // false
-
<font color = blue>注意:加密后的 Class
文件,正常的类加载器无法加载,会报 classFormatError
异常! 这时候就要用到自己定义类加载器来加载,在其中先解密 </font>
classFormatError 异常还有可能是被重复加载导致的,例如在使用 tomcat 时,启动了两次就回出现这个异常。
了解另外两个类加载器
线程上下文类加载器
JAVA
提供了很多服务提供者接口(Service Provider Interface
,SPI
),允许第三方为这些接口提供实现。SPI 实现的 JAVA
类一般是由系统类加载器来加载的。
JAVA
默认的线程上下文类加载器是系统类加载器(AppClassLoader
)
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader" );
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
Tomcat 服务器额类加载器
-
一切都是为了安全
Tomcat 不能使用系统默认的类加载器
- 如果
Tomcat
跑你的 WEB
项目使用系统的类加载器那是相当危险的,你可以直接是无忌惮的操作系统的各个目录了。
- 对于运行在
JavaEE
容器中的 Web
应用来说,类加载器的实现方式与一般 JAVA
应用有所不同
- 每个
Web
应用都有对应的一个类加载器实例。该类加载器也使用代{过}{滤}理模式(不同于前面所加的双亲委派机制),所不同的是它首先尝试去加载某个类,如果找不到再代{过}{滤}理给父类加载器。这与一般的加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。
-
为了安全 Tomcat
需要实现自己的类加载器
- 我可以限制你只能把类写在指定的地方,否则我不给你加载
eg:WEB
文件夹