<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          對(duì) "類(lèi)加載的時(shí)機(jī)"的思考

          共 11646字,需瀏覽 24分鐘

           ·

          2021-03-08 22:00

          作者:zzzzbw
          來(lái)源:SegmentFault 思否社區(qū)



          1 - 引言


          在閱讀書(shū)時(shí)看到里面的一道代碼題目,書(shū)中給出了題目的解答。自己對(duì)于這個(gè)題目拓展的想了幾個(gè)變式,結(jié)果有所差異,為了尋找產(chǎn)生差異的原因又深入了解了一番。




          2 - 類(lèi)初始化時(shí)機(jī)


          2.1 - 原題


          在書(shū)中 "類(lèi)加載的時(shí)機(jī)",其代碼清單7-1有這么一段代碼:


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

              public static int VALUE = 1;
          }

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

          public class Main {

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


          輸出的結(jié)果是:



          書(shū)中給出了這個(gè)結(jié)果的解答:


          上述代碼運(yùn)行之后,只會(huì)輸出“SuperClass init!”,而不會(huì)輸出“SubClass init!”。對(duì)于靜態(tài)字段, 只有直接定義這個(gè)字段的類(lèi)才會(huì)被初始化,因此通過(guò)其子類(lèi)來(lái)引用父類(lèi)中定義的靜態(tài)字段,只會(huì)觸發(fā)父類(lèi)的初始化而不會(huì)觸發(fā)子類(lèi)的初始化。


          所以 main() 方法里調(diào)用 SubClass.VALUE 時(shí)實(shí)際上調(diào)用了 SuperClass.VALUE。而 SuperClass 之前還未被加載過(guò),就觸發(fā)了加載的過(guò)程, 在初始化的時(shí)候調(diào)用了 SuperClass 里的 static 靜態(tài)代碼塊。


          2.2 - 變式一


          這里把上面代碼稍作修改。


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

              // public static int VALUE = 1;
              public final static int VALUE = 1; // 添加一個(gè) final 修飾
          }


          在其他代碼不變的情況下,把 SuperClass.VALUE 增加一個(gè) final修飾符,這時(shí)候輸出結(jié)果是:



          和原來(lái)的結(jié)果不同,"SuperClass init!"和"SubClass init!"都沒(méi)有輸出出來(lái)。


          對(duì)于這個(gè)結(jié)果,我一開(kāi)始猜測(cè)的是由于 VALUE 字段被 final 修飾,且又是基本數(shù)據(jù)類(lèi)型,所以JVM做了一些優(yōu)化,不通過(guò) SuperClass.VALUE 而是直接引用這個(gè)字段的值。


          后來(lái)看了一下IDEA反編譯 Main.class的源碼:


          // Main.class
          public class Main {
              public Main() {
              }

              public static void main(String[] args) {
                  System.out.println(1);
              }
          }


          Main類(lèi)在編譯的時(shí)候直接把 SubClass.VALUE 優(yōu)化成了值"1"。這和一開(kāi)始的猜測(cè)還是有些出入,Main類(lèi)不是被JVM在運(yùn)行時(shí)優(yōu)化的,而是在編譯器就直接被優(yōu)化了。


          對(duì)于這種情況編譯器是依據(jù)什么原理優(yōu)化的,在后面在深入展開(kāi),先繼續(xù)看下一種變式。


          2.3 - 變式二


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

              // public static int VALUE = 1;
              // public final static int VALUE = 1;
              public final static Integer VALUE = 1; // 把VALUE改成Integer包裝類(lèi)
          }


          這次把之前 int 類(lèi)型的 VALUE 改成 包裝類(lèi)Integer,看一下運(yùn)行的結(jié)果。



          這次的結(jié)果又輸出了"SuperClass init!"。確實(shí),包裝類(lèi)其實(shí)就是一種被 final 修飾的普通類(lèi),不能像基本數(shù)據(jù)類(lèi)型那樣被編譯器優(yōu)化,所以就要調(diào)用 SubClass.VALUE 而初始化 SuperClass。


          2.4 - 變式三


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

              // public static int VALUE = 1;
              // public final static int VALUE = 1;
              // public final static Integer VALUE = 1;
              public final static String VALUE = "1"; // 把VALUE改成String
          }


          這次把 SubClass.VALUE 從 Integer 改成 String,看一下運(yùn)行的結(jié)果:



          現(xiàn)在的結(jié)果和前面變式一的結(jié)果一樣了,這讓我有點(diǎn)疑惑的。String 和 Integer 不都是包裝類(lèi)嗎,為什么可以和基本數(shù)據(jù)類(lèi)型一樣不會(huì)觸發(fā) SuperClass 的初始化,難道 String 有什么特殊處理嗎?


          我還是先去看了一下IDEA反編譯的 Main.class 的源碼:


          // Main.class
          public class Main {
              public Main() {
              }

              public static void main(String[] args) {
                  System.out.println("1");
              }
          }


          確實(shí)和變式一的情況一樣,編譯器直接在編譯階段就把 String 類(lèi)型的 VALUE 值直接優(yōu)化了。




          3 - 編譯器優(yōu)化技術(shù)---條件常量傳播


          對(duì)于上文中變式一和變式三的代碼運(yùn)行結(jié)果,只輸出了 VALUE 的值而沒(méi)有輸出"SuperClass init!",首要原因就是編譯器優(yōu)化技術(shù)


          編譯器的目標(biāo)雖然是做由程序代碼翻譯為本地機(jī)器碼的工作,但其實(shí)難點(diǎn)并不在于能不能成功翻譯出機(jī)器碼,輸出代碼優(yōu)化質(zhì)量的高低才是決定編譯器優(yōu)秀與否的關(guān)鍵。


          OpenJDK的官方Wiki上,HotSpot虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)列出了一個(gè)相對(duì)比較全面的、即時(shí)編譯器中采用的優(yōu)化技術(shù)列表。地址:

          https://wiki.openjdk.java.net/display/HotSpot/PerformanceTacticIndex


          官方列出了很多的編譯器優(yōu)化技術(shù),其中 條件常量傳播(conditional constant propagation) 就是造成上文變式一和變式三輸出結(jié)果的原因。


          常量傳播是現(xiàn)代的編譯器中使用最廣泛的優(yōu)化方法之一,它通常應(yīng)用于高級(jí)中間表示(IR)。它解決了在運(yùn)行時(shí)靜態(tài)檢測(cè)表達(dá)式是否總是求值為唯一常數(shù)的問(wèn)題,如果在調(diào)用過(guò)程時(shí)知道哪些變量將具有常量值,以及這些值將是什么,則編譯器可以在編譯時(shí)期簡(jiǎn)化常數(shù)。


          3.1 - 優(yōu)化常量


          簡(jiǎn)單來(lái)說(shuō)就是編譯器會(huì)通過(guò)一定的算法發(fā)現(xiàn)代碼中存在的常量,然后直接替換指向它的變量值。例如:


          public class Main {
              public static final int a = 1; // 全局靜態(tài)常量

              public static void main(String[] args) {
                  final int b = 2; // 局部常量
                  System.out.println(a);
                  System.out.println(b);
              }
          }


          編譯器編譯之后:


          // Main.class
          public class Main {
              public static final int a = 1;

              public static void main(String[] args) {
                  int b = true;
                  System.out.println(1);
                  System.out.println(2);
              }
          }


          3.2 - 優(yōu)化常量表達(dá)式


          甚至一些常量的表達(dá)式,也可以預(yù)先直接把結(jié)果編譯出來(lái):


          public class Main {
              public static void main(String[] args) {
                  final int a = 3 * 4 + 5 - 6;
                  int b = 10;
                  if (a > b) {
                      System.out.println(a);
                  }
              }
          }


          編譯之后:


          // Main.class
          public class Main {
              public static void main(String[] args) {
                  int a = true;
                  int b = 10;
                  if (11 > b) {
                      System.out.println(11);
                  }

              }
          }


          3.3 - 優(yōu)化字符串拼接


          還可以編譯字符串的拼接,網(wǎng)上經(jīng)常有一些題目問(wèn)生成了多少個(gè) String 對(duì)象,在JVM虛擬機(jī)的層面一頓分析,其實(shí)都不正確,編譯器直接在編譯的時(shí)候就優(yōu)化掉了,根本到不了運(yùn)行時(shí)的內(nèi)存池。


          public class Main {
              public static void main(String[] args) {
                  final String str = "hel" + "lo";
                  System.out.println(str);
                  System.out.println("hello" == str);
              }
          }


          編譯后的源碼,看到 str 直接被替換成了"hello"字符串,且 "hello" == str 為true,所以全程就一個(gè)String對(duì)象生成。


          // Main.class
          public class Main {
              public static void main(String[] args) {
                  String str = "hello";
                  System.out.println("hello");
                  System.out.println(true);
              }
          }


          小拓展: 很多地方都說(shuō)多個(gè)字符串拼接不能用"+"直接拼接,要用StringBuilder之類(lèi)的。實(shí)際上,即使用"+"也會(huì)被編譯器優(yōu)化成StringBuilder的,有興趣可以自己嘗試一下。


          3.4 - 編譯器條件常量傳播帶來(lái)的風(fēng)險(xiǎn)


          雖然編譯器優(yōu)化代碼可以提升運(yùn)行時(shí)的效率,但是也會(huì)帶來(lái)一定的風(fēng)險(xiǎn)


          3.4.1 - 常量反射失效


          雖然一些被 final 修飾的字段編譯器會(huì)認(rèn)定其為常量而進(jìn)行優(yōu)化,但是Java有反射機(jī)制,通過(guò)一些奇淫技巧可以更改這些值。但是由于被編譯器優(yōu)化了,可能導(dǎo)致被修改的值不能像預(yù)期那樣生效。如:


          public class Main {
              public static final String VALUE = "A";

              public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
                  Class<Main> mainClass = Main.class;
                  Field value = mainClass.getField("VALUE");
                  value.setAccessible(true);

                  // 去除A的final修飾符
                  Field modifiersField = Field.class.getDeclaredField("modifiers");
                  modifiersField.setAccessible(true);
                  modifiersField.setInt(value, value.getModifiers() & ~Modifier.FINAL);

                  value.set(null, "B");
                  System.out.println(VALUE); // 實(shí)際還是輸出 "A"
              }
          }


          這段代碼雖然一通操作把 VALUE 的值從"A"改成"B"了,但是編譯器在編譯的時(shí)候早就把 System.out.println(VALUE); 替換成 System.out.println("A");
          ,最后運(yùn)行結(jié)果會(huì)和預(yù)期不同。


          3.4.2 - 部分編譯


          如果常量和其引用的對(duì)象不在一個(gè)文件中,當(dāng)修改常量之后只重新編譯常量所在文件,那么未重新編譯的文件就會(huì)使用舊值。如:


          // Constant.java
          public class Constant {
              public final static String VALUE = "A";
          }

          // Main.java
          public class Main {
              public static void main(String[] args) {
                  System.out.println(Constant.VALUE);
              }
          }


          假如把 Constant.VALUE 的值修改成"B"然后通過(guò) javac Constant.java 單獨(dú)編譯Constant.java文件,但是 Main 里面輸出的值依舊會(huì)是"A"。




          4 - 常量、靜態(tài)常量池、動(dòng)態(tài)常量池


          4.1 - 常量


          常量是指在程序的整個(gè)運(yùn)行過(guò)程中值保持不變的量。在Java開(kāi)發(fā)的時(shí)候通常指的是被 final 修飾的變量。但從虛擬機(jī)的角度看"常量"的定義會(huì)有所不同。


          在虛擬機(jī)中,常量會(huì)被存放于常量池中,而常量池中會(huì)存放兩大類(lèi)常量: 字面量(Literal)和符號(hào)引用(Symbolic References)。字面量比較接近于Java語(yǔ)言層面的常量概念,如文本字符串、被聲明為final的常量值等。


          而符號(hào)引用則屬于編譯原理方面的概念,主要包含類(lèi)、字段、方法信息等,這里就不展開(kāi)描述了。


          4.2 - 靜態(tài)常量池


          (靜態(tài))常量池可以比喻為Class文件里的資源倉(cāng)庫(kù),它是Class文件結(jié)構(gòu)中與其他項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù),通常也是占用Class文件空間最大的數(shù)據(jù)項(xiàng)目之一,另外,它還是在Class文件中第一個(gè)出現(xiàn)的表類(lèi)型數(shù)據(jù)項(xiàng)目。


          (靜態(tài))常量池里面存儲(chǔ)的數(shù)據(jù)項(xiàng)目類(lèi)型如下表:



          靜態(tài)常量池編譯之后就寫(xiě)定在class文件里了,可以直接查看字節(jié)碼來(lái)觀察其組成結(jié)構(gòu),如以下代碼:


          public class Main {
              final static String A = "A";
              final static int B = 1;
              final static Integer C = 2;

              public static void main(String[] args) {
                  System.out.println(A);
                  System.out.println(B);
                  System.out.println(C);
              }
          }


          編譯之后通過(guò) javap -verbose Main.class 命令查看反編譯之后的字節(jié)碼:



          可以發(fā)現(xiàn)代碼中的 String 和 int 型數(shù)據(jù)被存儲(chǔ)在靜態(tài)常量池中,Integer就沒(méi)有。因?yàn)榍罢邔?duì)應(yīng)常量池中的"CONSTANT_String_info"和"CONSTANT_Integer_info"類(lèi)型,而后者相當(dāng)于普通的對(duì)象,只被存儲(chǔ)了對(duì)象信息。


          這就解釋了上文中變式一、變式三與變式二結(jié)果不同的原因。


          4.3 - 動(dòng)態(tài)常量池


          運(yùn)行時(shí)常量池(動(dòng)態(tài)常量池)相對(duì)于Class文件常量池(靜態(tài)常量池)的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java語(yǔ)言并不要求常量一定只有編譯期才能產(chǎn)生,也就是說(shuō),并非預(yù)置入Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可以將新的常量放入池中,這種特性被開(kāi)發(fā)人員利用得比較多的便是String類(lèi)的intern()方法。


          <div id="refer-anchor"></div>




          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開(kāi)更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 45
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  你懂的网址在线 | 日本爱爱一区二区视频 | 国产精品色婷婷综合 | 国产黄色小电影 | 伊人九色 |