在说明组成结构之前,我们可以想象下Java程序运行的一个大致流程:
将源代码编译成字节码(编译器),加载字节码,JVM执行字节码成JVM指令,JVM指令翻译成OS指令,OS执行指令,程序运行。
第1步是编译器的工作,JVM并不管,后三步骤是JVM的工作:加载字节码需要一个加载系统,执行字节码,需要一个执行引擎系统,指令翻译成OS指令,需要一个解释器(或者JIT类似功能),当然除了代码(字节码,指令),还有数据,所以需要一个存储区。具体的流程图可以概括如下:
因此JVM的组成大概分成三部分:
1.类加载器(ClassLoader)子系统,
2.运行时数据区,
3.执行引擎(包括了解释器)。
这里先讲讲类加载子系统
一、类加载器(类加载器不是类加载子系统,而只是其中一部分)
顾名思义,就是用来加载.class文件(字节码文件)的。JVM可以安装多个类加载器,总的有两种类加载器:启动类加载器和用户自定义加载器,启动类加载器是JVM实现的一部分,用户自定义类加载器则是Java程序的一部分,必须是ClassLoader类的子类
系统默认的有三个类加载器(启动类加载器):
(1)Bootstrap:内嵌在JVM中由C++编写,JRE/lib/rt.jar($JAVA_HOME,即所有java.*开头的类,核心类),下面两个ClassLoader也是它加载的。(刚又查看了,有的JVM的Bootstrap是由Java编写的,这样一来,就不是很懂,这个Java类(Bootstrap)是怎么加载运行的了)
(2)ExtClassLoader:JRE/lib/ext/*.jar,例如所有javax.*开头的类和存放在JRE的ext目录下的类
(3)AppClassLoader:CLASSPATH指定的所有jar或目录,即应用程序自身的类
自定义ClassLoader:继承java.lang.ClassLoader,tomcat、JBoss都会根据j2ee规范自定义ClassLoader
除了Bootstrap之外,其他三种ClassLoader都是java.lang.ClassLoader的子类
检查某个类是否被加载,是自下向上的,只要某一个ClassLoader已加载就视该类为已加载,保证此类只被加载一次。若没有加载(它会传递给上层加载器),实际试图加载的顺序为自上而下的,也就是从最上层尝试加载此类。(也就是说,先自下而上检查类是否被加载(仅仅是检查,加载了,返回该类的Class的对象,否则传递给上层),检查到顶了(Bootstrap),发现该类还没有加载,则自上而下尝试加载,若到最后还没加载成功,返回异常。)
这里说的自上而下的顺序是:Bootstrap --> ExtClassLoader --> AppClassLoader --> 自定义ClassLoader 前一层是后一层的父加载器,但他们的这种关系不是通过继承实现的,而是使用组合关系复用父加载器中的代码,这种关系模型被称为双亲委派模型。
这种模型的作用是为了Java的稳定性,对于一个很重要的类,例如Object,无论是谁试图用其他的加载器加载,最终加载Object的一定是Bootstrap,这样就保证Object各种类加载器中都是同一个类。(这种模型并不是Java强制规定使用的,而只是推荐)
所谓的同一个类,不仅仅是指类的代码、包路径一样,在Java中,同一个类还要指相同的加载器,哪怕同一个class文件,加载器不同,类就不是同一个。
当运行一个程序的时候,JVM启动(能在一个OS下同时启动多个JVM,一条java指令启动一个main方法就是一个JVM),运行Bootstrap,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也在此时被加载),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载CLASSPATH目录下定义的Class,这就是一个程序最基本的加载流程。
下面是具体的类加载过程:
类的加载分为三步: 1.装载:查找并加载类的二进制数据(说是加载也可以,只是为了和整个加载过程区分来说成装载,装载完了不代表加载完了,装载只是将字节码装进JVM) 2.链接,又分为三个步骤: 验证:确保被加载类的正确性 准备:为类的静态变量分配内存,并将其初始化为变量类型的默认值 解析:把类中的符号引用转化为直接引用 3.初始化:为类的静态变量赋予正确的初始值 这五个阶段,解析阶段开始的顺序不定,可能在初始化前,可能在初始化后,这是为了支持Java的运行时绑定(可以查看静态绑定与动态绑定),其他四个阶段,开始的顺序是固定的(注意,只是开始的顺序,结束不一定)。 1.装载 就是将class文件中的二进制数据读到JVM内存中(就是通过前面的类加载器ClassLoader装载),将其放在运行时数据区的方法区内,然后在堆区创建一个这个类的对象(Class对象,每一个类唯一),具体步骤为: 1.通过类的全限定名获取该类的class文件(全限定名,类似:java.lang.Object) 2.类加载器进行加载 3.在Java堆中(方法区)生成该类的java.lang.Class对象,作为方法区这些数据的访问入口 关于第一点,很灵活,很多技术都是在这里切入,因为它并没有限定二进制流从哪里来: 1.从本地系统直接加载(本地Java程序) 2.通过网络下载.class文件(Applet) 3.从zip、jar等归档文件中加载class文件 4.从专有数据库中提取class文件 5.将Java源文件编译成class文件(服务器) 2.链接 1).验证 为什么要验证呢?首先,如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手直接对这些class文件进行编辑呢?或者自己手动写一个class文件呢?让JVM加载并运行,程序的危险性就提高了。 验证主要经历几个步骤:文件格式验证->元数据验证->字节码验证->符号引用验证 (1).文件格式验证:验证字节流是否符合class文件格式的规范并验证其版本是否能被当前的jvm版本所处理。ok没问题后,字节流就可以进入内存的方法区进行保存了。后面的3个校验都是在方法区进行的。 (2).元数据验证:对字节码描述的信息进行语义化分析,保证其描述的内容符合java语言的语法规范。 (3).字节码检验:最复杂,对方法体的内容进行检验,保证其在运行时不会作出什么出格的事来。 (4).符号引用验证:来验证一些引用的真实性与可行性,比如代码里面引了其他类,这里就要去检测一下那些来究竟是否存在;或者说代码中访问了其他类的一些属性,这里就对那些属性的可以访问行进行了检验。(这一步将为后面的解析工作打下基础) 验证阶段很重要,但也不是必要的,假如说一些代码被反复使用并验证过可靠性了,实施阶段就可以尝试用-Xverify:none参数来关闭大部分的类验证措施,以简短类加载时间。 2).准备 为类的静态变量分配内存,并将其初始化为变量类型的默认值,执行的方法:;(这里之前貌似搞错了,这里还是执行clinit();方法,前面准备阶段只是使用默认值,这里才是真正初始化为代码中赋予的值) 这两个方法(clinit和init)一个是虚拟机在装载一个类初始化的时候调用的(clinit),另一个是在类实例化时调用的,那么类什么时候才被初始化? 1.创建类的实例的时候,也就是说new或newInstance()的时候 2.访问某个类或接口的静态变量,或者对该静态变量赋值(这里注意是实际这个变量所在的类开始初始化,例如例1) 3.调用类的静态方法(2、3可以看做是一条) 4.反射(java.lang.reflect.*)以及Class.forName(className);ClassLoader的loadClass(className)该方法只会编译并加载,并没有对其初始化 5.初始化一个类的子类(会首先初始化子类的父类) 6.JVM启动时标明的启动类,即和文件名相同的那个类 0.对于已经初始化了的类不再初始化! 初始化步骤: 1.如果这个类没有被加载和链接,先进行这两步骤; 2.假如这个类存在直接父类,并且这个类还没被初始化(在一个类加载器中,类只能初始化一次),初始化这个直接父类 3.假如类中存在初始化语句(static变量或语句块),那就一次执行这些初始化语句。
/**
* 被动引用情景1
* 通过子类引用父类的静态字段,不会导致子类的初始化
* @author volador
*
*/
class SuperClass{
static{
System.out.print("super class init...");
}
public static int value=123;
}
class SubClass extends SuperClass{
static{
System.out.println("sub class init.");
}
public static int value=321;
}
class ChildClass extends SubClass{
static{
System.out.println("child class init.");
}
}
public class test{
public static void main(String[]args){
System.out.println(SubClass.value);
}
}
输出结果是:
super class init...
sub class init...
321
如果注释掉SubClass中的value赋值那一行,则输出为:
super class init...
123
如果ChildClass中加上:public static int value = 456; 则输出为
super class init...
sub class init...
child class init...
456
初始化了某一个类,其父类必先初始化;对静态字段的查找自下而上,若没有,该类并不会被初始化加载的具体顺序见上面:字段解析过程。(如果一个字段同时在类/父类和接口/父接口出现则编译器会报错)
/**
* 被动引用情景2
* 通过数组引用来引用类,不会触发此类的初始化
* @author volador
*
*/
public class test{
public static void main(String[] args){
SuperClass[] s_list=new SuperClass[10];
}
}
输出结果:没输出
/**
* 被动引用情景3
* 常量在编译阶段会被存入调用类的常量池中,本质上并没有引用到定义常量类类,所以自然不会触发定义常量的类的初始化
* @author root
*
*/
class ConstClass{
static{
System.out.println("ConstClass init.");
}
public final static String value="hello";
}
public class test{
public static void main(String[] args){
System.out.println(ConstClass.value);
}
}
最后总结下,类初始化的顺序:
(1).初始化父类静态成员变量和静态块(static{}),顺序执行 (2).初始化子类静态成员变量和静态块,顺序执行 (3).初始化父类成员变量和代码块(直接{}的快),顺序执行 (4).执行父类构造函数 (5).初始化子类成员变量和代码块,顺序执行 (6).执行子类构造函数
总的顺序就是:先从上到下执行静态块,再出上到下执行{普通构造块-构造函数}
这一节主要讲的是类的加载,下一节则是JVM的运行时数据区。