美文网首页Java精品android面试录JavaAPI
深入理解Java类加载机制(二)

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

作者: 黑马有点白986 | 来源:发表于2017-03-26 14:04 被阅读3045次

    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篇文章对你有帮助,请动动你的小指头点个赞吧。

    相关文章

      网友评论

      • 473c5c5b0c79:楼主,你好,我把你的例子执行了一遍,我发现控制台输出的都是1,和你所说的结果不符啊,我是JDK8版本执行的
      • 08_carmelo:类的初始化步骤 这一节有个疑问: 我不一定new这个对象,那么第5步 5)构造方法 不一定执行吧? 怎么感觉这个列表是new对象的步骤,而不是类的初始化
        08_carmelo:@小腊月 ??这里不是讲类的初始化么。 我没有创建对象
        黑马有点白986:@08_carmelo 创建对象可能会触发类的初始化
      • shUID:类的初始化不应该包括非静态属性和非静态代码块的以及实例构造方法(类的构造方法除外),因为这些属于类的实例化,初始化只能执行一次,而实例化可以多次执行。
      • c97cf2ded337:根据类的初始化步骤。1)类的静态属性
        2)类的静态代码块
        3)类的非静态属性
        4)类的非静态代码块
        5)构造方法
        那为什么构造方法现在在类的静态属性初始化之前呢?“第三个属性counter2我们初始化为0 ,而在初始化之前counter2=1,执行完counter2=0之后counter2=0了”,这个意思不是初始化之前counter2已经在构造方法里++过了再初始化counter2=0嘛。。。求教教啊,好像大家都很明白的样子。。
        黑马有点白986:private static Singleton singleton = new Singleton();
        public static int counter1;
        public static int counter2 = 0;
        首先会默认初始化:
        singleton=null, counter1=0, counter2=0;
        然后按照静态属性声明顺序初始化:
        singleton=new Singleton(),这时候会调用构造器方法++,此时 counter1=1, counter2=1;
        最后对 counter1, counter2进行初始化:
        counter1我们没有初始化,而counter2=0。
        因此,counter1=1,counter2=0;
      • nicktming:首先感谢一下楼主的分享!请教一下: 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问.
        请教一下为什么要这样规定吗?谢谢
      • 我的绰号叫小黄:在马路上,没测试过,我在想如果吧rivate static Singleton singleton = new Singleton(),去掉static的话,应该结果也是两个都是1吧,因为构造方法是最后加载的。
        热心网民小王:@我的绰号叫小黄 栈溢出是什么意思 在静态方法 不能调用非静态成员的
        我的绰号叫小黄:@我的绰号叫小黄 好吧,这个写法会报错的,栈溢出异常
      • 活着工作室:我想知道你是如何学到这些的?
      • 19f2b4880ede:楼主这个实例是风中叶张龙老师说过的吧?:smile::smile:
        黑马有点白986:@不让天使落泪 不记得了,好像看过一个视频
      • shUID:(1)创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。

        难道 一个类被初始化过了,再new 它的时候就不会初始化了?
        shUID:@小腊月 哦哦,原来如此,多谢指导
        黑马有点白986:@shUID 我想你应该把类初始化和创建对象混淆了,同一个类加载器只能初始化类一次。
      • binecy:这个例子真的非常好:+1:
        黑马有点白986:@binecy 谢谢
      • 夜秦淮:看到这篇文章前面问题之后,去看了一下第七章,看完了感觉没什么太大收获,之后又继续把你的文章看完,感觉一下子就清楚了许多,这种总结的方式很好,能否分享一下你的读书或是学习的方法,我也在总结,但是总是找不出自己的问题,有些地方看了好多还是不能懂,你写的很通俗,上车了。
        黑马有点白986:@夜秦淮 感谢支持,我会继续努力
      • 海山大包:出bug了 无法关注。。
        黑马有点白986:@海山大包 谁的锅?
      • VelyVelyGood:您好,文章中 5 结束并回顾,“ 2 加载和连接完毕之后,我们再进行初始化工作。初始化工作是从上往下依次执行的,注意这个时候还没有调用Singleton.getSingleton(); ” 为什么说这时候 Singleton.getSingleton() 还没有调用,Singleton 的初始化不就是由 Singleton.getSingleton() 触发的吗?
        VelyVelyGood:@小腊月 明白了,谢谢 :)
        黑马有点白986:调用getSingleton()方法之前需要对类进行初始化
      • 5c8bf7e23e9a:谢谢po主,写的很好
        黑马有点白986:@千里落日 感谢支持,能帮到您是我最大的动力
        黑马有点白986:@千里落日 感谢支持,能帮到您是我最大的动力
      • boboyuwu:那么将第一个例子的final去掉之后呢?输出又是什么呢?
        这就是对类的首次主动使用,引用类的静态变量,输出的当然是:
        FinalTest static block 这是为啥去掉final就引用了不去掉就没引用呢
        黑马有点白986:@boboyuwu final:常量 ; 无 final 变量。常量是在常量池中,你可以在字节码中看到。
        boboyuwu:@小腊月 不去掉的话这个难道不是类中静态常量吗
        黑马有点白986:去掉final之后,在第一个例子中就不是常量的引用了,而是类静态变量的引用;那么就会触发对类的初始化。
      • 8e142235e715:没有初始化就++,不会报错吗
        黑马有点白986:@杨洋Marco 初始化之前,在准备阶段会进会进行默认初始化。
        8e142235e715: @小腊月 ok看明白了,这个例子主要想表达counter2先++,又初始化为0了

        没初始化会不会抛异常的问题,我下了地铁自己试试😂😂
        黑马有点白986:@杨洋Marco 看懂了就不会,不信试试
      • 无奈的冻鱼:“接下来再看第二个静态属性,我们并没有对它进行初始化,所以它就没办法进行初始化工作了”
        这句话作者能解释一下么,因为你在之前说过,准备阶段会为类的静态变量分配内存并赋予默认的初始值,那他这里为什么没办法进行初始化工作
        dd2014:如果你在变量声明时显示的赋值初始化了,比如counter2=0,才会在类连接阶段的初始化中进行初始化操作,又从之前counter2++变成1后,又被赋成0了,
        黑马有点白986:因为我们没有给它赋值,所以没有进行初始化工作啊?=
      • Noddy:写得真好!!!
        黑马有点白986:@Noddy 谢谢~~
      • 諸星団:写的真好,能把复杂难懂的JVM类加载机制写的如此通俗易懂,绝对是此类文章的NO.1
        :+1:
        Noddy:Good!!!!!
      • liyanlei666:那么我们接下来改变一下代码顺序,将
        public static int counter1;
        public static int counter2 = 0;
        private static Singleton singleton = new Singleton();
        又会输出什么呢?为什么这样输出呢?
        这个问题留给大家去思考,主要还是理解为什么这样输出才是最重要的。

        结果 是counter1、counter2 等于1
      • liyanlei666:第三个属性counter2我们初始化为0,因此此时的counter2 =1 ?这句话counter2 =0吧
        liyanlei666:@小腊月 嗯嗯。
        黑马有点白986:@liyanlei666 在执行counter2=0语句之前,counter2=1;在执行完counter2=0之后counter2=0;

      本文标题:深入理解Java类加载机制(二)

      本文链接:https://www.haomeiwen.com/subject/bxckottx.html