Java笔记(class loader)

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

        《与深刻理解Java虚拟机》 sql

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虚拟机规范没有强制性约束在何时开始类加载过程,可是对于初始化阶段,虚拟机规范则严格规定了有且只有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文件中各个部分及文件自己

 

  • 。。。。。。

 

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

元数据验证

 

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

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

 

  • 这个类的父类是否继承了不容许被继承的父类

  • 若是这个类不是抽象类,是否实现了其父类或接口之中要求实现的全部方法

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

 

字节码验证

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

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

 

  • 保证跳转指令不会跳转到方法体之外的字节码指令上

  • 保证跳转指令不会跳转到方法体之外的字节码指令上

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

 

符号引用引证

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

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

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

符号引用中的类、字段、方法的访问性(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程序的稳定运做很重要,但它的实现却很是简单,实现双亲委派的代码都集中在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 类。

相关文章
相关标签/搜索