Java笔记(class loader)

摘自:https://www.ibm.com/developerworks/cn/java/j-lo-classloader/

        《与深入理解Java虚拟机》

1.类加载器基本概念
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

2.类加载的流程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking),这七个阶段的发生顺序如下图所示:

Java笔记(class loader)

如上图所示,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这个顺序来按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段后再开始。

类的生命周期的每一个阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

    Java虚拟机规范没有强制性约束在什么时候开始类加载过程,但是对于初始化阶段,虚拟机规范则严格规定了有且只有5种情况必需立即对类进行“初始化”(而加载、验证、准备阶段则必需在此之前开始),这五种情况归类如下:

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。

  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。

  • 当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。

  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

对象的创建:

有4种显式地创建对象的方式:

  1. 用new语句创建对象,这是最常用的创建对象的方式。

  2. 运用反射手段,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。

  3. 调用对象的clone()方法。

  4. 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法.

除了以上4种显式地创建对象的方式以外,在程序中还可以隐含地创建对象,包括以下几种情况:

  1. 对于java命令中的每个命令行参数,Java虚拟机都会创建相应的String对象,并把它们组织到一个String数组中,再把该数组作为参数传给程序入口main(String args[])方法。
  2. 程序代码中的String类型的直接数对应一个String对象,例如:
    String s1="Hello";
    
    String s2="Hello"; //s2和s1引用同一个String对象
    
    String s3=new String("Hello");
    
    System.out.println(s1==s2); //打印true
    
    System.out.println(s1==s3); //打印false
    执行完以上程序,内存中实际上只有两个String对象,一个是直接数,由Java虚拟机隐含地创建,还有一个通过new语句显式地创建。
  3. 字符串操作符“+”的运算结果为一个新的String对象。例如:
    String s1="H";
    
    String s2=" ello";
    
    String s3=s1+s2; //s3引用一个新的String对象
    
    System.out.println(s3=="Hello"); //打印false
    
    System.out.println(s3.equals("Hello")); //打印true
  4. 当Java虚拟机加载一个类时,会隐含地创建描述这个类的Class实例.

不可变类String类型在创建的时候会在虚拟机中创建一个String对象。

可变类:当你获得这个类的一个实例引用时,你可以改变这个实例的内容。
不可变类:当你获得这个类的一个实例引用时,你不可以改变这个实例的内容。不可变类的实例一但创建,其内在成员变量的值就不能被修改。


为什么String类是不可变的?

String是所有语言中最常用的一个类。我们知道在Java中,String是不可变的、final的。Java在运行时也保存了一个字符串池(String pool),这使得String成为了一个特别的类。

    String类不可变性的好处

    1. 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现(译者注:String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串。),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

    2. 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。

    3. 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

    4. 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

    5. 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

    原文链接: Journaldev 翻译: ImportNew.com - 唐小娟


     

    3.类加载的过程

    1.加载

    在加载阶段,虚拟机完成以下3件事:

      1. 通过一个类的全限定名来获取定义此类的二进制字节流,如可以从ZIP包中读取(JAR,EAR,WAR格式的基础),从网络中获取(Applet),运行时计算生成(动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为"*$Proxy"的代理类的二进制字节流),由其他文件生成(JSP文件生成对应的Class类),从数据库中读取。。。

      2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

      3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

        加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后再内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。

        加载数据与链接阶段的部分内容(如果一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成完成,链接阶段可能已经开始,但这些夹在夹在夹在阶段致之中的动作,仍然属于连接段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序

        2.验证


             验证时链接阶段的第一步,这一阶段的目的是微利确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

             验证阶段大致上会完成一下4个阶段的检验动作:文件格式的验证、元数据的验证、字节流验证、符号引用的验证

        文件格式的验证

             第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。可能包括以下验证点:

        1. 是否以魔方数0xCAFEBABE开头

        2. 主、次版本号是否在当前虚拟机的处理范围之内

        3. 常量池的常量中是否有不被支持的常量类型(检验常量tag标志)

        4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

        5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据

        6. Class文件中各个部分及文件本身

        7. 。。。。。。

        该验证阶段的主要目的是保证输入的字节流能正确的解析并存储于方法区之内,格式上符合一个Java类型信息的要求。这阶段的验证时基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

        元数据验证

        第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:

        1. 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)

        2. 这个类的父类是否继承了不允许被继承的父类

        3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法

        4. 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都不一致,但返回值类型却不同等)

        5. 字节码验证

          第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件,例如:

        1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载如本地变量表中

        2. 保证跳转指令不会跳转到方法体以外的字节码指令上

        3. 保证跳转指令不会跳转到方法体以外的字节码指令上

        4. 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋给父类数据类型,这是安全的,但是把父类对象赋给子类数据类型,甚至是把对象赋给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的

        5. 符号引用引证

          主要是在虚拟机将符号引用转化为直接引用的时候进行校验,这个转化动作是发生在解析阶段。符号引用验证可以看做是对类自身以外(常量池的各种符号引用)的信息进行匹配性的校验。通常需要校验下列内容:

          符号引用中通过字符串描述的全限定名是否通过字符串描述的全限定名是否能找到对应的类

          在指定类中是否在符合方法的字段描述符以及简单名称所描述的方法和字段

          符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问

          验证阶段对于虚拟机的类加载机制来说,是一个非常重要但不一定是必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,从而缩短虚拟机类加载的时间。

        3.准备

        准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注:这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起被分配在Java堆中。

        4.解析


           解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

            符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。

            直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

            对于同一个符号引用可能会出现多次解析,虚拟机可能会对第一次解析的结果进行缓存。

            解析动作分为四类:包括类或接口的解析、字段解析、类方法解析、接口方法解析。

        5.初始化


        类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

            初始化阶段是执行类构造器<clinit>()方法的过程。对于<clinit>()方法具体介绍如下:

            1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。

            2)<clinit>()方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个执行的<clinit>()方法的类一定是java.lang.Object。

            3)由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

            4)<clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

            5)接口中可能会有变量赋值操作,因此接口也会生成<clinit>()方法。但是接口与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。

            6)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那么就可能造成多个进程阻塞。

        4.类与类加载器

        对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,对于每一个类加载器,都拥有一个独立的类名称空间。通俗讲:比较两个类是否“相等”,只有在这两个类由一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不同。

        这里的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判断等情况

        虽然来自同一个Class文件,如果被两个不同的类加载器加载,但依然是两个独立的类,做对象所属类型检验时结果为false

        5.双亲委派模型

        从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstarp ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一个就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader

        从Java开发人员的角度来看,类加载器还可以为:

        • 启动类加载器(Bootstarp ClassLoader):这个类将负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载器请求委派给引导类加载器,那直接使用null代替即可。
        • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
        • 应用程序类加载器(Application ClassLoder):这个类加载由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,也称为系统类加载器,它负责加载用户路径(ClassPath)上指定的类库。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

        类加载器之间这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)

        Java笔记(class loader)

         

         

         

         

         

         

         

         

         

         

         

         

         

         

        双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到父类加载器去完成,每一个层次的类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

        双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader()方法中。

        6.java.lang.ClassLoader类介绍

        java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如所示。

        方法
        说明

        getParent()
        返回该类加载器的父类加载器。

        loadClass(String name)
        加载名称为 name的类,返回的结果是 java.lang.Class类的实例。

        findClass(String name)
        查找名称为 name的类,返回的结果是 java.lang.Class类的实例。

        findLoadedClass(String name)
        查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。

        defineClass(String name, byte[] b, int off, int len)
        把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。

        resolveClass(Class<?> c)
        链接指定的 Java 类。

        Java笔记(class loader),布布扣,bubuko.com

        Java笔记(class loader)

        上一篇:IPython,让Python显得友好十倍的外套——windows XP/Win7安装详解


        下一篇:自己研究的新软件HappyTime(类似微信聊天)