注:本文大多都是本人收集的资料总结,并非原创。
一.Java类的装载、链接和初始化(摘自网络)
装载 链接 初始化
其中装载阶段又三个基本动作组成:
通过类型的完全限定名,产生一个代表该类型的二进制数据流
解析这个二进制数据流为方法区内的内部数据结
构创建一个表示该类型的java.lang.Class类的实例
另外如果一个类装载器在预先装载的时遇到缺失或错误的class文件,它需要等到程序首次主动使用该类时才报告错误。
链接阶段又分为三部分:
1.验证(verification)
链接的第三部解析会把类中成员方法、成员变量、类和接口的符号引用替换为直接引用,而在这之前,需要检测被引用的类型正确性和接入属性是否正确(就是 public ,private 的的问题),诸如检查 final class 又没有被继承,检查静态变量的正确性等等。(注意到实际上有一部分验证过程已经在加载的过程中执行了。)
2.准备(preparation)
对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(因为这里不会执行任何 Java 代码)。具体如下:
所有原始类型的值都为 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底层实现大多使用 int),引用类型则为 null。值得注意的是,JVM 可能会在这个时期给一些有助于程序运行效率提高的数据结构分配空间。比如方发表(类似与 C++中的虚函数表,参见另一篇博文《Java:方法的虚分派和方法表》)。
3.解析(Resolution)
为类、接口、方法、成员变量的符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。
这一步是可选的。可以在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即使运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!
当一个类被主动使用时,Java虚拟机就会对其初始化,如下六种情况为主动使用:
当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
当调用某个类的静态方法时
当使用某个类或接口的静态字段时
当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
当初始化某个子类时
当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
让我们看一下代码:
package trying.classLoad;
class Parent {
protected static int c = 10;
static {
System.out.println("Parent Initial");
}
public static void say() {
System.out.println("number=" + c);
}
}
class Son extends Parent {
static {
System.out.println("Son Initial");
}
}
/**
* @ClassName: TestInitial
* @Description: TODO(这里用一句话描述这个类的作用)
* @author wangcc
* @date 2016-11-30 下午1:50:06
*
* 由此可见,一般的,我们在某个类中定义了其他类的成员变量引用,只要该变量没有 new 出一个新的对象,则 JVM
* 也不会初始化这个类,类此时只是被加载了而已,而没有链接和初始化。
*/
public class TestInitial {
/**
* @Title: main
* @Description: TODO(这里用一句话描述这个方法的作用)
* @param @param args 设定文件
* @return void 返回类型
* @throws
*/
static {
System.out.println("TestInitial Initial");
}
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(Son.c);
Son.say();
}
}
结果为:
由此可见,一般的,我们在某个类中定义了其他类的成员变量引用,只要该变量没有 new 出一个新的对象,则 JVM 也不会初始化这个类,类此时只是被加载了而已,而没有链接和初始化。
让我们再来看一段代码:
package trying.classLoad;
package trying.classLoad;
public class TestLoad {
static {
System.out.println("静态代码块");
}
public TestLoad() {
System.out.println("创建类实例");
}
}
package trying.classLoad;
public class Test {
public static void main(String[] args) {
try {
// Class.forName("trying.classLoad.TestLoad");
// Class.forName("trying.classLoad.TestLoad", true,
// Test.class.getClassLoader());
Class.forName("trying.classLoad.TestLoad", true,
Test.class.getClassLoader());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
运行Test.java可以得到控制台输出为:静态代码块
当把Test类中的Class.forName(“trying.classLoad.TestLoad”, true,
Test.class.getClassLoader());中的true改为false时,我们再运行Test发现,控制台并没有任何输出,这是为什么呢:
查看JDK API 我们发现,只有当initialize 参数为 true 且以前未被初始化时,才初始化该类。 所以这时的类是没有经过初始化的,也就证明了static代码块是在类初始化时执行,而不是在链接过程中执行的。
最后再贴一段大神的代码:
package trying.classLoad;
import java.util.Random;
class Initable {
public static final int staticFinal = 47;
public static final int staticFinal2 = ClassInitializationTest.rand
.nextInt(1000);
static {
System.out.println("初始化Initable");
}
}
class Initable2 {
public static int staticNotFinal = 147;
static {
System.out.println("初始化Initable2");
}
}
class Initable3 {
public static int staticNotF = 74;
static {
System.out.println("初始化 Intiable3");
}
}
/**
* @ClassName: ClassInitializationTest
* @Description: TODO(这里用一句话描述这个类的作用)
* @author wangcc
* @date 2016-11-30 上午11:55:45 1.首先说一下 Class initable =
* Initable.class;,他的意思是仅仅使用Class的方法将
*
* Initable.java的字节码装载到虚拟机中,得到对类的引用,并不初
*
* 始化类,更不可能执行static代码块
*
* 2.加载完字节码后,直接访问Initable的static final常量,没有任何
*
* 问题,说明虽然类没有被初始化,但是static final常量已经可以被
*
* 访问(这个是解释接口的重点)
*
*
* 3.当访问Initable的在编译时候没有指定值的static final常量的时
*
* 候,他就不能直接访问,看运行结果可知道,他需要初始化类,
*
* 即调用static静态代码块,才可以使用.
*
*
* 4.当调用Initable2的static变量的时候,类就必须被执行初始化才能
*
* 被访问 5.当调用Class.Forname()方法的时候,将类装载进入虚拟机,就
*
* 果断执行了static代码块,初始化后才能继续访问.
*
*
* 6.因此总结一下,一个类的初始化准备工作如下
*
* 6.1加载:这个由类加载器完成,他去查找字节码,并且创建一个
*
* Class对象
*
* 6.2连接:验证类中的字节码,为静态域分配存储空间,并且如果
*
* 必须的话,将解析这个类创建的对其他类的引用
*
* 6.3初始化:如果这个类有超类,则对其进行初始化,执行静态初
*
* 始化器和静态初始化代码块
*
* 6.4初始化被延迟到了对静态方法(构造器是隐士的静态方法)或
*
* 非静态方法的首次引用才执行
*
* 7.由上面可以知道,接口不能被初始化,如果想定义常量,必须是
*
* 在接口的字节码被装载到虚拟机的时候他的常量就得被访问,所 以他必须是static final的
*
* 8static final叫编译期常量,不需要初始化就能读取。
*/
public class ClassInitializationTest {
public static Random rand = new Random(47);
@SuppressWarnings("rawtypes")
public static void main(String[] args) throws ClassNotFoundException {
Class initable = Initable.class;
System.out.println("After creating Initable ref");
System.out.println(Initable.staticFinal);
System.out.println(Initable.staticFinal2);
System.out.println(Initable2.staticNotFinal);// 访问static的值
Class initable3 = Class.forName("trying.classLoad.Initable3");// 装载并且初始化
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNotF);
}
}