深入JVM内核(六)——类装载器

由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。

一、class装载验证流程

1、加载

2、链接

      连接又可以分为三步:

       (1)、验证

       (2)、准备

       (3)、解析

3、初始化

(一)、class装载验证流程 -加载

1、装载类的第一个阶段

2、取得类的二进制流(通过不同方式取得二进制流的内容)

3、转为方法区数据结构 

4、在Java堆中生成对应的java.lang.Class对象

(二)、class装载验证流程 -链接 验证

1、链接 -> 验证 : 是连接的第一步

    目的:保证Class流的格式是正确的

              (1)文件格式的验证

                       a.是否以0xCAFEBABE开头(class文件都是以这开头的,不是则不是正确的)

                       b.版本号是否合理

             (2)元数据验证(看基本的数据是否合理,就是检查基本的语义是否合理,是否正确的)

                      a.是否有父类(比如一个父类或方法不存在,等,都是不真孤儿)

                      b.继承了final类?

                      c.非抽象类实现了所有的抽象方法

            (3)字节码验证 (很复杂)

                     a.运行检查

                     b.栈数据类型和操作码数据参数吻合

                     c.跳转指令指定到合理的位置

          (4).符号引用验证

                     a.常量池中描述类是否存在

                     c.访问的方法或字段是否存在且有足够的权限

所有的检查都是大部分,不可能是全部都能验证全,但如果不经过验证则一定是错误的

2、链接 -> 准备

            分配内存,并为类设置初始值 (方法区中)

                   public static int v=1;

                   在准备阶段中,v会被设置为0

                   在初始化的<clinit>中才会被设置为1(比如在static和final则是例外,在准备阶段则已经初始化置为1)

                   对于static final类型,在准备阶段就会被赋上正确的值

                   public static final  int v=1;

3、链接 -> 解析

             符号引用替换为直接引用(符号不能真正被使用,只是被引用,直接真正的引用则是通过指针与地址)

                      符号引用:字符串 引用对象不一定被加载

                      直接引用:指针或者地址偏移量 引用对象一定在内存

(三)、class装载验证流程 – 初始化

1、执行类构造器<clinit>(clinit表示类初始化)下面则是类初始化就会被初始化

            static变量 赋值语句

            static{}语句

2、子类的<clinit>调用前保证父类的<clinit>被调用

3、<clinit>(类初始化)是线程安全的,一个类初始化时其他类则等待。

(四)、class装载验证流程-总结

Java.lang.NoSuchFieldError错误可能在什么阶段抛出

二、什么是类装载器ClassLoader

1、ClassLoader是一个抽象类

2、ClassLoader的实例将读入Java字节码将类装载到JVM中

3、ClassLoader可以定制,满足不同的字节码流获取方式(可以从网络中加载class文件,从文件中加载等等方式)

4、ClassLoader负责类装载过程中的加载阶段(他只负责把类读进来)

三、JDK中ClassLoader默认设计模式

1、ClassLoader的重要方法

public Class<?> loadClass(String name) throws ClassNotFoundException

         载入class并返回一个Class

protected final Class<?> defineClass(byte[] b, int off, int len)--------->byte[]指的是二进制流的class文件

         定义一个类,不公开调用   

protected Class<?> findClass(String name) throws ClassNotFoundException

      findClass时  loadClass回调该方法,自定义ClassLoader的推荐做法,去重载这个方法

protected final Class<?> findLoadedClass(String name)

        寻找已经加载的类,已经加载则不会二次进行加载

(一)、JDK中ClassLoader默认设计模式 – 分类

1、BootStrap ClassLoader (启动ClassLoader)

2、Extension ClassLoader (扩展ClassLoader)

3、App ClassLoader (应用ClassLoader/系统ClassLoader)这个classloader去加载跟我们应用相关的class。

4、Custom ClassLoader(自定义ClassLoader)

5、每个ClassLoader都有一个Parent作为父亲

(二)、JDK中ClassLoader默认设计模式 – 协同工作

JDK中classloader括谱图:

工作顺序如下图:

自底向上检查类是否已经加载;跟我们应用相关的,也是最主要的则是App ClassLoader这个加载器,我们自己写的类都是加载在这个地方的。什么是自底向上检查类是否已经加载呢?就是当我们去找一个类的时候,会在当前class类中去找。因此他会在App ClassLoader中去找我们写的类中是否已经加载这个类。如果自己写的class没没有做找到,他不会去做加载。他会把这个请求传给他的父类,也就是Extension ClassLoader.(扩展classLoader),徐问他是否这个class类。他也是一样,会在当前自己的class类中去找,找到则返回,否者继续去他的父类中去找。如果最近没有找到,则会去加载所需要的这个类。

而,加载则是相反的,可以这么理解,他是自底向上的询问,那么到了最顶端,则开始自上而下的去加载:他不会再在某个阶段的classloader找不到时就去加载这个类。

他是会去由启动ClassLoader去加载,加载成功则返回,如果加载失败;,则会去扩展classLoader去加载,如果也失败,则会去让AppclassLoader去加载

r

rt.jar这个是启动classLoader的架包,他也是系统的核心类.所以启动classLoader,则区加载这个架包中的所有类。当然在jvm启动时可以去配置后面-Xbootclasspath这个参数,去启动这个类的路径,那么这个里面的class也会存在启动classLoader里面的。

我们这个路径下的jar包都会被这个扩展ClassLoader所加载。

而我们classpath下载类都是被appClassLoader所加载。

下面则是classLoader代码实现

(1)他先去找,找不到,则去加载。

2、下面案例测试加载顺序:

上面则可以看出他是加载在appclassloader中的,因为他们是在我们创建的findorder包中的,下面演示把他们放在如下图的右上角中位置,这个位置可以是当前系统中任何位置,他跟我们classpth是没关系的。

然后通过下面来测试他们两个的当执行main函数时,他们是如何找到这两个类的。

1、直接运行上面代码:我们可以发现他是加载在appclassloader中的

          I am in apploader

2、加上参数 -Xbootclasspath/a:D:/tmp/clz   (当我们加上这个参数,指定他的加载位置及上面右上角的位置时,我们可以看到如下:)

          I am in bootloader

         此时AppLoader中不会加载HelloLoader(因为他找不到这个类了,然后他由当前往上找也没有找到,那他就开始加载,以为在bootloader指定了这个类,所以他加载成功)

                    也就是 I am in apploader 在classpath中却没有加载

                   因此由上: 说明类加载是从上往下的

下面强制它在apploader中加载

public static void main(String args[]) throws Exception {
	ClassLoader cl=FindClassOrder2.class.getClassLoader();
	byte[] bHelloLoader=loadClassBytes("geym.jvm.ch6.findorder.HelloLoader");
	Method md_defineClass=ClassLoader.class.getDeclaredMethod("defineClass", byte[].class,int.class,int.class);
	md_defineClass.setAccessible(true);
	md_defineClass.invoke(cl, bHelloLoader,0,bHelloLoader.length);
	md_defineClass.setAccessible(false);
	
	HelloLoader loader = new HelloLoader();
	System.out.println(loader.getClass().getClassLoader());
	loader.print();
}

通过反射去调用这个方法。

-Xbootclasspath/a:D:/tmp/clz  对于这个程序依然用这个参数。运行如下面结果:

I am in apploader   这是因为之前我们强行的让Appclassloader去加载这个类。

因为开始在查找类的时候,先在底层的Loader查找,是从下往上的。Apploader能找到,就不会去上层加载器加载

5、能否只用反射,仿照上面的写法,将类注入启动ClassLoader呢?

(三)、JDK中ClassLoader默认设计模式 – 问题

上面介绍的加载过程其实是jdk中的双亲模式,因为我们看到每个clasLoader都会有一个parent,一个父亲,但是这个父亲不是他的super的class,不是他的超类,而他的内部会聚集一个classloader类,作为他的parent,当每次加载或者去找类的时候,会去委托这个parentq去做一些事情,但是这个模式存在一个小问题。 顶层ClassLoader,无法加载底层ClassLoader的类,也就是说在启动的这个classLoader里面无法生成加载 appClassLoader中的任何一个类。既然无法加载这个类,也就没有办法去生成这个类的实例对象。

双亲模式的问题: 顶层ClassLoader,无法加载底层ClassLoader的类

Java框架(rt.jar)如何加载应用的类?

javax.xml.parsers包中定义了xml解析的类接口 Service Provider Interface SPI 位于rt.jar 即接口在启动ClassLoader中。 而SPI的实现类,在AppLoader。

(四)、JDK中ClassLoader默认设计模式 – 解决(解决双亲问题)

1、Thread. setContextClassLoader()  

          -》上下文加载器

          -》是一个角色

          -》用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题

          -》基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例

static private Class getProviderClass(String className, ClassLoader cl,
        boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
    try {
        if (cl == null) {
            if (useBSClsLoader) {
                return Class.forName(className, true, FactoryFinder.class.getClassLoader());
            } else {
                cl = ss.getContextClassLoader();
                if (cl == null) {
                    throw new ClassNotFoundException();
                }
                else {
                    return cl.loadClass(className); //使用上下文ClassLoader
                }
            }
        }
        else {
            return cl.loadClass(className);
        }
    }
    catch (ClassNotFoundException e1) {
        if (doFallback) {
            // Use current class loader - should always be bootstrap CL
            return Class.forName(className, true, FactoryFinder.class.getClassLoader());
        }
…..

代码来自于 javax.xml.parsers.FactoryFinder 展示如何在启动类加载器加载AppLoader的类

代码中可看书cl就是上下文的加载器

上下文ClassLoader可以突破双亲模式的局限性

6、双亲模式的破坏

         双亲模式是默认的模式,但不是必须这么做

破坏的方式有很多种,如下面:

        1.Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent

         2.OSGi的ClassLoader形成网状结构,根据需要自由加载Class

7、破坏双亲模式例子-  先从底层ClassLoader加载

OrderClassLoader的部分实现

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // First, check if the class has already been loaded
    Class re=findClass(name);
    if(re==null){
        System.out.println(“无法载入类:”+name+“ 需要请求父加载器");
        return super.loadClass(name,resolve);   通过父类委托去加载
    }
    return re;
}
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class clazz = this.findLoadedClass(className);
if (null == clazz) {
    try {
        String classFile = getClassFile(className);
        FileInputStream fis = new FileInputStream(classFile);
        FileChannel fileC = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel outC = Channels.newChannel(baos);
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
         省略部分代码
        fis.close();
        byte[] bytes = baos.toByteArray();

        clazz = defineClass(className, bytes, 0, bytes.length);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
return clazz;
}

下面是使用测试如下

下面是打印结果:

这个例子可以看出由底层开始加载。

如果OrderClassLoader不重载loadClass(),只重载findClass,那么程序输出为

 

四、热替换

1、含义:

      当一个class被替换后,系统无需重启,替换的类立即生效  比如PHP很容易实现,但java很难实现。

      例子: geym.jvm.ch6.hot.CVersionA

public class CVersionA {
	public void sayHello() {
		System.out.println("hello world! (version A)");
	}
}

2、DoopRun 不停调用CVersionA . sayHello()方法,因此有输出:

         hello world! (version A)

3、在DoopRun 的运行过程中,替换CVersionA 为:

public class CVersionA {
	public void sayHello() {
		System.out.println("hello world! (version B)");
	}
}

4、把class文件在系统中给替换掉;替换后, DoopRun 的输出变为

         hello world! (version B)

最后,思考如果通过classloader如何去实现这个功能。

总结:jdk的classloader没有强制大家去使用这个双新模式。因此我们可以重载这个classloader去做很多的事情。比如tomcat、osgi等他们都是破坏双亲模式的产物。因此有这个机制存在才会让我们的程序存在无线的可能性。必须热替换;没有这个机制是无法实现的。因此这个功能给大家带来无限的想像空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平凡之路无尽路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值