虚拟机类加载机制

来源:互联网 发布:云计算技术 编辑:程序博客网 时间:2024/06/06 02:35

参考资料:《深入理解Java虚拟机》

                    《深入分析Java Web技术内幕》

本文中我们将来回答以下5个问题:

What(什么是虚拟机的类加载机制)

When(什么时候开始加载)

Who(谁来加载,这部分内容涉及到后面讲的3种类加载器)

How(如何加载,涉及到双亲委派模型的工作过程)

Why(为什么采用这种加载机制)

1.      什么是虚拟机的类加载机制?

在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能拿运行和使用。虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

2.      类加载的时机

类的生命周期包括加载、连接(包括验证、准备、解析3个阶段)、初始化、使用和卸载7个阶段。类加载过程包含前面5个阶段,即加载、验证、准备、解析、初始化。


加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、 准备自然需要在此之前开始):

(1) 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候.

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

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

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

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

3.      类加载机制包含哪几个阶段。

类加载过程包含类的生命周期的前面5个阶段,即加载、验证、准备、解析、初始化。


每个阶段要做什么事,是怎么做的?


1) 加载:
“加载”的过程是“类加载”过程的一个阶段,在加载阶段,虚拟机需要完成以下三件事:
(1) 通过一个类的全限定名来获取定义此类的二进制流
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时的数据结构
(3) 在内存中(并没有明确规定是在Java堆中,对于HotSpot而言,放在方法区里)Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
怎么做:
在抽象类ClassLoader中并没有定义如何去加载,如何去找到指定类并且把它的字节码加载到内存需要的子类中去实现,也就是要实现findClass()方法。
下面举例说明子类URLClassLoader是如何实现findClass()的:在URLCLassLoader中通过一个URLClassPath类帮助取得要加载的class文件字节流,而这个URLClassPath定义了到哪里去找这个class文件,如果找到了这个class文件,再读取它的byte字节流,通过调用defineClass()方法来创建类对象。
注意:数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但是数组类的元素类型(指的是数组去掉所有维度的类型)最终是要靠类加载器去创建。


2) 验证:
验证的目的:
验证时连接的第一步,是为了确保Class文件的字节流中的包含信息符合当前虚拟机的要求,并且不会危险虚拟机自身的安全。
虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
验证主要包括四个阶段:
(1)文件格式验证,判断是否符合格式规范
 这个阶段会验证格式是否正确、是否符合版本要求、编码是否是UTF-8等
(2)元数据验证:对字节码进行语义分析
 这个阶段验证是否有父类(除了Object之外的类都应该有父类)、是否继承了不允许继承的类(如被final修饰的类)、是否实现了父类或接口中的所有方法等。

(3)字节码数据验证
 是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

(4)符号引用验证
 例如,符号引用通过字符串描述的全限定名是否能找到对应的类(JDBC用到很多),还有一个很重要的就是类、字段、方法的访问性(private\protected\public\default)是否可以被当前类访问。


3) 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值。


4) 解析
在这个阶段类装入器装入类所引用的其他所有类。可以用许多方式引用类,如超类、接口、字段、方法签名、方法中使用的本地变量。


5) 初始化
初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。初始化阶段是执行类构造器<clinit>()方法的过程。
Java程序初始化工作可以在许多不同的代码块中来完成(例如静态代码块、构造函数等),它们执行的顺序如下:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类非静态变量、父类非静态代码块、父类构造函数、子类非静态变量、子类非静态代码块、子类构造函数。


4.      ClassLoader的作用?


(1) 负责将Class加载到JVM中
(2) 审查每个类应该由谁加载,它是一种父优先的等级加载机制
(3) 负责将Class字节码重新解析成JVM统一要求的对象格式。


5.       ClassLoader类结构分析:


ClassLoader主要会用到以下5个方法:


defineClass方法用来将byte字节流解析成JVM能够识别的Class对象。

通过直接覆盖ClassLoader父类的findClass方法来实现类的加载规则, 从而取得要加载类的字节码。然后调用defineClass方法生成类的Class对象。

如果不想重新定义加载类的规则,只想在运行时能够加载自己指定的一个类,那么可以用this.getClass().getClassLoader().loadClass(“class”)调用CLassLoader的loadClass方法以获取这个类的Class对象。

这几个方法都是在扩展ClassLoader时需要用到的


6.      ClassLoader的等级加载机制(即双亲委派模型)

整个JVM平台提供三层ClassLoader,可以分为两种类型;一就是JVM自身启动类加载器(BootstrapClassLoader、Extension ClassLoader),是虚拟机自身的一部分;另一种就是所有其他的类加载器(Application ClassLoader),独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。从开发的角度,绝大部分Java程序都会使用以下3种系统提供的类加载器。

(1)    Bootstrap ClassLoader(启动类加载器)

它主要加载JVM自身工作需要的类,这个ClassLoader完全是由JVM自己控制的,需要加载哪个类、怎么加载都由JVM自己控制,别人也无法访问这个类,所以它不遵守等级加载规则,它仅仅是一个类的加载工具而已,既没有父加载器,也没有子加载器。负责加载放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。

(2)    Extension ClassLoader(扩展类加载器)

这个加载器负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

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

它的父类是Extension ClassLoader。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的.


双亲委派模型的工作过程:

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己

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


7.      为什么ClassLoader(类加载器)是父优先的等级加载机制

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果读者有兴趣的话,可以尝试去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。


原创粉丝点击