类加载器ClassLoader-2

原创
2018/06/02 18:42
阅读数 54

类装载器

大纲:

n  class装载验证流程

n  什么是类装载器ClassLoader

n  JDK中ClassLoader默认设计模式

n  打破常规模式

n  热替换

class装载验证流程:

n  加载

n  链接

–      验证

–      准备

–      解析

n  初始化

加载

n  装载类的第一个阶段

取得类的二进制流[z1] 

转为方法区数据结构

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

链接

验证

n  链接 -> 验证

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

文件格式的验证

是否以0xCAFEBABE开头

版本号是否合理

元数据验证

是否有父类

继承了final类?

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

1,  什么是元数据:

举例:任何文件系统中的数据分为数据和元数据。数据是指普通文件中的实际数据,而元数据指用来描述一个文件的特征的系统数据诸如访问权限、文件拥有者以及文件数据块的分布信息(inode...)等等。在集群文件系统中,分布信息包括文件在磁盘上的位置以及磁盘在集群中的位置。用户需要操作一个文件必须首先得到它的元数据才能定位到文件的位置并且得到文件的内容或相关属性。

2,元数据最大的好处是它使信息的描述和分类可以实现格式化从而为机器处理创造了可能。

3,  元数据概念:元数据是关于数据的数据。在编程语言上下文中,元数据是添加到程序元素如方法、字段、类和包上的额外信息。对数据进行说明描述的数据。

4,元数据的作用: 一般来说,元数据可以用于创建文档(根据程序元素上的注释创建文档),跟踪代码中的依赖性可声明方法是重载,依赖父类的方法),执行编译时检查可声明是否编译期检测),代码分析。如下:

1)编写文档通过代码里标识的元数据生成文档

2)代码分析通过代码里标识的元数据对代码进行分析

3).编译检查通过代码里标识的元数据让编译器能实现基本的编译检查

5、java平台元数据

     注解Annotation就是java平台的元数据,是 J2SE5.0新增加的功能,该机制允许在Java 代码中添加自定义注释,并允许通过反射(reflection),以编程方式访问元数据注释。通过提供为程序元素(类、方法等)附加额外数据的标准方法元数据功能具有简化和改进许多应用程序开发领域的潜在能力,其中包括配置管理、框架实现和代码生成。

Annotation不直接影响程序的语义。然而,开发和部署工具可以读取这些注释,并以某种形式处理这些注释,可能生成其他 Java源程序、XML配置文件或者要与包含注释的程序一起使用的其他组件,从而影响运行状态的程序的语义。注释可以从源代码中读取,从编译后的.class文件中读取,也可以通过反射机制在运行时读取。

1、Annotation具有以下的一些特点: 
元数据以标签的形式存在于Java代码中。  
元数据描述的信息是类型安全的,即元数据内部的字段都是有明确类型的。  
元数据需要编译器之外的工具额外的处理用来生成其它的程序部件。  
 元数据可以只存在于Java源代码级别,也可以存在于编译之后的Class文件内部。

2、JDK5.0前没出现语言级的元数据机制Annotation,java元数据的解决方案:

在注解诞生之前,程序的元数据存在的形式仅限于xml 部署描述文件java注释或javadoc,但注解可以提供更多功能,它不仅包含元数据,还能作用于运行期,注解解析器能够使用注解决定处理流程

字节码验证 (很复杂)

n  运行检查

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

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

符号引用验证

常量池中描述类是否存在

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

符号引用是基于字符串的。

准备

n  链接 -> 准备

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

•      public static int v=1;

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

•       在初始化的<clinit>中才会被设置为1

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

•      public static final  int v=1;

解析

n  链接 -> 解析

–     将符号引用替换为直接引用

–     符号引用是一个字符串,被引用对象不一定被加载

–     直接引用是指向一个对象的指针或者对象的地址偏移量,被引用的对象一定在内存

符号引用和直接引用:

简单来讲符号引用就是一个字符串常量。比如:比如说我这个类的超类是java.lang.object.符号引用就是说,我在我这个class的常量池里面,我有一个字符串,字符串的内容就是java.lang.object.这就是符号引用。符号引用并不能直接的引用到要引用的内容,他只是一种表示的方式。要真正能够直接使用这个引用,那么我们需要直接引用,直接引用就是指针指向一个内存地址或者地址偏移量(偏移量一定是有一个基地址,然后相对这个基地址偏移多少得到的那个地址),直接指向到要引用的内容。引用对象一定在内存,而符号引用,引用对象不一定被加载。

1.        符号引用(Symbolic References):

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量[z2] ,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info[z3] 的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

 

2.        直接引用:直接引用可以是

(1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针

(2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量),偏移量就是内存地址。

(3)一个能间接定位到目标的句柄

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

初始化

n  执行 类构造器<clinit>

–      static变量的 赋值语句

–      static{}语句

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

<clinit>是线程安全的

问题:

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

域不存在错误。当应用试图访问或者修改某类的某个域,而该类的定义中没有该域的定义时抛出该错误。

很显然是在链接的验证阶段的符号引用验证时会抛出这个异常,或者NoSuchMethodError等异常。

装载器ClassLoader

什么是类装载器ClassLoader?

n  ClassLoader是一个抽象类

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

n  ClassLoader可以定制,满足不同的字节码流获取方式

n  ClassLoader负责类装载过程中的加载阶段

JDK中ClassLoader默认设计模式

ClassLoader的重要方法

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

•       载入并返回一个Class(猜测它内部调用了defineClass方法)

–     protected final Class<?> defineClass(byte[] b, int off, int len)

•       定义一个类,不公开调用

•       真正完成类的加载工作是通过调用 defineClass来实现的;将字节数组转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象等

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

•       loadClass会调用该方法,自定义ClassLoader的推荐做法,这个方法在jdk中是空实现,在jdk自带的加载器都无法加载该类时,会调用这个方法。

–     protected final Class<?> findLoadedClass(String name)

•       寻找已经加载的类

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

        throw new ClassNotFoundException(name);

    }

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;

        }

    }

加载有三步:

1,  找到或得到.class字节码文件(或字节码内容)。

2,  通过流,将字节码内容读取到内存中,并转化为字节数组。(这一步已经完成了加载到内存的操作)

3, 将该字节数组作为参数传入defineClass方法,在堆中定义该类的Class对象。(真正完成类的加载工作是通过调用 defineClass来实现的;将字节数组转为方法区数据结构,在Java堆中生成对应的java.lang.Class对象等

类加载器的分类

BootStrap ClassLoader (启动ClassLoader

Extension ClassLoader (扩展ClassLoader

App ClassLoader (应用ClassLoader/系统ClassLoader

Custom ClassLoader(用户自定义ClassLoader)

每个ClassLoader都有一个Parent作为父亲(除了根加载器BootStrap ClassLoader,它是最顶级的)

类加载器的协同工作(父委托机制/双亲模式)

双亲模式存在的问题(父ClassLoader无法访问子ClassLoader加载的类的问题)

用Thread. setContextClassLoader()解决父ClassLoader无法访问子ClassLoader加载的类的问题

n  Thread. setContextClassLoader(ClassLoader cl)

–      上下文加载器

–      是一个角色

–      用以解决顶层ClassLoader无法访问底层ClassLoader加载的类的问题

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

–     将类加载器对象在线程中传递,当我们要加载一个类时,我们可以从线程中获取这个类加载器来加载,这样可以保证当我们想要用同一个加载器加载那些在不同类加载器作用区域的类。

示例:

代码来自于

javax.xml.parsers.FactoryFinder

展示如何在启动类加载器加载AppLoader的类上下文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());

            }

            else {

                throw e1;

            }

        }

    }

 

ClassLoader getContextClassLoader() throws SecurityException{

        return (ClassLoader)

                AccessController.doPrivileged(new PrivilegedAction() {

            public Object run() {

                ClassLoader cl = null;

                //try {

                cl = Thread.currentThread().getContextClassLoader();

                //} catch (SecurityException ex) { }

 

                if (cl == null)

                    cl = ClassLoader.getSystemClassLoader();

 

                return cl;

            }

        });

    }

 

双亲模式的破坏

n  双亲模式的破坏

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

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

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

示例:破坏双亲模式-  先从底层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

OrderClassLoader myLoader=new OrderClassLoader("D:/tmp/clz/");

Class clz=myLoader.loadClass("geym.jvm.ch6.classloader.DemoA");

System.out.println(clz.getClassLoader());

System.out.println("==== Class Loader Tree ====");

ClassLoader cl=myLoader;

while(cl!=null){

    System.out.println(cl);

    cl=cl.getParent();

}

输出结果:

java.io.FileNotFoundException: D:\tmp\clz\java\lang\Object.class (系统找不到指定的路径。[z4] )

       at java.io.FileInputStream.open(Native Method)

       .....

       at geym.jvm.ch6.classloader.ClassLoaderTest.main(ClassLoaderTest.java:7)

无法载入类:java.lang.Object需要请求父加载器[z5] 

geym.jvm.ch6.classloader.OrderClassLoader@18f5824

==== Class Loader Tree ====

geym.jvm.ch6.classloader.OrderClassLoader@18f5824

sun.misc.Launcher$AppClassLoader@f4f44a

sun.misc.Launcher$ExtClassLoader@1d256fa

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

sun.misc.Launcher$AppClassLoader@b23210[z6] 

==== Class Loader Tree ====

geym.jvm.ch6.classloader.OrderClassLoader@290fbc

sun.misc.Launcher$AppClassLoader@b23210

sun.misc.Launcher$ExtClassLoader@f4f44a

不重载loadClass()方法,会使用双亲的模式,就会先让AppLoader加载

热替换

含义:

n  含义:

–      当一个class被替换后,系统无需重启,替换的类立即生效

–      例子:

•       geym.jvm.ch6.hot.CVersionA

public class CVersionA {

       public void sayHello() {

              System.out.println("hello world! (version A)");

       }

}

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

–      hello world! (version A)

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

public class CVersionA {

       public void sayHello() {

              System.out.println("hello world! (version B)");

       }

}

n  替换后, DoopRun 的输出变为

–      hello world! (version B)

 

 

 

 

 [z1]1,class字节码文件是根据JVM虚拟机规范中规定的字节码组织规则生成的。

2,字节是电脑里的数据量单位。字节码是一种中间码,它比机器码更抽象。它经常被看作是包含一个执行程序的二进制文件,更像一个对象模型。字节码被这样叫是因为通常每个 opcode 是一字节长(java中自个字节占8个比特位)。

3,机器码就是说计算机能读懂的代码,简单点说就是给计算机执行的二进制代码.字节码,是JAVA语言专有的,它是JVM来执行的二进制代码。虽然都是二进制代码,但是由于执行它的哥们不一样,所以它们存在一些指令集上的区别。

4,机器码完全依附硬件而存在,并且不同硬件由于内嵌指令集不同,即使相同的0 1代码 意思也可能是不同的,换句话说,根本不存在跨平台性,比如:不同型号的CPU,你给他个指令10001101,他们可能会解析为不同的结果。

5,我们知道JAVA是跨平台的,为什么呢?因为他有一个jvm,不论那种硬件,只要你装有jvm,那么他就认识这个JAVA字节码,至于底层的机器码,咱不用管,有jvm搞定,他会把字节码再翻译成所在机器认识的机器码。

6,机器码就是0101这样的二进制代码,而二进制码要想操作机器就需要相应的指令系统而指令系统就是对二进制代码所表示意思的规定,比如0表示某个开关关,1表示某个开关开这样的.关于这一部分属于硬件部分了。

 [z2]

Java字面量(Java直接量)

int i = 1;把整数1赋值给int型变量i,整数1就是Java字面量

同样,String s = "abc";中的abc也是字面量。

 [z3]

1,常量池中的CONSTANT_Class_info可以看做是一个CONSTANT_Class数据类型的一个实例。 他是对类或者接口的符号引用。 它描述的可以是当前类型的信息, 也可以描述对当前类的引用, 还可以描述对其他类的引用。 也就是说, 如果访问了一个类字段, 或者调用了一个类的方法, 对这些字段或方法的符号引用, 必须包含它们所在的类型的信息,CONSTANT_Class_info就是对字段或方法符号引用中类型信息的描述。

2,CONSTANT_Class_info的第一个字节是tag值为7也就是说, 当虚拟机访问到一个常量池中的数据项, 如果发现它的tag值为7就可以判断这一个CONSTANT_Class_infotag下面的两个字节是一个叫做name_index的索引值它指向一个CONSTANT_Utf8_info这个CONSTANT_Utf8_info中存储了CONSTANT_Class_info要描述的类型的全限定名。

 [z4]因为先从OrderClassLoader加载,找不到父类Object,之后使用appLoader加载Object

 [z5]DemoA在ClassPath中,但由OrderClassLoader加载,因为方法中是先由自己加载,加载不了再父加载器加载。

 [z6]DemoA由AppClassLoader加载

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
10 收藏
0
分享
返回顶部
顶部