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++语言实现的吗,所以输出的是nullSystem.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());}}
运行结果如下
nullsun.misc.Launcher$ExtClassLoadersun.misc.Launcher$AppClassLoader@c387f44sun.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即可@Overrideprotected 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 blocke.printStackTrace();throw new ClassNotFoundException();}}//打破了双亲委派机制protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> 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 statssun.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 loadedClass<?> 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 statssun.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.langat 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类加载机制
