Java虚拟机入门(2)------类加载机制(上)

来源:互联网 发布:网络创业优势 编辑:程序博客网 时间:2024/06/07 16:53

经过Java虚拟机入门(1)后,相信大家对Class文件的字节码形式有了一定理解,接下来我们将讲解类加载机制,这也是android动态加载机制的重要基础。

Java虚拟机入门(2)—–类加载机制 系列我会分为上、中、下来讲解,这样不至于在一篇博客内融入太多知识点而显得冗杂,希望大家喜欢。
今天这一篇是类加载机制系列的第一篇,主要讲述以下几点内容:

一:类的生命周期总览
二:类加载的时机
三:类加载的详细过程之上篇

接下来让我们开始吧!

一:类的生命周期

类从被加载到虚拟机内存到从虚拟机中卸载,经历了一下几个阶段:
1.加载
2.验证
3.准备
4.解析
5.初始化
6.使用
7.卸载
用图表示如下:

这里写图片描述

从图可以看出,加载、验证、准备、解析、初始化、使用和卸载的顺序是确定的,类加载过程必须按照这种顺序开始。但是注意,按照这种顺序开始不意味着按照这种顺序进行,事实上,这些阶段是交叉进行的,在前一个过程还没结束的时候,后一个过程已经开始了,这点要特别注意。

二:类加载机制的时机

java虚拟机没有对类加载机制的第一个阶段—–加载 进行严格的要求,什么时候开始由java虚拟机的具体实现决定。但是java虚拟机对于类的初始化过程却有相当严格的规定:有且只有以下四种情况下,类必须立即进行初始化(也就意味着加载、验证、准备要在此之前开始):

1、遇到new、getstatic、putstatic或者invokestatic这4条字节码指令时。

通俗地说,对应于new一个类实例,get一个静态变量,set一个静态变量以及调用一个静态方法。但是有一种情况例外,final修饰的静态变量不会使得类进行初始化。为什么呢?还记得吗?final修饰的static变量实际上是一个常量,在编译期就已经存放在常量池中了。

2、使用反射机制reflect对一个类进行反射调用时,如果这个类没有进行初始化,则会先触发它的初始化才被反射。

3、当初始化一个类,如果它的父类没有初始化,则会先初始化父类再初始化子类。(接口的加载时机只有这里和类不一样,接口并不需要初始化全部父接口,只有在真正用到父接口的时候,比如使用父接口的变量,才对父接口进行初始化)

4、当虚拟机启动时,含有main函数的那个类(也就是主类)会先被初始化。

除了上面提到的4种情况下的引用类成为主动引用,其余对类的引用都不会对这个类进行初始化,这种情况下的引用成为被动引用。

具体点来说,可能会遇到的被动引用有以下三种:

1、当调用子类的父类的static字段时,子类不会被初始化。

2、当一个类的static字段是final修饰的,这个类不会被初始化。

3、当new的是以一个类为item的数组,不会初始化这个类。

下面通过具体的例子来对这三种情况进行说明:

1、当调用子类的父类的static字段时,子类不会被初始化。

package javaLearning;/** * @author jacky * */class FatherClass{    static{        System.out.println("FatherClass is inited");    }    public static int a = 1;}class SonClass extends FatherClass{    static{        System.out.println("SonClass is inited");    }}public class Test2{    public static void main(String[]args){        System.out.println(SonClass.a);    }}

输出:
FatherClass is inited
1

和我们说明的一致,对于父类的static变量进行引用,并不会初始化子类。

2、当一个类的static字段是final修饰的,这个类不会被初始化。

在上面的例子中,我们看到输出了“FatherClass is inited”,说明在引用FatherClass的static变量时,FatherClass确实被初始化了。那我们来实验一下,如果在static变量前面加上final会怎么样呢?
答案是只输出1,而没输出“FatherClass is inited”,这和我们预期是一样的,原因是final修饰的static变量在编译期就已经写入常量池了,而且写入的并不是FatherClass的常量池,而是另一个类NotInitialization类的常量池,这个类用于存储常量。而我们平时对常量FatherClass.a引用实际上都是转化为NotInitialization类对自身的常量池的引用。

3、当new的是以一个类为item的数组,不会初始化这个类。

实验的代码如下:

package javaLearning;/** * @author jacky * */class FatherClass{    static{        System.out.println("FatherClass is inited");    }    public final static int a = 1;}class SonClass extends FatherClass{    static{        System.out.println("SonClass is inited");    }}public class Test2{    public static void main(String[]args){        SonClass[] a = new SonClass[233];    }}

运行后发现没有输出,意味着SonClass没有被初始化,这又是为什么呢?因为像这种数组类型在运行后并不是直接触发SonClass,而是触发另一个类“LjavaLearning.SonClass“的初始化阶段。这个类并不是一个用户类,而是由字节码指向newarray生成的一个类,代表一个元素类型为javaLearning.SonClass的一维数组。

三:类加载的详细过程

(一)加载

类加载机制的第一个阶段是加载过程,这个过程主要做以下三件事情:

(1)通过类的全限定名获得定义此类的二进制字节流
(2)将这个字节流存放到方法区
(3)在java堆中生成一个代表此类的java.lang.Class对象,作为对方法去这些数据的访问入口

在这三件事情中,(1)阶段具有最强的可控性,因为java虚拟机并没有给出从哪里获取二进制字节流以及怎么获取二进制字节流的方法,这完全取决于开发人员,许多java技术就建立在这里:

(1)从zip包读取,这是JAR,WAR,EAR的基础
(2)从网络获取,比如Applet
(3)运行时计算生成,比如动态代理技术(Proxy)
(4)由其他文件生成,比如JSP应用
(5)从数据库中读取,比如某些中间件服务器,用于完成程序代码在集群中的分发

(二)验证

连接阶段分为三块:验证、准备、解析。首先来看验证阶段。

验证阶段的目的是为了确保Class文件的字节流符合java虚拟机的要求,并且不会伤害到虚拟机。

符合java虚拟机的要求这点很容易理解,如果不符合一个Class文件应有的结构,那么java虚拟机也就没必要对这个文件进行解析了。那伤害虚拟机是什么意思呢?大家也知道,java虚拟机接收到的Class文件本质上是字节码文件,更本质地说就是十六进制文件。那么?有没有一种可能,我只要是一个十六进制文件,并且伪装地符合Class文件的结构,但本质上我并不是一个合格的字节码文件,比如我会访问到非法内存等等?这种可能是存在的,因此为了java虚拟机自身的安全,验证显得就非常重要了。

说了这么多验证的重要性和必要性,验证到底是怎么实现的呢?很遗憾,java虚拟机只说了,“如果验证到输入的字节流文件不符合Class文件的规范,就抛出一个异常”,但是没有说怎么验证,因此不同的虚拟机对于验证这一块的实现或许有不同的地方。不过,以下四点基本上每个合格的虚拟机都应该进行验证(请注意每一点验证针对的是什么):

1、文件格式验证:验证字节流是否符合Class文件格式的规范,并且被当前版本的虚拟机处理。

文件格式验证主要针对的是字节流。
这一阶段可能会验证以下几点:

(1)是否以魔数0xCAFEBABE开头
(2)主、次版本是否在当前虚拟机的处理范围内
(3)常量池中的常量是否有不被支持的类型(检查tag)
。。。。。。

这一阶段是针对字节流进行的,经过这一阶段验证完毕后字节流文件才能进入内存的方法区进行储存,所以后面的三个阶段都是基于方法区的存储结构进行的。

2、元数据验证:对字节码描述的消息进行语义分析。

元数据验证针对的是类,主要是对类的合格性进行验证,主要包括以下几点:

(1)这个类是否有父类(除了java.lang.Object外,其余类都应该有父类)
(2)这个类的父类是否继承了不被允许继承的类(被final修饰的类)
(3)如果这个类不是抽象类,是否实现了父类或者接口中要求实现的全部方法
(4)类中的字段、方法是否与父类产生矛盾(比如覆盖了父类的final字段,和父类的方法名字参数一样,却返回不同的类型)

3、字节码验证:进行数据流和控制流的分析

字节码验证针对的是类的方法体。注意这里和元数据验证区分开来,字节码验证是对类的方法体进行验证,也就是方法的具体实现,而元数据验证仅仅是对全部的类方法进行宏观上的验证。
字节码验证可能从以下几点入手:

(1)保证任意时刻操作数栈的数据结构与指令代码序列都能配合工作,保证不会出现类似于往操作数栈中放入一个int类型的数据,使用时却按照long类型来加载到本地变量表中。
ps:关于操作数栈和本地变量表的知识可以参见这篇博客

(2)保证跳转指令不会跳转到方法体以外的字节码指令上

(3)保证方法体的类型转化是安全的。比如可以将子类对象赋值给父类数据类型,但是相反则不可以。

ps:上述的字节码验证(3)称作类型推导,为了节省时间,提高效率,在jdk1.7后,已经用类型检查代替类型推导了。类型检查是在方法体的code字段的属性表中加入一项“StackMapTable”,这项属性描述了方法体中所有的基本块(按照控制流拆分的代码块)开始时本地属性表和操作栈应该有的状态。如果验证错误,则说明字节码验证失败。

4、符号引用验证

符号引用验证主要针对的是常量池中的各种符号引用。
而且它发生在连接的第三个阶段—–解析阶段。
【还记得吗?虽然java类加载机制的各个阶段的开始时间是严格按照顺序进行的,但是过程却是交叉进行的。】

这个验证主要做以下的事情:

1、通过字符串描述的全限定名是否能找到对应的类。
2、在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
3、符号引用中的类、方法、字段能否被当前类访问。

ps:关于符号引用和直接引用的关系可以参见这里

(三)准备阶段:正式为类变量分配内存并设置类变量的初始值

准备阶段需要注意的地方有两点:

(1)只为类变量(static修饰)分配内存
(2)类变量的初始值“通常情况下“是数据类型的零值

下面对这两个地方进行详细阐述:

(1)只为类变量(static修饰)分配内存

类变量对应的就是实例变量了,那么实例变量在哪里进行分配内存呢?实例变量要等到对象实例化的时候跟随对象一起分配在java堆中。

(2)类变量的初始值“通常情况下“是数据类型的零值

举个栗子:public static int a = 1;

请问,a在准备阶段的初始值是多少?如果回答是1就错了。类变量的初始值通常情况下都是零值。那什么情况下会是1呢?就是特殊情况了:
public static final int a = 1
这种情况下就是1了。
因为有final修饰,因此final修饰的static变量在编译期就已经写入常量池了,而且写入的并不是a所在的类的常量池,而是另一个类NotInitialization类的常量池,这个类用于存储常量,并且在a所在的类用ConstantValues属性指引。而我们平时对常量a引用实际上都是转化为NotInitialization类对自身的常量池的引用。而准备阶段读取的时候,就会将a初始化为ConstantValues属性指定的值,也就是1了。

好了,这就是类加载机制(上)的全部内容了,希望透过这篇博客,大家对于类加载机制有一个入门的认识,后面将会继续呈上类加载机制(中),请大家尽情期待!

0 0
原创粉丝点击