类加载器

一、类加载器详解

首先理解一个概念,就是虚拟机实例。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。如果同一台计算机上同时运行三个Java程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。
类加载器是负责将可能是网络上、也可能是磁盘上的class文件加载到内存中(方法区), 并为其生成对应的java.lang.class对象。
java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制.JVM中用来完成上述功能的具体实现就是类加载器.类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例.每个实例用来表示一个java类.通过该实例的newInstance()方法可以创建出一个该类的对象.
一旦一个类被载入JVM了,同一个类就不会被再次加载。那么怎样才算是同一个类?在JAVA中一个类用其全限定类名(包名和类名)作为其唯一标识,但是在JVM中,一个类用其全限定类名和加载它的类加载器作为其唯一标识。也就是说,在JAVA中的同一个类,如果用不同的类加载器加载,则生成的class对象认为是不同的。
JAVA类装载方式,有两种:
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 2.显式装载, 通过class.forname()等方法,显式加载需要的类

当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构

1、启动类加载器BootstrapClassLoader:
是嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负载加载JAVA_HOME/lib下的类库(rt.jar,如、List、Thread、Object等的加载),启动类加载器无法被应用程序直接使用

2、扩展类加载器Extension ClassLoader:
该加载器器是用JAVA编写,且它的父类加载器是Bootstrap,是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类库。开发者可以自己使用扩展类加载器。

我们知道java中系统属性java.ext.dirs指定的目录由ExtClassLoader加载器加载,如果程序中没有指定该系统属性(-Djava.ext.dirs=sss/lib)那么该加载器默认加载$JAVA_HOME/lib/ext目录下的所有jar文件,通过程序来看下系统变量java.ext.dirs所指定的路径:

public class Test
{
    public static void main(String[] args)
    {
        System.out.println(System.getProperty("java.ext.dirs"));
    }
}

执行结果:

C:\Program Files (x86)\Java\jdk1.6.0_43\jre\lib\ext;C:\Windows\Sun\Java\lib\ext

3、系统类加载器App ClassLoader:
系统类加载器,也称为应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件(包括开发自己实现的类)。它的父加载器为Ext ClassLoader。

public class Test
{
    public static void main(String[] args)
    {
        System.out.println(ClassLoader.getSystemClassLoader());
    }
}

执行结果:

sun.misc.Launcher$AppClassLoader@addbf1

程序中的方法是返回委托的系统类加载器,通过执行结果,可以知道,系统类加载器是通过sun.misc.Launcher$AppClassLoader实现的。

上述三种类加载器的层次关系如下:
在这里插入图片描述
为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型。

我们可以通过程序来验证下:

    public static void main(String[] args)
    {
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
    }

执行结果:


sun.misc.Launcher$ExtClassLoader@42e816

在这里可以看到,Application ClassLoader的父加载器确实是ExtClassLoader。

我们在往上走一层,如果猜想没错的话,ExtClassLoader的父加载器应该是BootStrap ClassLoader

public static void main(String[] args)
    {
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
    }

执行结果:

null
这里不是说ExtClassLoader没有父加载器,而是因为Bootstrap ClassLoader使用C++写的。
在这里插入图片描述
使用代码观察一下类加载器:

package com.wang.test;

public class TestClassLoader {

    public static void main(String[] args) {
        ClassLoader loader = TestClassLoader.class.getClassLoader();
        System.out.println(loader.toString());
        System.out.println(loader.getParent().toString());
        System.out.println(loader.getParent().getParent());
    }
}

观察打印结果:

sun.misc.Launcher$AppClassLoader@500c05c2
sun.misc.Launcher$ExtClassLoader@454e2c9c
null

第一行打印的是应用程序类加载器(默认加载器),第二行打印的是其父类加载器,扩展类加载器,按照我们的想法第三行应该打印启动类加载器的,这里却返回的null,原因是getParent(),返回null的话,就默认使用启动类加载器作为父加载器.
UML类图:
在这里插入图片描述
除了启动类加载器Bootstrap ClassLoader,其他的类加载器都是ClassLoader的子类。

二、双亲委派模型

如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是把这个请求转交给父类加载器去完成。每一个层次的类加载器都是如此。因此所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载。委派的好处就是避免有些类被重复加载。

双亲委派的实现比较简单,我们来看下源码:

protected synchronized Class<?> loadClass(String paramString, boolean paramBoolean)
 2     throws ClassNotFoundException
 3   {
       //检查是否被加载过
 4     Class localClass = findLoadedClass(paramString);
       //如果没有加载,则调用父类加载器
 5     if (localClass == null) {
 6       try {
           //父类加载器不为空
 7         if (this.parent != null)
 8           localClass = this.parent.loadClass(paramString, false);
 9         else {
             //父类加载器为空,则使用启动类加载器
10           localClass = findBootstrapClass0(paramString);
11         }
12       }
13       catch (ClassNotFoundException localClassNotFoundException)
14       {
           //如果父类加载失败,则使用自己的findClass方法进行加载
15         localClass = findClass(paramString);
16       }
17     }
18     if (paramBoolean) {
19       resolveClass(localClass);
20     }
21     return localClass;
22   }

这种模式的主要目的是为了安全,比如用户自己定义了一个类List,定义了java.lang.util包,放在,该包下,在加载该类时,会先使用启动类加载器加载,而启动类加载的rt.jar中包含了List,就好抛异常,试想如果jdk提供的基本包都能被随意改写、覆盖,那就存在很多安全隐患。
再比如java.langObject,它存放在\jre\lib\rt.jar中,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了.
为什么要使用这种双亲委托模式呢?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。

定义自已的ClassLoader

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

自定义类加载器
对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:

如果不想打破双亲委派模型,那么只需要重写findLoadClass方法即可
如果想打破双亲委派模型,那么就重写整个loadClass方法

Class.forname()与ClassLoader.loadClass()

Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize的值为 true,loader的值为当前类的类加载器。如: Class.forName(“com.wang.HelloWorld”);
 ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.
  如:
  ClassLoader cl=…;
  cl.loadClass(“com.wang.HelloWorld”);

文献:https://www.jianshu.com/p/478a78c04240
https://www.cnblogs.com/dongguacai/p/5879931.html
https://www.cnblogs.com/fingerboy/p/5456371.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值