(2) Java类的加载

在说明组成结构之前,我们可以想象下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>();(就是类或接口有static块或者static变量有被赋值):    (1).<clinit>()方法叫做类构造器方法(注意,不是类构造方法),由编译器自动将类中的所有类变量(static变量)的赋值动作和静态语句块中的语句合并而成的,至于他们的顺序,与在源文件中排列的顺序一样。    (2).<clinit>()方法与类构造方法不一样,他不需要显示得调用父类的<clinit>()方法,虚拟机会保证子类的<clinit>()方法在执行前父类的这个方法已经执行完毕了,也就是说,虚拟机中第一个被执行的<clinit>()方法肯定是java.lang.Object类的。    (3).<clinit>()方法对类跟接口来说不是必须的,假如类或者接口中没有对类变量进行赋值且没有静态代码块,<clinit>()方法就不会被编译器生成。    (4).接口中不能有静态块,但是可以有静态变量的赋值操作,所以,也会有clinit方法,但与类不同的是,执行接口的clinit方法不会执行父接口的clinit方法,只有用到的时候,才会去执行(这可以从继承extends和实现implement这方面理解,继承,严格的父子关系,儿子的东西从父亲继承过来的,要用得打个招呼,而实现,只是借用,二者没有那么严格,所以,同样的,实现类的初始化同样不回去执行接口的clinit方法)。    很显然,一个类只被加载一次,<clinit>()一个类也就只会调用一次(如果同一时间,多个线程初始化一个类,只有一个会线程会去执行该类的初始化,其他的会被堵塞)。    当static变量同时被final修饰的时候:    public static final int value=123;(static final 同时修饰 必须显式初始化,否则编译不通过,事实上被final修饰的,同局部变量一样,使用前必须显式初始化)    这里在准备阶段value的值就会初始化为123了。这个是说,在编译期(编译器干的事),javac会为这个特殊的value生成一个ConstantValue属性,并在准备阶段jvm就会根据这个ConstantValue的值来为value赋值了。    这一阶段,也就是说明了,类变量、全局变量可以不显式的初始化就可以直接使用(用的是默认值),而局部变量必须显式初始化,否则报错,因为局部变量不会被默认使用默认值。    3).解析:    对类的字段,方法等东西进行转换,具体涉及到class文件的格式内容,有兴趣的可以查看下这篇文章:【深入Java虚拟机】之二:Class类文件结构(兰亭风雨)。    解析包括对类/接口的解析(是数组还是普通对象)、字段的解析、类方法的解析以及接口方法的解析。    (1)类/接口的解析:判断所要转换成的直接引用,是对数组对象(一个数组在JVM里面也是一个类,但类头部与一般类头部不一样)的引用,还是普通对象引用,从而对其进行不同解析。    (2)字段解析,首先会查找本类里面是否有该字段,如果有,查找结束,如果没有,则首先查找该类的接口及父接口(以此往上类推)(如果该类有实现接口的话)是否有该字段,有,结束,还没有,查找父类及祖父类(同样以此往上类推,如果该类有父类的话),直至查找结束。这里可以查看下面的例1。    (3)类方法解析,同字段解析类似,但顺序不同,先是查找类的,再是接口的。    (4)接口方法,同类方法类似。 3.初始化:    在前面的类加载过程中,除了在加载阶段用户可以通过自定义类加载器参与之外,其他的动作完全有jvm主导,到了初始化这块,才开始真正执行java里面的代码。    这里的初始化,执行的方法:<init>();(这里之前貌似搞错了,这里还是执行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);
		}
	}

输出结果:hello(tip:在编译的时候,ConstClass.value已经被转变成hello常量放进test类的常量池里面了)

最后总结下,类初始化的顺序:

(1).初始化父类静态成员变量和静态块(static{}),顺序执行
(2).初始化子类静态成员变量和静态块,顺序执行
(3).初始化父类成员变量和代码块(直接{}的快),顺序执行
(4).执行父类构造函数
(5).初始化子类成员变量和代码块,顺序执行
(6).执行子类构造函数
总的顺序就是:先从上到下执行静态块,再出上到下执行{普通构造块-构造函数}

这一节主要讲的是类的加载,下一节则是JVM的运行时数据区。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值