本博客主要讨论类加载过程中主要的内容,如果想了解更加详细的内容可以参考JVM规范或者其他数据。本文的主要参考资料为《深入理解Java虚拟机》。
一 类的生命周期
1 类的生命周期
说明:在类加载过程中,加载、验证、准备、初始化、卸载这几个阶段的顺序是固定的,而解析的阶段不固定。
2 加载
1) 主要职责
通过类的权限定名来获取此类的二进制字节流
将字节流代表的静态存储结构转换成方法去的运行时数据结构
在内存中生成一个代表这个类的Class对象,作为方法区这个类的访问入口
2) 说明
加载阶段与连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。但是两者的开始时间仍然保有固定的先后顺序
3 验证
1) 主要职责
确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟节自身安全。
包括:文件格式验证、元数据验证,字节码验证、符号引用验证。
2) 文件格式验证
检查class文件格式是否符合规范,并且能够被当前虚拟节处理。例如:魔数是否是0Xcafebabe,主次版本号是否被jvm接受,常量池中是否有不被支持的常量类型等等。
3) 元数据验证
对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。
例如:是否有父类(出object类以外都应该有父类);是否继承了不允许继承的类(final修饰的类);如果不是抽象类,是否实现了父类、父接口中的所有抽象方法等等。
4) 字节码验证
通过数据流和控制流分析,确定程序语义是合法性、符合逻辑的。主要是对类的方法进行校验分析,确保运行时不会做出危害虚拟机安全的事情。
例如:确保操作数栈的数据类型与指令集能匹配工作;保证跳转指令不会包含跳转到方法外的字节码指令上;确保类型转换是有效的等等。
5) 符号引用验证
此校验发生在解析阶段中进行,可以认为是对类自身以外的信息进行匹配性校验。此阶段的目的是确保解析动作能正常执行。
例如:符号引用的类是否能够被找到;符号引用指定的类中是否存在执行的尚需经、方法;符号引用的类、字段、方法是否可以被当前类访问。
4 准备
1) 主要职责
为类变量(static修饰的)变量分配内存并设置初值(也称之为零值)。
这些变量所需的内存在方法区中分配
2) 数据类型的零值
数据类型 |
零值 |
int |
0 |
long |
0L |
short |
(short)0 |
char |
'\u0000' |
byte |
(byte)0 |
boolean |
false |
float |
0.0f |
double |
0.0d |
reference |
null |
3) 例外
对于使用final修饰的类变量,在准备阶段将会被设置为指定的值。例如:public static final int value = 1 在准备阶段完成以后,value的值是1。
4) 字节码比较
以下示例的差别是一个使用final修饰,一个不使用final修饰,从编译出来的字节码我们看到使用final修饰的类变量的初始值就被设置成了1
a) 类变量示例
Ø 示例代码
public class StaticTester {
public static int value = 1;
}
Ø 编译后的字节码
Constant pool:
#1 = Class #2 //com/wzf/greattruth/jvm/classinit/StaticTester
#2 = Utf8 com/wzf/greattruth/jvm/classinit/StaticTester
#3 = Class #4 //java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 value
#6 = Utf8 I
#7 = Utf8 <clinit>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Fieldref #1.#11 //com/wzf/greattruth/jvm/classinit/StaticTester.value:I
#11 = NameAndType #5:#6 // value:I
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 <init>
#15 = Methodref #3.#16 //java/lang/Object."<init>":()V
#16 = NameAndType #14:#8 // "<init>":()V
#17 = Utf8 this
#18 = Utf8 Lcom/wzf/greattruth/jvm/classinit/StaticTester;
#19 = Utf8 SourceFile
#20 = Utf8 StaticTester.java
{
public static int value;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0,args_size=0
0:iconst_1
1:putstatic #10 // Field value:I
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
b) final修饰类变量示例
Ø 示例代码
public class StaticTester {
public static final int value = 1;
}
Ø 编译后的字节码
Constant pool:
#1 = Class #2 //com/wzf/greattruth/jvm/classinit/StaticTester
#2 = Utf8 com/wzf/greattruth/jvm/classinit/StaticTester
#3 = Class #4 //java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 value
#6 = Utf8 I
#7 = Utf8 ConstantValue
#8 = Integer 1
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Methodref #3.#13 //java/lang/Object."<init>":()V
#13 = NameAndType #9:#10 // "<init>":()V
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/wzf/greattruth/jvm/classinit/StaticTester;
#18 = Utf8 SourceFile
#19 = Utf8 StaticTester.java
{
public static final int value;
descriptor: I
flags: ACC_PUBLIC,ACC_STATIC, ACC_FINAL
ConstantValue: int 1
publiccom.wzf.greattruth.jvm.classinit.StaticTester();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1,args_size=1
0: aload_0
1:invokespecial #12 //Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wzf/greattruth/jvm/classinit/StaticTester;
5 解析
1) 主要职责
将常量池的符号引用替换成直接引用的过程。虚拟机规范中没有规定解析阶段的具体发生时间,只要求在执行anewarray, checkcase, getfield, getstatic, instanceof,invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual,ldc, ldc_w, multianewarray, new, putfield, putstatic这16个指令前,先对他们使用的符号引用解析。
解析主要包括:类或接口的解析,字段解析,类方法解析,接口方法解析
6 初始化
1) 主要职责
是执行类构造器<clinit>()方法的过程,即按照程序去初始化类变量和其他资源。
2) clinit
<clinit>()方法是由编译器自动收集类中的所有变量赋值动作、静态语句块合并产生的。编译器收集的顺序是由语句在代码中出现的顺序决定。
<clinit>()方法不需要显示调用父类的构造器,虚拟节会保证子类<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。所以虚拟机中第一个被执行的<clinit>()方法一定是Object的<clinit>()方法。
父类中<clinit>()方法先执行,意味着父类中的静态语句块要优先于子类的静态语句块。
<clinit>()方法是非必须的,如果类中没有静态语句块,也没有对变量赋值操作,那么编译器可以为这个类生成<clinit>()方法。
接口中可以存在赋值语句,所以编译器也可能为接口生成<clinit>()方法。注意接口中执行<clinit>()方法前不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。接口的实现类在初始化时,也不需要执行接口的<clinit>()方法。
虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。多线程同时初始化一个类时,只有一个线程执行<clinit>()方法,其他的线程都需要阻塞等待。
3) 初始化
虚拟机规定了有且只有5中情况需要对类进行初始化:
Ø 遇到new(实例化对象), getstatic(读取一个静态字段), putstatic(设置一个静态字段), invokestatic(调用静态方法)这4个指令时,如果类还没有初始化,那么要先触发器初始化。
Ø 使用java.lang.reflect报的方法对类进行反射调用时,如果类还没有初始化,那么先触发器初始化。
Ø 初始化一个类时,如果父类未初始化,则先触发其父类的初始化。
Ø 虚拟机启动时,用户需要制定一个要执行的主类,虚拟机先初始化主类。
Ø 使用1.7动态语言支持是,如果一个MethodHandler实例最后解析结果是REF_getStatic,REF_putStatic, REF_invokeStatic的方法句柄,并且这个方法句柄对应的类未初始化时,那么先触发其初始化。
二 类加载器
1 双亲委派工作过程
如果一个类加载器收到了类加载请求,他首先将这个类加载请求委派给父类加载器去完成(每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该先传送到顶层的启动类加载器中),只有父类加载器反馈无法完成加载请求时,子加载器才会尝试自己去加载。
2 双亲委派模型
示例为tomcat的双亲委派模型
1) jdk自定义的类加载器
BootstrapClassLoader由C++语言实现,是虚拟机自身的一部分;负责加载<JAVA_HOME>/lib目录下或者被-Xbootclasspath参数指定的类库。无法被java程序直接引用。
ExtensionClassLoader由sun.misc.Launcer$ExtClassLoader实现;负责加载<JAVA_HOME>/lib/ext目录中的或者被java.ext.dirs指定路径的jar,此类加载器可以被开发者直接使用。
ApplicationClassLoader由sun.misc.Launcher$AppClassLoader实现。负责架子用户类路径上的jar。此类可以被开发者直接使用。
2) Tomcat定义的类加载器
CommonClassLoader加载/common目录中的类库。这些类库可被Tomcat和所有的web应用程序共同使用
CatalinaClassLoader加载/server目录中的类库。这些类库可被Tomcat使用,对所有的Web应用程序都不可见
SharedClassLoader加载/shared目录中的类库。这些类库可被所有web应用程序共同使用,但对Tomcat自己不可见
WebAppClassLoader加载/WebApp/WEB-INF目录中的类库。类库仅仅可以被此web应用程序使用,对Tomcat和其他web应用程序都不可见
3 热部署
以后提供一个示例,详细讲述热部署,等待以后更新