Java虚拟机_类加载机制

来源:互联网 发布:优化神马pc关键词排名 编辑:程序博客网 时间:2024/06/05 10:41

1:类加载机制

类加载机制:虚拟机把描述类的数据从Class文件(或者jar或者从网络获取的class字节流)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型

类的加载和连接过程都是在程序运行期间完成的,这为java程序提供高度的灵活性,JAVA可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的;例如一个使用接口的应用程序,可以等到运行时再指定其实际的实现;

2:类加载的过程

类加载的生命周期:加载(Loading)-->验证(Verification)-->准备(Preparation)-->解析(Resolution)-->初始化(Initialization)

其中验证、准备、解析三个阶段构成了连接阶段

2.1 加载

加载阶段完成的工作:

1、通过一个类的全限定名来获取定义此类的二进制字节流。但是虚拟机规范没有明确规定在哪里获取或者如何获取,可以通过定义类加载器控制字节流加载方式;

      可以有很多具体的实现。从zip包种获取(jar/war/ear等);从网络中获取(Applet);运行时计算生成(动态代理技术);其他文件生成(JSP)等

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

     方法区中数据存储格式由虚拟机自行定义,虚拟机规范并未规定此区域的具体数据结构,二进制字节流加载完成后,将按照虚拟机所需的格式存储在方法区之中

3、在java堆中生成一个代表这个类的java.lang.Class对象,这个对象做为程序访问方法区中这些类型数据的访问入口

加载阶段和连接阶段的部分内容(比如一部分字节码文件格式的验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段进行的动作仍然属于连接阶段的内容

2.2 验证

验证是连接的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全;
虚拟机规范对这个阶段的限制和指导非常笼统,具体检查哪些内容如何检查等都没有做出规定,一般会有如下几方面的检查:

2.2.1文件格式验证

主要验证字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理,保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个Javal类型信息的要求;这个阶段的验证是基于字节流进行的,经过这个阶段验证之后,字节流才能进入内存的方法区进行存储,后面的三个阶段的验证全部是基于方法区的存储结构进行的;

2.2.2元数据验证

对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范的要求,比如是否继承了不允许被继承的final类等;

2.2.3字节码验证

这个阶段的主要工作是进行数据流和控制流的分析。任务是确保被验证类的方法在运行时不会做出危害虚拟机安全的行为

2.2.4符号引用验证

这一阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。目的是确保解析动作能够正常执行.

验证阶段对于虚拟机类加载机制来说是一个重要但是不一定必要的阶段,如果运行的代码已经反复使用和验证过,那么就可以通过-Xverify:none来关闭

2.3 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,这些内存都在方法区中进行分配。
这个阶段分配内存的仅仅是类变量(static修饰)不包括实例变量,实例变量会在对象初始化时随着对象一起分配的对内存中;设置类变量的初始值是指数据类型的零值;
例如public static int value=123;这个阶段设置的初始值为0而不是123,因为这个时候尚未开始执行任何的java方法,而把value设置为123的putstatic指令实在程序被编译之后存放于类构造器<clinit>方法中的,所以value=123的动作将在初始化阶段才会被执行;
但是public static final value=123,那么这个阶段会将value设置为123

2.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在Class文件中以CONSTANT_Class_info,CONSTANT_Field_info,CONSTANT_Methodref_info等类型的常量出现。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要在使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。    
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
解析动作分为四类:包括类或接口的解析、字段解析、类方法解析、接口方法解析。

2.5 初始化

前面的加载和连接过程除了在加载阶段用户可以通过自定义的加载器参与之外,其余动作全部由虚拟机主导和完成,到了初始化阶段,才真正开始执行类中定义的Java程序代码; 
初始化阶段是执行类构造器<clinit>()方法的过程。对于<clinit>()方法具体介绍如下:    
1:<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。
2:<clinit>()方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个执行的<clinit>()方法的类一定是java.lang.Object。
3:由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
4:虚拟机会保证<cinit>方法的线程安全性

3:类加载器

任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性;加载器不同,即使两个类加载自相同的class文件,两者仍不相等;

3.1类加载器的类型

3.1.1启动类加载器(Bootstrap ClassLoader)

这个加载器负责将存放在<java_home>/lib目录中的或者被-Xbootclasspath参数所指定的路径中的并且是虚拟机识别的(仅按照文件名识别,例如rt.jar,名字不符合的即使放到lib目录下也不会加载)类库加载到虚拟机内存中。使用C++语言实现,是虚拟器自身的一部分,不能被Java程序直接引用

3.1.2扩展加载器(Extension ClassLoader)

这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<java_home>/lib/ext目录中的或者是系统变量-Djava.ext.dirs指定的目录中的所有类库,开发者可以直接使用扩展加载器

3.1.3应用程序类加载器(Application ClassLoader)

这个加载器由sun.misc.Launcher$AppClassLoader来是实现。这个加载器是ClassLoader类中方法getSystemClassloader()的返回值因此也被称为系统加载器。它负载加载用户类路径(ClassPath由java -classpath/-Djava.class.path)上指定的类库,开发者可以直接使用这个类加载器,一般情况下也是程序默认的类加载器。

3.1.4用户加载器(User ClassLoader)

以上三种加载器都是系统提供的类加载器。UserClassLoader加载器在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性.

除了引导类加载器之外,所有的类加载器都有一个父类加载器,可以通过getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。
但是加载器之间的父子关系不是通过继承来实现的,而是使用组合关系来复用父加载器的代码;

3.2 类加载器的双亲委派模型

3.2.1双亲委派模型

某个特定的类加载器在接到加载类的请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。

3.2.2双亲委派模型的优点

使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:

protected synchronized Class<?> loadClass(String name, boolean resolve)   throws ClassNotFoundException{

    // First, checkif theclass has already been loaded

    Class c = findLoadedClass(name);

   if (c == null) {

       try {

           if (parent != null) {

                c = parent.loadClass(name, false);

            }else {

                c = findBootstrapClassOrNull(name);

            }

        } catch (ClassNotFoundException e) {

            // ClassNotFoundException thrownifclassnot found

            //from the non-null parentclass loader

            }

       if (c == null) {

            // If stillnot found, then invoke findClassin order

            // to find theclass.

            c = findClass(name);

        }

    }

   if (resolve) {

        resolveClass(c);

    }

   return c;

}

先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载.

3.2.3双亲委派模型缺点

父加载器中无法加载子加载器来查找类。
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory类中的 newInstance()方法用来生成一个新的 DocumentBuilderFactory的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。

3.2.4线程上下文加载器

线程上下文加载器可以解决双亲委派模型父加载器无法找到由子加载器负责加载的类的问题
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源.
1 1