一、类加载的过程
提到类加载,从字面上理解是把class类加载到内存中区,实际上JVM将类的加载是分为3个重要的阶段,加载,连接(验证,准备,解析),初始化。具体可用下图来表示:
加载:
将类的二进制数据加载到内存当中。将.class的二进制数据读入到内存中,将其放在运行时数据区的方法区(虚拟机运行环境中),然后在内存中创建一个java.lang.Class对象(规范并未说明Class位于哪,HotSpot虚拟机将其放置在方法去中,class对象是一枚镜子,反应了整个类的结构,也是反射的根源)用来封装类在方法区内的数据结构。
连接:
(1)验证:
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
- 文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
- 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
- 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
- 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。
(2)准备
准备阶段是正式为类静态变量分配内存并为其设置默认值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
- 这时候为类的静态变量分配内存,而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 这里所设置的初始值通常情况下是数据类型默认值(如0、null、false等),而不是被在Java代码中被显式地赋予的值。
(3)解析
将类中的符号引用特换为直接引用。这里不做详细阐述,有兴趣的可以去了解下。
初始化:
初始化是将类中的静态变量赋予正确的初始化值。静态变量声明语句,静态代码块都被看作是类的初始化语句。java虚拟机会按照初始化语句在类文件中的顺序一次执行他们。
关于实例变量:
类实例化后为实例变量分配内存,赋予默认值,最后初始化为默认值。java虚拟机为每一个类的实例创建一个初始化方法< init>,同时为每个构造方法也分配一个< init>。
二、类的初始化过程详解。
一段摘自
首先我们需要知道java对类的使用方式分为2种:主动使用和被动使用。
每个类或接口只有被java程序主动使用时才会被初始化。
主动使用主要分为以下七种:(后续通过代码对其中的6种进行验证)
- 创建类的实例。(new 关键字)
- 访问类或接口(直接定义的)静态变量,或对静态变量赋值。(助记符分别为getstatic putstatic。被final修饰、已在编译期把结果放入常量池中的除外)。
- 访问类(直接定义的)静态方法。(助记符为invokestatic)
- 反射。
- 初始化一个类的子类。(注:这个规则不适用于接口:
1.在初始化一个类时,并不会初始化其所实现的接口
2.在初始化一个接口时,并不会先初始化它的父接口
因此一个父接口,并不会其子接口或者实现类的初始化而初始化,只有当程序首次使用特定的接口静态变量时,才会导致该接口的初始化。) - java虚拟机启动时被标记为启动类的类
当使用jdk1.7的动态语言支持时,如果一个java.long.invoke.MethodHandle实例最后的的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic
下面我们就通过代码来验证这几个主动使用是否导致类的初始化。
- 创建类的实例
代码片如下
.
public class Test9 {
public static void main(String[] args){
Parent9 parent = new Parent9();
}
}
class Parent9{
static {
System.out.println("Parent9初始化!");
}
}
这个比较简单,不做详细阐述,运行结果如下:
结论:创建类的实例会导致类的初始化
2. 访问类或接口(直接定义的)静态变量,或对静态变量赋值。(助记符分别为getstatic,putstatic)。
我们同样通过下面一段代码来验证,可以先思考下自己认为的结果,然后在运行结果验证,可以加深自己的印象。
代码片
.
public class Test2 {
public static void main(String[] args)
{
System.out.println(Child2.str);
}
}
class Parent2 {
public static String str = "hello world";
static {
System.out.println("Parent2 初始化");
}
}
class Child2 extends Parent2{
static {
System.out.println("Child2 初始化");
}
}
运行结果:
可能和某些友友想的不太一样。
注意这段主题内容我标红的地方,对于一个静态字段而言,只有直接定义了该类的字段的类才会被初始化。虽然Child2继承了Parent2,也有了此变量,但是并非Child2直接定义,所以并不会导致Child2的初始化。
对于接口的验证由于无法使用静态块。我们可以使用非静态块来大致验证下:
代码片
.
public class Test6 {
public static void main(String[] args){
System.out.println(Parent6.thread);
}
}
interface Parent6{
Thread thread = new Thread(){
{
System.out.println("interface Parent6 block!");
}
};
}
结果如下:
可以看出访问一个接口的静态变量会导致接口的初始化。这边我们使用的是运行时常量。具体关于常量的分析,下文也会讲到。
访问静态方法这里就不阐述了 ,和静态变量的访问大致类似。有兴趣的可以,自己验证下。
在上面对于静态变量或者静态方法访问的例子中,也可以通过查看助记符的方式来看类是否初始化。我们反编译一下刚刚的Test2文件看下相应的助记符,如下图所示:
着重看运行的这一行,访问静态变量时助记符为getstatic,这也是验证的一种方式。看到这些助记符说明类被初始化了。还有putstatic静态变量赋值,invokestatic访问静态方法等。
- 反射
反射我们使用Class.forName()以及ClassLoader系统类加载器中的loadClass方法来对比验证下:代码片如下
.
public class Test7 {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classzz = ClassLoader.getSystemClassLoader();
classzz.loadClass("com.newgain.classloader.Parent7");
System.out.println("==================");
Class classzzz = Class.forName("com.newgain.classloader.Parent7");
}
}
class Parent7{
static {
System.out.println("Parent7初始化");
}
}
运行结果:
可以看出只有反射Class.forName导致类的初始化。
- 初始化一个类的子类
把上面Test2的例子稍微变动下就可以:代码片如下
.
public class Test2 {
public static void main(String[] args)
{
System.out.println(Child2.str1);
}
}
class Parent2 {
public static String str = "hello world";
static {
System.out.println("Parent2 初始化");
}
}
class Child2 extends Parent2{
public static String str1 = "i love my country!";
static {
System.out.println("Child2 初始化");
}
}
结果如下:
我们访问Child2,它的父类Parent2被初始化了!这是对于类而言。对于接口而言并非如此,但是在举接口的例子时,这边要先把常量与类的初始化的关系先阐述一下。
说到常量,根据编译器的不同行为其实也会分为编译时常量和运行时常量。先看下面这段代码:代码片如下
.
public class Test3 {
public static void main(String[] args){
System.out.println(Parent3.str);
}
}
class Parent3 {
public static final String str = "happy birthday!";
static {
System.out.println("Parent3 的初始化");
}
}
可以先思考下结果。运行结果如下:
Parent3并没有被初始化!amazing!我们增加-XX:+TraceClassLoading虚拟机参数来看下运行这段程序都加载了哪些类,我们看主要的部分,如下图:
查看了下虚拟机连Parent3这个类都没有加载,那更不要谈初始化了,类Parenta3肯定没初始化。所以得到如下结论:
编译器具有常量优化机制。常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中, 本质上调用类并没有直接引用到定义常量的类,以此并不会触发Parent3的初始化。(是将常量存入到Test3的常量池中,之后Test3和Parent3没有任何关系了,甚至可以将Prent1的class文件删除。我们通过类的加载情况也证明了这一点。) 接下来在把上面的代码做下修改,把 public static final String str = “happy birthday!”;改成 public static final String str =UUID.randomUUID().toString();我们得到的结果如下:
Parent3被初始化了。这就是编译常量和运行常量在初始化的区别。 当一个常量的值并非编译期决定的,那么其值就不会在调用期间放入调用类的常量池中,这时程序运行会主动使用该常量所在的类,当然会导致该类的初始化。下面讲下本人遇到的一个比较细的问题:还是上面这段程序,我们把 public static final String str = “happy birthday!”; 这句话改下。改为 public static final Integer i = 127;可能有能有些朋友认为结果就是输出127,并不会导致Parent3类的初始化。并非如此,运行结果请看下图:
事实上Parent3被初始化了,其实包装类的写法在编译时要被编译器改成public static finalInteger i = Integer.valueOf(127);只是简化了写法。实际上属于运行时常量。会导致类的初始化。其他包装类常量也是如此。反编译Test3也可以在这一行看到getstatic助记符,证明类被初始化了。
- 在说回一个接口的初始化是否会导致其父类的初始化呢。大家知道接口中都是常量,那我们要用运行时常量来验证这一点。
代码片如下
.
public class Test5 {
public static void main(String[] args){
System.out.println(Childa.str);
}
}
interface Parent5 {
Thread thread = new Thread(){
{
System.out.println("Prent5初始化!");
}
};
}
interface Child5 extends Parent5 {
String a = UUID.randomUUID().toString();
Thread thread = new Thread(){
{
System.out.println("Child5初始化!");
}
};
}
结果如下:
Prent5没有被初始化,与类不一样。所以访问一个接口的子接口并不会导致其父接口的初始化。只有访问父接口直接定义的(运行时)常量时,父接口才会被初始化。
前六个的主动使用会使类初始化的验证,就总结到这了。仅以此记录自己的学习过程。有位大神说过,有输入,就要有输出这样才能更好的吸收知识。fighting!