深入理解Java虚拟机(第七章):虚拟机类加载机制

时间:2022-12-21 09:51:51

Java虚拟机
类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。

类的生命周期:
从被加载到JVM开始,直到卸载出内存:
加载(loading)—验证(verification)—-准备(preparation)—-解析(resolution)
—-初始化(initialization)—使用(using)—-卸载(unloading)

验证、准备和解析统称为连接;

其中解析阶段的发生时机不一定,可能在初始化之后(动态绑定);

五种情况必须立即对类进行初始化(有且仅有这五种情况):
(1)遇到new、getstatic、putstatic、invokestatic这四条指令时,如果类没有初始化,需先触发其初始化。(使用new关键字实例化对象时、读取或者设置一个类的静态字段(除了被final修饰,编译器就把结果放进常量池的)、以及调用一个类的静态方法的时候)
(2)使用java.lang.reflect包内方法对类进行反射调用的时候,如果没初始化,就先初始化;
(3)初始化一个类,如果其父类没有初始化,则先初始化其父类;
(4)启动JVM时,初始化包含main方法的主类
(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并这个句柄对应的类,没有进行初始化,则先初始化;
以上五种称为主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用;

类加载的过程
一、加载
在加载阶段,java虚拟机需要完成以下3件事:
a.通过一个类的全限定名来获取定义此类的二进制字节流。
b.将定义类的二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构
c.在java堆中生成一个代表该类的java.lang.Class对象,作为方法区数据的访问入口。

加载阶段完成后,JVM外部的二进制字节流就按照JVM所需的格式存储在方法区之中(数据的存储格式由JVM定义),然后在内存中实例化一个java.lang.Class类的对象;

二、验证(class文件不一定是java文件编译来的)
目的是为了确保,class文件的字节流中包含的信息符合当前JVM的需求,并且不会危害JVM自身安全;
(工作量比例很大)
1、文件格式验证(与加载过程交叉进行)
验证字节流是否符合class文件格式规范,并且能否被当前版本JVM处理;
只有经过这个阶段的验证,字节流才会进入内存的方法区中存储,所以后边三个阶段的验证都是基于方法区的存储结构进行的,不会再操作字节流;
2、元数据验证
对字节码描述的信息进行语义分析,以保证描述信息符合java语言规范的要求;
3、字节码验证(验证的过程中,最复杂的阶段)
通过数据流和控制流分析,确定程序语义事都是合法的、符合逻辑的;
由于太复杂,所以优化,只需要检查StackMapTable属性中的记录是否合法即可;(可能被篡改,所以不一定安全)
4、符号引用验证
确保解析动作能正确执行:符号引用是否能找到确切的直接引用;解析就是将符号引用转为直接引用;

以上四个阶段都是验证的一部分;非常重要但是不一定一定要进行的阶段。因为有些代码都是经过反复使用和验证过的,很安全,因此可以考虑使用-Xverify:none参数来关闭大部分的类验证措施;

三、准备
为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配;

类变量:被static修饰的变量,不包括实例变量,实例变量将在对象实例化的时候随对象分配到java堆中;
变量初始值:赋值为0;真正的赋值动作在初始化阶段执行;
(在编译时javac会为static变量设置ConstantValue属性,在准备阶段就按照该属性将真正的值赋值;比如:final关键字修饰)

四:解析

JVM将常量池中的符号引用转为直接引用(直接引用则引用目标一定在内存中了);
1、类和接口的解析
2、字段解析
3、类方法解析
4、接口方法解析

五、初始化
初始化是类加载过程最后一步;除了在加载阶段用户可以自定义类加载器参与外,其余步骤全部由JVM主导和控制;;到了初始化阶段才可以真正执行Java程序代码;
初始化阶段,将变量的真正初始值赋值给变量和其他资源;
初始化过程就是执行类构造器()方法的过程

1、<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问如下代码;
public class Test{
static{
i=0;//给变量赋值可以正常编译通过
System.out.print(i);//这句编译器会提示”非法向前引用”
}
static int i=1;
}
2、与实例构造器中的()方法不同, 它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。 因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
3、由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类类的变量赋值操作。
4、()方法对于类或者接口来说并不是必须的,如果一个类没有静态语句块,也就没有变量的赋值操作,那么编译器可以不为这个类生成()方法。
5、接口中不能使用静态语句块,但仍然可以有变量初始化的赋值操作,因此接口与类一样都会生成()方法。但接口与类不同,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
6、虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的()方法,若其中有耗时很长的操作,就可能造成多个线程阻塞。

/*
clinit方法和init方法的区别
1、执行时机不同:
init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
2、目的不同
init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化
init执行的顺序是:先初始化成员变量,最后再调用类的构造方法
*/

类加载器:“通过一个类的全额定名来获取描述此类的二进制字节流”
实现的是类的加载动作;(JVM外部)
对于任何一个类,都需要加载它的类加载器和它本身一同确立它在JVM中的唯一性;每一个类加载器都拥有一个独立的类名称空间;两个类是否相同,必须是在由同一个类加载器加载的前提下(包括equals()方法,instanceof()方法);
不同的类加载器对instanceof关键字运算时有影响的;
instanceof:用来在运行时指出对象是否是特定类的一个实例。instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例。result = object instanceof class

双亲委派模型:
从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:

* 启动类加载器(Bootstrap classloader):这个类装载器是在JVM启动的时候创建的。它负责装载Java API(java 核心类库),包含Object对象。和其他的类装载器不同的地方在于这个装载器是通过native code来实现的,而不是用Java代码。负责将存放在\lib 目录中的、或者是 -Xbootclasspath参数所指定的路径中的,并且是JVM所识别的类库加载到JVM内存中;
* 扩展类加载器(Extension classloader):它装载除了基本的Java API以外的扩展类。它也负责装载其他的安全扩展功能。\lib\ext
* 系统类加载器(System classloader)或者叫应用程序加载器(Application classloader):如果说bootstrap class loader和extension class loader负责加载的是JVM的组件,那么system class loader负责加载的是应用程序类。它负责加载用户在$CLASSPATH里指定的类。
当然用户也可以自定类加载器:
* 用户自定义类加载器(User-defined classloader):这是应用程序开发者用直接用代码实现的类装载器。

双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。增强了稳定性;但是这个模型是非强制性的,是java设计者的推荐模型;

破坏双亲委派模型
第一次:为了向前兼容
第二次:为了“调回”,线程上下文加载器
第三次:为了代码热替换,模块热部署:
OSGi:实现模块化热部署的关键在于它自定义的类加载器机制的实现。每一个程序模块(OSGi中称之为Bundle)都有一个自己的类加载器,每次更换Bundle,就把Bundle连同类加载器一起换掉以实现代码的热替换;
<*
OSGI
面向Java的动态模型系统
是Java动态化模块化系统的一系列规范,OSGi服务平台向Java提供服务,这些服务使Java成为软件集成和软件开发的首选环境。

*>