类加载机制与对象初始化
一 . 类加载机制
-
类加载机制是指.class文件加载到jvm并形成Class对象的机制。之后应用可对Class对象进行实例化并调用。类加载机制可在运行时动态加载外部的类,还可以达到类隔离的效果。
-
类从而加载到虚拟机中开始,整个过程分为下图七个阶段,其中验证,准备,解析统称为解析。图中加载,验证,准备,初始化,卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种过程按部就班的开始,而解析则不一定,它在某些情况下可能在初始化之后才开始,这是为了支持java语言的运行时绑定。
1 . 装载: 装载过程负责找到二进制字节码并加载至JVM--即负责查找和导入class文件。
2 . 链接:
(1) 检验:链接过程负责对二进制字节码的格式进行检验,检查class文件数据的正确性 (2) 准备:初始化装载类中的静态变量,给类的静态变量分配存储空间 (3) 解析:解析类中调用的接口,类,将符号引用转为直接引用。
3 . 初始化:初始化过程即执行类中的静态初始化代码,构造器代码以及静态属性的初始化。以下四种情况会立即触发初始化
(1) 调用了new,读取或设置一个类的静态字段的时候,或者调用一个类的静态方法的时候。
(2) 反射调用了类中的方法
(3) 子类调用了初始化,如果子类初始化的时候发现父类尚未初始化,则会先出发父类的初始化。
(4) JVM启动过程中指定的初始化类(包含main方法的类)
(5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化,则需要先触发其初始化.
-
JVM 的类加载通过ClassLoader及其子类来完成,分别是Bootstrap ClassLoader,Extension ClassLoader,App ClassLoader 以及用户自定义的继承自ClassLoader抽象类的实现Custom ClassLoader。
-
(1) Bootstrap ClassLoader : 此类并非ClassLoader的子类,在代码中没办法拿到这个类的对象,Sun JDK会在启动时自动加载此类。此加载器会将存放于<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中
(2) Extension ClassLoader : 此加载器会将<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载.
(3) App ClassLoader : 此加载器会将用户类路径(ClassPath)上所指定的类库加载。
-
(4) Custom ClassLoader : 基于自定义的ClassLoader加载非classpath中的jar及目录,还可以在加载之前对class文件做一些动作,例如加密。
- JVM 的ClassLoader采用的是树形结构,除 Bootstrap ClassLoader外都会有父级的ClassLoader。加载类时通常会按照树形结构的原则进行。如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。 这样做的好处是:java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。
二 . 初始化
java会尽量保证变量在使用前得到恰当的初始化,对于局部变量,如果未初始化会得到编译时的错误,对于成员变量会自动初始化(0,null等)。
可以使用构造方法初始化成员变量,在运行时刻,可以调用方法或者执行某些动作来确定初值。但需要记住一点:无法阻止自动初始化的进行,它在构造方法被调用之前发生。对于所有基本类型和对象引用,包括在定义时已经指定初始值的变量,情况也是一样的.例如如下实例代码,i首先会被置为0,然后替换为指定的初始值7.
public class TestClass(){ int i;//自动初始化赋值为0 public static void main(String[] args){ i = 7;//赋值为7 } }
1 . 初始化的顺序:在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量的定义散布于方法定义之间,他们仍会在任何方法(包括构造方法)被调用之前得到初始化
对于如下代码:w3这个引用会被初始化两次,一次在调用构造方法之前,一次在调用期间,这时第一次引用的对象将被丢弃并被垃圾回收.
public class House { Window w1 = new Window(1); public House() { System.out.println("House()"); w3 = new Window(3_3); } Window w2 = new Window(2); public void method() { System.out.println("method()"); } Window w3 = new Window(3); public static void main(String[] args) { House house = new House(); } } class Window { Window(int order) { System.out.println("window" + order); } } //输出结果为: window1 window2 window3 House() window33
2 . 静态数据的初始化
静态初始化只有在必要的时候才进行,初始化的顺序是先静态对象(如果它们尚未因前面的的对象创建过程而被初始化),而后是“非静态”对象。
示例代码如下:
public class StaticInitailization { public static void main(String[] args) { System.out.println("creating new CupBoard() in main"); new CupBoard(); System.out.println("creating new CupBoard() in main"); new CupBoard(); table.f2(1); cupBoard.f3(1); } static Table table = new Table(); static CupBoard cupBoard = new CupBoard(); } class Bowl { public Bowl(int i) { System.out.println("Bowl(" + i + ")"); } void f1(int i) { System.out.println("f1(" + i + ")"); } } class Table { static Bowl bowl1 = new Bowl(1); Table() { System.out.println("Table()"); bowl2.f1(1); } void f2(int i) { System.out.println("f2(" + i + ")"); } static Bowl bowl2 = new Bowl(2); } class CupBoard { Bowl bowl3 = new Bowl(3); static Bowl bowl4 = new Bowl(4); CupBoard() { System.out.println("CupBoard()"); bowl4.f1(2); } void f3(int i) { System.out.println("f3(" + i + ")"); } static Bowl bowl5 = new Bowl(5); } // 结果如下: Bowl(1) Bowl(2) Table() f1(1) Bowl(4) Bowl(5) Bowl(3) CupBoard() f1(2) creating new CupBoard() in main Bowl(3) CupBoard() f1(2) creating new CupBoard() in main Bowl(3) CupBoard() f1(2) f2(1) f3(1)
3 . 对象的创建过程: 假设有个名为Dog 的类
(1) 即使没有显示的使用static关键字,构造方法实际上也是静态方法。因此,当首次创建Dog类的对象时,或者Dog类的静态方法或者静态变量被访问时,java解释器必须先查找类路径,以定位Dog.class文件
(2)载入Dog.class文件(这将创建一个Class对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在类首次加载的时候进行一次
(3)用new Dog 创建对象的时候,首先会在堆上为该对象分配足够的存储空间
(4)这块存储空间会被清零,这将会自动的为该对象的的所有基本数据类型赋初值
(5)执行所有出现于字段定义处的初始化动作。
(6)执行构造方法。
-
复杂对象调用构造方法的顺序:
(1) 在其他任何事物发生之前,将分配给对象的的存储空间初始化为二进制的零
(2) 调用父类的构造方法
(3) 按照声明顺序调用成员的初始化方法
(4) 调用子类构造方法的主体部分 -
编写构方法注意避免调用其他方法,在构造方法内唯一能安全调用的方法是父类中的final方法(也适用于private方法,他们自动属于final方法)。这些方法不能被override(覆写)
参考书籍:分布式java应用基础与实践--林昊 3.1.2节 类加载机制
参考博客1:深入理解Java:类加载机制及反射
参考博客2:Java中普通代码块,构造代码块,静态代码块区别及代码示例