java类加载机制主要包括:加载—>验证—>准备—>解析—>初始化—>使用—>卸载,而类加载器的作用主要发生在加载阶段。
加载阶段,类加载器主要做了但不限于如下三件事:
1、通过一个类的全限定名获取这个类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
因为每一个类加载器,都拥有一个独立的类名称空间,所以一个类的唯一性由加载它的类加载器和这个类的本身决定(一个类由类的全限定名和一个类加载器的实例ID作为唯一标识)。比较两个类是否相等(包括Class对象的equals()、isAssignableFrom()、isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。
JVM中提供了三层的ClassLoader:
其实还有一个类加载器,就是用户自定义类加载器,CustomClassLoader
如果有一个我们写的Hello.java编译成的Hello.class文件,它是如何被加载到JVM中的呢?别着急,请继续往下看。
1、源码分析
打开“java.lang”包下的ClassLoader类,然后将代码翻到loadClass方法:
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
其实这段代码已经很好的解释了双亲委派机制,下面这张图来描述一下上面这段代码的流程:
从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。
1、不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。
2、父类中同理也会先检查自己是否已经加载过,如果没有再往上。
注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。
3、直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层。
4、如果没有任何加载器能加载,就会抛出ClassNotFoundException。
5、“父委派模型”是怎么工作的?
举个例子,当前有个Test.class,需要加载rt.jar中的java.lang.String,那么加载的流程如下图所示,整体的加载流程是向上委托父加载器完成的。
如果整个链路中,父加载器都没有加载这个类,且无法加载这个类时,才会由Test.class所在的加载器去加载某个类(例如希望加载开发人员自定义的类 Test2.class)。
这种设计有个好处是,如果有人想替换系统级别的类:String.java,篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
“父委派模型”保证了系统级别的类的安全性,使一些基础类不会受到开发人员“定制化”的破坏。
如果没有使用父委派模型,而是由各个类加载器自行加载的话,如果开发人员自己编写了一个称为java.lang.String的类,并放在程序的ClassPath中,那系统将会出现多个不同的String类, Java类型体系中最基础的行为就无法保证。应用程序也将会变得一片混乱。
双亲委派机制的作用:
1、防止重复加载同一个.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class
不能被篡改。通过委托方式,不会去篡改核心.clas
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
package java.lang;
public class String {
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
在另外的程序中加载 String 类,看看加载的 String 类是 JDK 自带的 String 类,还是我们自己编写的 String 类
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类
为什么呢?由于我们定义的String类本应用系统类加载器,但它并不会自己先加载,而是把这个请求委托给父类的加载器去执行,到了扩展类加载器发现String类不归自己管,再委托给父类加载器(引导类加载器),这时发现是java.lang包,这事就归引导类加载器管,所以加载的是 JDK 自带的 String 类
2、举例 2 :
在我们自己的 String 类中整个 main() 方法
package java.lang;
public class String {
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
由于双亲委派机制找到的是 JDK 自带的 String 类,但在引导类加载器的核心类库API里的 String 类中并没有 main() 方法
3、举例 3:
在 java.lang 包下整个 ShkStart 类 (自定义类名)
package java.lang;
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
}
}
会报错,出于保护机制,java.lang 包下不允许我们自定义类
通过上面的例子,我们可以知道,双亲机制可以
(1)避免类的重复加载
(2)保护程序安全,防止核心API被随意篡改