深入理解Java类加载机制(二)

1 前言

深入理解Java类加载机制(一)一文中,我们了解了类的加载和连接过程,这篇文章重点讲述类的初始化过程,这样,我们就将类的加载机制弄明白了。

2 初始化时机

在上一篇 类的加载时机5.2中我们提到了“首次主动使用”这个词语,那什么是“主动使用”呢?
主动初始化的6种方式
(1)创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。
(2)调用类的静态属性或者为静态属性赋值
(3)调用类的静态方法
(4)通过class文件反射创建对象
(5)初始化一个类的子类:使用子类的时候先初始化父类
(6)java虚拟机启动时被标记为启动类的类:就是我们的main方法所在的类
只有上面6种情况才是主动使用,也只有上面六种情况的发生才会引发类的初始化。

同时我们需要注意下面几个Tips:
1)在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了.
这里多说一点,为什么只初始化一次呢?因为我们上面讲到过类加载的最终结果就是在堆中存有唯一一个Class对象,我们通过Class对象找到
类的相关信息。唯一一个Class对象说明了类只需要初始化一次即可,如果再次初始化就会出现多个Class对象,这样和唯一相违背了。
2)在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化;
3)在编译时无法确定下来的静态变量(运行时常量),会对类进行初始化;
4)如果这个类没有被加载和连接的话,那就需要进行加载和连接
5)如果这个类有父类并且这个父类没有被初始化,则先初始化父类.
6)如果类中存在初始化语句,依次执行初始化语句.

public class Test1 {
  public static void main(String args[]){
    System.out.println(FinalTest.x);
  }
}

class FinalTest{
  public static final int x =6/3;
  static {
      System.out.println("FinalTest static block");
  }
}

上面和下面的例子大家对比下,然后自己看看输出的是什么?

public class Test2 {
    public static void main(String args[]){
        System.out.println(FinalTest2.x);
    }
}
class FinalTest2{

public static final int x =new Random().nextInt(100);
static {
    System.out.println("FinalTest2 static block");
}
}

第一个输出的是
2
第二个输出的是
FinalTest2 static block
61(随机数)
为何会出现这样的结果呢?
参考上面的Tips2和Tips3,第一个能够在编译时期确定的,叫做编译常量;第二个是运行时才能确定下来的,叫做运行时常量。编译常量不会引起类的初始化,而运行常量就会。

那么将第一个例子的final去掉之后呢?输出又是什么呢?
这就是对类的首次主动使用,引用类的静态变量,输出的当然是:
FinalTest static block
2
那么在第一个例子的输出语句下面添加
FinalTest.x =3;
又会输出什么呢?
大家不妨试试!提示(Tips1)

3 类的初始化步骤

讲到这里我们应该对类的加载-连接-初始化有一个全局概念了,那么接下来我们看看类具体初始化执行步骤。我们分两种情况讨论,一种是类有父类,一种是类没有父类。(当然所有类的顶级父类都是Object)

没有父类的情况:

1)类的静态属性
2)类的静态代码块
3)类的非静态属性
4)类的非静态代码块
5)构造方法

有父类的情况:

1)父类的静态属性
2)父类的静态代码块
3)子类的静态属性
4)子类的静态代码块
5)父类的非静态属性
6)父类的非静态代码块
7)父类构造方法
8)子类非静态属性
9)子类非静态代码块
10)子类构造方法

在这要说明下,静态代码块和静态属性是等价的,他们是按照代码顺序执行的。
类的初始化内容这样看起来还是挺多的,包括“主动使用”大家可以自己去写一些demo去验证一下。

4 结束JVM进程的几种方式

了解完类加载机制之后,接下来我们了解一下结束JVM进程的几种方式吧。

(1) 执行System.exit()
(2) 程序正常结束
(3) 程序抛出异常,一直向上抛出没处理
(4) 操作系统异常,导致JVM退出

JVM有上面4种结束的方式,我们一一了解下:

(1)我们先来看看第一种方式,找到源代码我们发现:

/**
     * Terminates the currently running Java Virtual Machine. The
     * argument serves as a status code; by convention, a nonzero status
     * code indicates abnormal termination.
     */
    public static void exit(int status) {
        Runtime.getRuntime().exit(status);
    }

上面的代码解释了System.exit()方法的作用就是:是中断当前运行的java虚拟机。这是自杀方式。

(2)第二种程序正常结束的方式,我们在运行main方法的时候,运行状态按钮由绿色变红色再变绿色的过程就是程序启动-运行-结束的过程。 那么,我们来看看Android的程序,同样,安卓也有自己的启动方式,也是一个main方法。那么我们的android程序能够一直运行的前提就是我们的main方法一直被执行着,一旦main方法执行完毕,程序就是kill。我们找找源代码才能有更好的说服力;我们找到ActivityThread的main方法

 public static void main(String[] args) {
    SamplingProfilerIntegration.start();

    // CloseGuard defaults to true and can be quite spammy.  We
    // disable it here, but selectively enable it later (via
    // StrictMode) on debug builds, but using DropBox, not logs.
    CloseGuard.setEnabled(false);

    Environment.initForCurrentUser();

    // Set the reporter for event logging in libcore
    EventLogger.setReporter(new EventLoggingReporter());

    Security.addProvider(new AndroidKeyStoreProvider());

    Process.setArgV0("<pre-initialized>");

    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    AsyncTask.init();

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

上面的代码都不用看,直接看最后两行代码。执行完Looper.loop()之后,直接抛出了异常。但是我们并没有见到这个异常,说明我们的Looper一直在执行这样保证我们的app不被kill掉。Android就是用这种方式来保证我们的app一直运行下去的。

(3)第三种方式不用过多解释,一直没有处理被抛出的异常,这样导致了程序崩溃。
(4)第四种方式是系统异常导致了jvm退出。其实jvm就是一个软件,如果我们的操作系统都出现了错误,那么运行在他上面的软件(jvm)必然会被kill。

5 结束并回顾

到这里,我们基本都清楚了类的加载机制。那么我们在第一篇文章中开头提到一个例子,我们这里来讲讲输出的是什么,并且为何如此输出,大家坐稳。

public class Singleton {
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;

private Singleton() {
    counter1++;
    counter2++;
}

public static Singleton getSingleton() {
    return singleton;
}

}

下面是我们的测试类TestSingleton

public class TestSingleton {
public static void main(String args[]){
    Singleton singleton = Singleton.getSingleton();
    System.out.println("counter1="+singleton.counter1);
    System.out.println("counter2="+singleton.counter2);

}
}

输出是:
counter1=1
counter2=0
why?我们一步一步分析:

1 执行TestSingleton第一句的时候,因为我们没有对Singleton类进行加载和连接,所以我们首先需要对它进行加载和连接操作。在连接阶-准备阶段,我们要讲给静态变量赋予默认初始值。
singleton =null
counter1 =0
counter2 =0
2 加载和连接完毕之后,我们再进行初始化工作。初始化工作是从上往下依次执行的,注意这个时候还没有调用Singleton.getSingleton();
首先 singleton = new Singleton();这样会执行构造方法内部逻辑,进行++;此时counter1=1,counter2 =1 ;
接下来再看第二个静态属性,我们并没有对它进行初始化,所以它就没办法进行初始化工作了;
第三个属性counter2我们初始化为0 ,而在初始化之前counter2=1,执行完counter2=0之后counter2=0了;

3 初始化完毕之后我们就要调用静态方法Singleton.getSingleton(); 我们知道返回的singleton已经初始化了。
那么输出的内容也就理所当然的是1和0了。这样一步一步去理解程序执行过程是不是让你清晰的认识了java虚拟机执行程序的逻辑呢。

那么我们接下来改变一下代码顺序,将
 public static int counter1;
 public static int counter2 = 0;
 private static Singleton singleton = new Singleton();
 又会输出什么呢?为什么这样输出呢?
 这个问题留给大家去思考,主要还是理解为什么这样输出才是最重要的。

结合第一篇文章深入理解Java类加载机制(一),我们讲完了类的加载机制。大家是否对java又有了不一样的认识了呢?如果这2篇文章对你有帮助,请动动你的小指头点个赞吧。

    原文作者:小腊月
    原文地址: https://www.jianshu.com/p/8c8d6cba1f8e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞