JVM的类加载会经过如下图几个过程:加载、验证、准备、解析、初始化、使用、卸载
加载过程
加载
从硬盘、网络、数据库等读取java类字节码字节流的过程。
验证
校验字节码文件格式的正确性,当然如果可以保证字节码文件一定正确的话,可以使用-Xverfity:none来关闭该过程
准备
给类的静态变量分配内存并进行初始化操作,比如整型会初始化为0,对象会初始化为null
解析
将符号引用替换为直接引用
初始化
对类的静态变量初始化为指定的值,执行静态代码块.
双亲委派机制
几种类加载器
上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器:
- 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
- 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
- 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
- 自定义加载器:负责加载用户自定义路径下的类包
如下代码,输出相应的类加载器
public class TestJDKClassLoader {
public static void main(String[] args) {
//启动类加载器是C++语言实现的吗,所以输出的是null
System.out.println(String.class.getClassLoader());
System.out.println(sun.text.resources.cldr.aa.FormatData_aa.class.getClassLoader().getClass().getName());
System.out.println(TestJDKClassLoader.class.getClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
}
}
运行结果如下
null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader@c387f44
sun.misc.Launcher$AppClassLoader
启动类加载器是C++语言实现的吗,所以输出的是null
自定义类加载器
自己定义类加载器呢?这主要有两种方式
(1)遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
public class MyClassLoaderTest extends ClassLoader{
private String classPath;
public MyClassLoaderTest(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name)throws Exception{
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath+"/"+name+".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
//不破坏双亲委托机制,则只需要重写findClass即可
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name,data, 0, data.length);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
throw new ClassNotFoundException();
}
}
//打破了双亲委派机制
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();
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
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;
}
}
public static void main(String[] args) throws Exception{
MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/Test");
Class clazz = classLoader.loadClass("com.suibibk.jvm.User");
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
我们先把loadClass方法注释,然后执行代码发现输出结果如下:
sun.misc.Launcher$AppClassLoader
为什么不是打印自定义加载器呢,首先让我们看一下双亲委派机制的概念:当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。
我们上面是注释了loadClass方法来测试的,所以是用的第一种遵守双亲委派模型,然后看一下默认的loadClass方法。
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 {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
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;
}
}
可以很清楚的看到逻辑,先判断parent是否存在,很明显此时我们的parent:AppClassLoader是存在的,并且我们的classPath目录下也有User.class,所以就直接加载了。所以我们应该要把classpath目录下的User.class删除,然后添加到D:/Test目录下,然后就可以正常输出了。
打破双亲委派机制,加载String
我们把loadClass方法放开,然后main方法执行下面逻辑
MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/Test");
Class clazz = classLoader.loadClass("java.lang.String");
System.out.println(clazz.getClassLoader().getClass().getName());
我们在Test目录把String.class字节码加上,结果会怎么样呢,理论上应该是可以加载的把!
结果如下
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)
因为java虚拟机堆相关目录有保护作用,禁止加载 Prohibited package name: java.lang。
打破双亲委派机制,加载自定义类
我们把loadClass方法放开,然后main方法执行下面逻辑
MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/Test");
Class clazz = classLoader.loadClass("com.suibibk.jvm.User");
System.out.println(clazz.getClassLoader());
我们把Test目录把User.class字节码加上,结果会怎么样呢,这次理论上应该是可以加载的把,毕竟跟上面不同,已经是自定义的包了,运行结果如下:
java.io.FileNotFoundException: D:\Test\java\lang\Object.class (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
因为所有自定义类的父类默认都是Object,但是Test目录下没有Object.class,所以也不能加载,打破双亲委派机制还是蛮麻烦的,所以可以在loadClass方法做一些过滤
有些类用父类加载,有些类打破委派。比如在loadClass加上如下代码:
...
if (c == null) {
if("java.lang.Object".equals(name)) {
System.out.println("111");
c = super.loadClass(name, false);
}
...
那就可以正常执行啦。
为什么要设计双亲委派机制?
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
好了具体可以参考:JVM:Java类加载机制