java类加载常见的几种问题理解类加载机制

  • Post author:
  • Post category:java

类加载过程中的相关概念详见-
http://m.myexception.cn/program/1602930.html
http://www.codeceo.com/article/java-class-loader-learn.html
http://blog.csdn.net/ns_code/article/details/17881581
理解类加载过程的5步。
Round1:
首先请理解如下代码:

class X{

    Y y=new Y();
    public X(){

        System.out.print("X");
    }
}
class Y{

    public Y(){
        System.out.print("Y");
    }
}
public class Z extends X{
    Y y=new Y();
    public Z(){
        System.out.print("Z");
    }
    public static void main(String[] args) {
        new Z();
    }
}

初始化过程:
1. 初始化父类中的静态成员变量和静态代码块;
2. 初始化子类中的静态成员变量和静态代码块;
3. 初始化父类的普通成员变量和普通代码块,再执行父类的构造方法;
4. 初始化子类的普通成员变量和普通代码块,再执行子类的构造方法;

(1)初始化父类的普通成员变量和代码块,执行 Y y=new Y(); 输出Y
(2)再执行父类的构造方法;输出X
(3)初始化子类的普通成员变量和代码块,执行 Y y=new Y(); 输出Y
(4)再执行子类的构造方法;输出Z
所以输出YXYZ

Round 2.
对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)>构造器。我们也可以通过下面的测试代码来验证这一点:

public class InitialOrderTest {
// 静态变量
public static String staticField = "静态变量";
// 变量
public String field = "变量";
// 静态初始化块
static {
System.out.println(staticField);
System.out.println("静态初始化块");
}
// 初始化块
{
System.out.println(field);
System.out.println("初始化块");
}
// 构造器
public InitialOrderTest() {
System.out.println("构造器");
}
public static void main(String[] args) {
new InitialOrderTest();
}
}

运行以上代码,我们会得到如下的输出结果:
1. 静态变量
2. 静态初始化块
3. 变量
4. 初始化块
5. 构造器
再看以下代码

public class InitialOrderTest{
  static{
             b=6;
    }
    static int a=5;
    static int b=9;
    static int c;
    public static void main(String[] args) {
        System.out.println(Test.b);
    }

}

输出9.
JVM初始化一个类主要包括以下步骤:
1)若该类还没有被加载连接,则先加载并连接该类(这里连接包括验证;准备-准备阶段会为类变量分配内存,并设置默认初始值即系统默认值;解析)
2)若该类的直接父类还没有被初始化,则先初始化其直接父类
3)若类中有初始化语句,则系统按照顺序执行这些初始化语句
在类的初始化阶段,JVM负责对类进行初始化,主要就是对类变量的初始化。
当执行第2步时,系统对直接父类的初始化步骤也遵循1-3;如果该直接父类又有直接父类,则再次重复这三个步骤,依次类推,所以JVM最先初始化的总是java.lang.Object.

Round 3:

class Parent {
 // 静态变量
 public static String p_StaticField = "父类--静态变量";
 // 变量
 public String p_Field = "父类--变量";
 protected int i = 9;
 protected int j = 0;
 // 静态初始化块
 static {
  System.out.println(p_StaticField);
  System.out.println("父类--静态初始化块");
 }
 // 初始化块
 {
  System.out.println(p_Field);
  System.out.println("父类--初始化块");
 }
// 构造器
 public Parent() {
  System.out.println("父类--构造器");
  System.out.println("i=" + i + ", j=" + j);
  j = 20;
 }
}

public class SubClass extends Parent {
 // 静态变量
 public static String s_StaticField = "子类--静态变量";
 // 变量
 public String s_Field = "子类--变量";
 // 静态初始化块
 static {
  System.out.println(s_StaticField);
  System.out.println("子类--静态初始化块");
 }
 // 初始化块
 {
  System.out.println(s_Field);
  System.out.println("子类--初始化块");
 }
// 构造器
 public SubClass() {
  System.out.println("子类--构造器");
  System.out.println("i=" + i + ",j=" + j);
 }
// 程序入口
 public static void main(String[] args) {
  System.out.println("子类main方法");
  //new SubClass();
 }
}

运行一下上面的代码,结果马上呈现在我们的眼前:
父类–静态变量
父类–静态初始化块
子类–静态变量
子类–静态初始化块
子类main方法
以上代码在new SubClass();之前完成,当实例化一个对象时,
父类–变量
父类–初始化块
父类–构造器
i=9, j=0
子类–变量
子类–初始化块
子类–构造器
i=9,j=20
现在,结果已经不言自明了。子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了。

执行过程分析
(1)访问SubClass.main(),(这是一个static方法),于是装载器就会为你寻找已经编译的SubClass类的代码(也 就是SubClass.class文件)。在装载的过程中,装载器注意到它有一个基类(也就是extends所要表示的意思),于是它再装载基类。不管你创不创建基类对象,这个过程总会发生。如果基类还有基类,那么第二个基类也会被装载,依此类推。
(2)执行根基类的static初始化,然后是下一个派生类的static初始化,依此类推。这个顺序非常重要,因为派生类的“static初始化”有可能要依赖基类成员的正确初始化。
(3)当所有必要的类都已经装载结束,开始执行main()方法体,并用new SubClass()创建对象。
(4)类SubClass存在父类,则调用父类的构造函数,你可以使用super来指定调用哪个构造函数(也就是Beetle()构造函数所做的第一件事)。
基类的构造过程以及构造顺序,同派生类的相同。首先基类中各个变量按照字面顺序进行初始化,然后执行基类的构造函数的其余部分。
(5)对子类成员数据按照它们声明的顺序初始化,执行子类构造函数的其余部分。

总结:
(静态变量、静态初始化块),(变量、初始化块)初始化了顺序取决于它们在类中出现的先后顺序。

Round 4:final
一种特殊的情况:
对于一个final类型的类变量,如果该类变量的值在编译时就可以确定下来,那么这个类变量相当于“宏变量”。Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量,也不会导致该类的初始化。

class MyTest{
    static{
        System.out.println("static innit");
    }
    static final String constantTest="hello";
}
public class CompileConstantTest {
   public static void main(String[] args) {
    System.out.println(MyTest.constantTest);
}
}

只输出hello
程序中的所有constantTest的类变量在编译时就会被直接替换成它的值。
总结:
当某个类变量使用了final修饰,而且它的值可以在编译时就确定下来,那么程序其他地方使用该类变量时,实际上并没有使用该类变量,而是相当于使用了常量。

class MyTest {
    static {
        System.out.println("static innit");

    }

    static final String constantTest = System.currentTimeMillis() + "";
}

public class CompileConstantTest {
    public static void main(String[] args) {
        System.out.println(MyTest.constantTest);
    }
}

输出
static innit
1458790756858
如果final修饰的类变量的值不能在编译时确定下来,则必须等到运行时才可以确定该类变量的值,如果通过该类来访问它的类变量,则会导致该类被初始化。
理解上面的代码后,升级版如下:
先理解:
主动引用与被动引用:
jvm有严格的规定(五种情况):
1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,假如类还没进行初始化,则马上对其进行初始化工作。其实就是3种情况:用new实例化一个类时、读取或者设置类的静态字段时(不包括被final修饰的静态字段,因为他们已经被塞进常量池了)、以及执行静态方法的时候。
2.使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。
3.初始化某个类的子类,当初始化某个类的子类时,该类的所有父类都会被初始化。
4.当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。
5.用Class.forName(String className);来加载类的时候,也会执行初始化动作。注意:ClassLoader的loadClass(String className);方法只会加载并编译某类,并不会对其执行初始化。
以上5种预处理称为对一个类进行主动的引用,其余的其他情况,称为被动引用,都不会触发类的初始化。
/*
被动引用情景1
通过子类引用父类的静态字段,不会导致子类的初始化*/

class SuperClass {
    static {
        System.out.println("super class init.");
    }

    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("sub class init.");
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

输出为:
super class init.
123

/**
* 被动引用情景2
* 通过数组引用来引用类,不会触发此类的初始化*
*
*/

public class Test {
    public static void main(String[] args) {
        //System.out.println(SubClass.value);
        SuperClass []s_list=new SuperClass[10];
    }
}

没有输出
/**
* 被动引用情景3
* 静态常量在编译阶段会被存入调用类的常量池中,本质上并没有引用到定义常量类类,所以自然不会触发定义常量的类的初始化
*
*
*/

class ConstClass {
    static {
        System.out.println("ConstClass init.");
    }

    public final static String value = "hello";
}
public class Test {
    public static void main(String[] args) {
        // System.out.println(SubClass.value);
        //SuperClass[] s_list = new SuperClass[10];
        System.out.println(ConstClass.value);
    }
}

输出hello
tip:在编译的时候,ConstClass.value已经被转变成hello常量放进test类的常量池里面了
接口与类初始化的不同
以上是针对类的初始化,接口也要初始化,接口的初始化跟类的初始化有点不同:
上面的代码都是用static{}来输出初始化信息的,接口没法做到,但接口初始化的时候编译器仍然会给接口生成一个()的类构造器,用来初始化接口中的成员变量,这点在类的初始化上也有做到。真正不同的地方在于第三点,类的初始化执行之前要求父类全部都初始化完成了,但接口的初始化貌似对父接口的初始化不怎么感冒,也就是说,子接口初始化的时候并不要求其父接口也完成初始化,只有在真正使用到父接口的时候它才会被初始化(比如引用接口上的常量的时候啦)。
如下代码:

class Father{
    public static final String F="final father";
    static {
        System.out.println("father static");
    }
    static{
        System.out.println(Son.F);
    }
    public Father(){
        init();
    }
    public void init(){
        System.out.println("father init");
    }

}
public class Son extends Father{
    public static final String F="final Son";
    public void init(){
        System.out.println("Son init");
    }
    public static void main(String[] args) {
        System.out.println(Father.F);
        Father f=new Son();
        System.out.println(f.F);
    }
}

类的初始化会执行以下两段代码:
father static
final Son

final father—-System.out.println(Father.F);

Son init—-Father f=new Son();

final father—-System.out.println(f.F);

class Father{
    public static final String F="final father";
    static {
        System.out.println("father static");
    }
    static{
        System.out.println(Son.F);
    }
    public Father(){
        init();
    }
    public void init(){
        System.out.println("father init");
    }

}
class Son extends Father{
    public static final String F="final Son";
    public void init(){
        System.out.println("Son init");
    }

}
public class Test {
      public static void main(String[] args) {
        System.out.println(Father.F);
        Father f=new Son();
        System.out.println(f.F);
    }
}
final father-----System.out.println(Father.F);

father static-----Father f=new Son();
final Son-----Father f=new Son();
Son init-----Father f=new Son();

final father-----System.out.println(f.F);

Round 5:
1 一个例子引发的血案

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;

    private SingleTon() {
        count1++;
        count2++;
    }

    public static SingleTon getInstance() {
        return singleTon;
    }
}

public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

错误答案

count1=1
count2=1
正确答案

count1=1
count2=0
为神马?为神马?这要从java的类加载时机说起。
2 类的加载时机

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。
3 何时开始类的初始化

什么情况下需要开始类加载过程的第一个阶段:”加载”。虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握,但是对于初始化阶段虚拟机规范是严格规定了如下几种情况,如果类未初始化会对类进行初始化。 以上情况称为称对一个类进行“主动引用”,除此种情况之外,均不会触发类的初始化,称为“被动引用” 接口的加载过程与类的加载过程稍有不同。接口中不能使用static{}块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。
4 被动引用例子

class SuperClass {
    static {
        System.out.println("superclass init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("subclass init");
    }
}

public class Test {
    public static void main(String[] args) {
        System.out.println(SubClass.value);// 被动应用1
        SubClass[] sca = new SubClass[10];// 被动引用2
    }
}

程序运行输出 superclass init 123 从上面的输入结果证明了被动引用1与被动引用2

class ConstClass {
    static {
        System.out.println("ConstClass init");
    }
    public static final String HELLOWORLD = "hello world";
}

public class Test {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);// 调用类常量
    }
}

程序输出结果 hello world 从上面的输出结果证明了被动引用3
5 类的加载过程

5.1 加载

“加载”(Loading)阶段是“类加载”(Class Loading)过程的第一个阶段,在此阶段,虚拟机需要完成以下三件事情:
1、 通过一个类的全限定名来获取定义此类的二进制字节流。
2、 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
加载阶段即可以使用系统提供的类加载器在完成,也可以由用户自定义的类加载器来完成。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

5.2 验证

   验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
   Java语言本身是相对安全的语言,使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,编译器将拒绝编译。但是,Class文件并不一定是由Java源码编译而来,可以使用任何途径,包括用十六进制编辑器(如UltraEdit)直接编写。如果直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。
  不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。
   1、文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。
   2、元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法……
   3、字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。
   4、符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。验证符号引用中通过字符串描述的权限定名是否能找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问

验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。
5.3 准备

   准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
    public static int value=123;//在准备阶段value初始值为0 。在初始化阶段才会变为123 。

5.4 解析

   解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
   符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
   直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

5.5 初始化

   类初始化是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
    初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

6 题目分析

上面很详细的介绍了类的加载时机和类的加载过程,通过上面的理论来分析本文开门见上的题目

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;

    private SingleTon() {
        count1++;
        count2++;
    }

    public static SingleTon getInstance() {
        return singleTon;
    }
}

public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

分析:
1:SingleTon singleTon = SingleTon.getInstance();调用了类SingleTon的静态方法,触发类的初始化
2:类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0
3:类初始化,为类的静态变量赋值和执行静态代码快。singleton赋值为new SingleTon()调用类的构造方法
4:调用类的构造方法后count=1;count2=1
5:继续为count1与count2赋值,此时count1没有赋值操作,所有count1为1,但是count2执行赋值操作就变为0
注意:
当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class.forName()静态方法才会导致强制初始化该类

class Tester {
    static {
        System.out.println("static init");
    }
}
public class ClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        cl.loadClass("com.testclassloder2."
                + "Tester");
        System.out.println("系统加载Tester类");
        Class.forName("com.testclassloder2.Tester");
    }
}

版权声明:本文为fay625原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。