尚硅谷2020最新版宋红康JVM教程-中篇-第3章类的加载过程(类的生命周期)详解-4-过程三:Initialization(初始化)阶段
static與final的搭配問題
初始化階段,簡言之,為類的靜態變量賦予正確的初始值。
具體描述
- 類的初始化是類裝載的最后一個階段。如果前面的步驟都沒有問題,那么表示類可以順利裝載到系統中。此時,類才會開始執行Java字節碼。(即:到了初始化階段,才真正開始執行類中定義的Java程序代碼。)
- 初始化階段的重要工作是執行類的初始化方法:<clinit>()方法。
- 該方法僅能由Java編譯器生成并由JVM調用,程序開發者無法自定義一個同名的方法,更無法直接在Java程序中調用該方法,雖然該方法也是由字節碼指令所組成。
- 它是由類靜態成員的賦值語句以及static語句塊合并產生的。
代碼舉例
public class InitializationTest {public static int id = 1;public static int number;static {number = 2;System.out.println("father static(}");}// clinit方法:// 0 iconst_1// 1 putstatic #2 <T1/InitializationTest.id>// 4 iconst_2// 5 putstatic #3 <T1/InitializationTest.number>// 8 getstatic #4 <java/lang/System.out>//11 ldc #5 <father static(}>//13 invokevirtual #6 <java/io/PrintStream.println>//16 return }說明
- 在加載一個類之前,虛擬機總是會試圖加載該類的父類,因此父類的<clinit>總是在子類<clinit>之前被調用。也就是說,父類的static塊優先級高于子類。
- Java編譯器并不會為所有的類都產生<clinit>()初始化方法。哪些類在編譯為字節碼后,字節碼文件中將不會包含<clinit>()方法?
- 一個類中并沒有聲明任何的類變量,也沒有靜態代碼塊時
- 一個類中聲明類變量,但是沒有明確使用類變量的初始化語句以及靜態代碼塊來執行初始化操作時
- 一個類中包含static final修飾的基本數據類型的字段,這些類字段初始化語句采用編譯時常量表達式
代碼舉列
/*** 哪些場景下,java編譯器就不會生成<cLinit>()方法*/ public class InitializationTest1 {//場景1:對應非靜態的字段,不管是否進行了顯式賦值,都不會生成<clinit>()方法public int num = 1;//場景2:靜態的字段,沒有顯式的賦值,不會生成<clinit>()方法public static int numl;//場景3:比如對于聲明為static final的基本數據類型的字段,不管是否進行了顯式賦值,都不會生成<clinit>()方法public static final int num2 = 1;}關于static + final
/*** 說明:使用static+ final修飾的字段的顯式賦值的操作,到底是在哪個階段進行的賦值?* 情況1:在鏈接階段的準備環節賦值* 情況2:在初始化階段<cLinit>()中賦值* * 結論:* 在鏈接階段的準備環節賦值的情況:* 1.對于基本數據類型的字段來說,如果使用static final修飾,則顯式賦值(直接賦值常量,而非調用方法)通常是在鏈接階段的準備環節進行* 2.對于String來說,如果使用字面量的方式賦值,使用static final修飾的話,則顯式賦值通常是在鏈接階段的準備環節進行* * 在初始化階段<cLinit>()中賦值的情況:* 排除上述的在準備環節賦值的情況之外的情況。* * 最終結論:使用static+final修飾,且顯示賦值中不涉及到方法或構造器調用的基本數據類到或String類型的顯式財值,是在鏈接階段的準備環節進行。*/ public class InitializationTest2 {public static int a = 1; //在初始化階段<clinit>()中賦值public static final int INT_CONSTANT = 10; //在鏈接階段的準備環節賦值public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在初始化階段<clinit>()中賦值public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100); // 在初始化階段<clinit>()中概值public static final String se = "helloworlde"; // 在鏈接階段的準備環節賦值public static final String s1 = new String("helloworld1"); // 在初始化階段<clinit>()中賦值public static final int NUM1 = new Random().nextInt(10);//在初始化階段clinit>()中賦值 }所有非final的static都是在初始化<clini>()顯示賦值
不涉及符號引用(鏈接階段的解析環節),就直接在鏈接階段的準備環節顯示賦值(沒驗證)
<clinit>()的線程安全性
- 對于<clinit>()方法的調用,也就是類的初始化,虛擬機會在內部確保其多線程環境中的安全性。
- 虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。
- 正是因為函數<clinit>()帶鎖線程安全的,因此,如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個線程阻塞,引發死鎖。并且這種死鎖是很難發現的,因為看起來它們并沒有可用的鎖信息。
- 如果之前的線程成功加載了類,則等在隊列中的線程就沒有機會再執行<clinit>()方法了。那么,當需要使用這個類時,虛擬機會直接返回給它已經準備好的信息。
代碼舉列
package T1;public class StaticDeadLockMain extends Thread {private char flag;public StaticDeadLockMain(char flag) {this.flag = flag;this.setName("Thread" + flag);}@Overridepublic void run() {try {Class.forName("T1.Static" + flag);} catch (ClassNotFoundException e) {e.printStackTrace();}System.out.println(getName() + "over");}public static void main(String[] args) throws ClassNotFoundException, InterruptedException {StaticDeadLockMain loadA = new StaticDeadLockMain('A');loadA.start();StaticDeadLockMain loadB = new StaticDeadLockMain('B');loadB.start();} }class StaticA {static {try {Thread.sleep(100);} catch (InterruptedException e) {}try {Class.forName("T1.StaticB");} catch (ClassNotFoundException e) {e.printStackTrace();}System.out.println("staticA init Ok");} }class StaticB {static {try {Thread.sleep(100);} catch (InterruptedException e) {}try {Class.forName("T1.StaticA");} catch (ClassNotFoundException e) {e.printStackTrace();}System.out.println("staticB init Ok");} }類的初始化情況:主動使用vs被動使用
引言
- Java程序對類的使用分為兩種:主動使用和被動使用。
- 主動使用才會調用<clinit>(初始化),被動使用不會引起類的初始化
- 被動使用不會引起類的初始化,但有可能只是加載了沒進行初始化,比如調用類的final+static的字段,有加載能輸出字段,但沒經歷初始化
主動使用
Class只有在必須要首次使用的時候才會被裝載,Java虛擬機不會無條件地裝載Class類型。Java虛擬機規定,一個類或接口在初次使用前,必須要進行初始化。這里指的“使用”,是指主動使用,主動使用只有下列幾種情況:(即:如果出現如下的情況,則會對類進行初始化操作。而初始化操作之前的加載、驗證、準備已經完成。)
針對5,補充說明:
當Java虛擬機初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則并不適用于接口。
- 在初始化一個類時,并不會先初始化它所實現的接口
- 在初始化一個接口時,并不會先初始化它的父接口
- 因此,一個父接口并不會因為它的子接口或者實現類的初始化而初始化。只有當程序首次使用特定接口的靜態字段時,才會導致該接口的初始化。
針對7,說明:
VM啟動的時候通過引導類加載器加載一個初始類。這個類在調用public static void main(String[])方法之前被鏈接和初始化。這個方法的執行將依次導致所需的類的加載,鏈接和初始化。
代碼舉列-主動使用
package T1;import java.util.Random;public class ActiveUse1 {// main()方法的那個類),虛擬機會先初始化這個主類static {System.out.println("ActiveUse1 的初始化(main觸發)");}public static void main(String[] args) throws ClassNotFoundException {// 當創建一個類的實例時,比如使用new關鍵字 // Order order = new Order();// 當調用類的靜態方法時,invokestatic // Order.method1();// 當使用類、接口的靜態字段時(final修飾特殊考慮) // System.out.println(Order.num); // System.out.println(CompareA.num);// 當使用java.lang.reflect包中的方法反射類的方法時 // Class.forName("T1.Order");// 當初始化子類時,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化(沒能觸發實現的接口)Class.forName("T1.Order");// 初始化一個接口時,并不會先初始化它的父接口 // System.out.println(CompareC.cnum2);// 接口定義了default方法,那么直接實現或者間接實現該接口的類的初始化Class.forName("T1.Order");} }class Order extends OrderFather implements CompareB, CompareD {public static int num = 10;static {System.out.println("Order類的初始化過程。。。。。。。。。。。");}public static void method1() {System.out.println("靜態代碼 method1");} }class OrderFather {public static int num = 20;static {System.out.println("OrderFather類的初始化過程。。。。。。。。。。。");} }interface CompareA {public static final Thread t = new Thread() {{System.out.println("CompareA初始化");}};public static int num = 10; // 為啥這個沒能觸發初始化public static final int cnum2 = new Random().nextInt(10); }interface CompareB {public static final Thread t = new Thread() {{System.out.println("CompareB初始化");}};}interface CompareC extends CompareB {public static final Thread t = new Thread() {{System.out.println("CompareC初始化");}};public static int num = 10; // 為啥這個沒能觸發初始化public static final int cnum2 = new Random().nextInt(10); }interface CompareD {public static final Thread t = new Thread() {{System.out.println("CompareD初始化");}};public default void abc() {System.out.println("default");} }-XX:+TraceClassLoading ,可以打印出類的加載順序,可以用來排查 class 的沖突問題。
被動使用
除了以上的情況屬于主動使用,其他的情況均屬于被動使用。被動使用不會引起類的初始化。
也就是說:并不是在代碼中出現的類,就一定會被加載或者初始化。如果不符合主動使用的條件,類就不會初始化。
當訪問一個靜態字段時,只有真正聲明區個字段的類才會被初始化。
當通過子類引用父類的靜態變量,不會導致子類初始化通過數組定義類引用,不會觸發此類的初始化
引用常量不會觸發此類或接口的初始化。因為常量在鏈接階段就已經被顯式賦值了。
調用ClassLoader類的LoadClass()方法加載一個類,并不是對類的主動使用,不會導致類的初始化。
代碼舉列-被動使用
package T1;import java.util.Random;public class PassiveUse1 {public static void main(String[] args) throws ClassNotFoundException {// 1. 當訪問一個靜態字段時,只有真正聲明區個字段的類才會被初始化。// 當通過子類引用父類的靜態變量,不會導致子類初始化 // System.out.println(Child.num);/// 2. 通過數組定義類引用,不會觸發此類的初始化 // Parent[] parents= new Parent[10]; // System.out.println(parents.getClass());// 但new的話還是會初始化 // parents[0] = new Parent();// 3. 引用常量不會觸發此類或接口的初始化。因為常量在鏈接階段就已經被顯式賦值了。System.out.println(Parent.age);System.out.println(Serival1.num);// 但引用其他類的話還是會初始化System.out.println(Serival1.cnum2);// 調用ClassLoader類的LoadClass()方法加載一個類System.out.println(ClassLoader.getSystemClassLoader().loadClass("T1.Parent"));} }class Parent {public static int num = 10;public static final int age = 20;static {System.out.println("Person 初始化");} }class Child extends Parent {static {System.out.println("Son 初始化");} }interface Serival1 {public static final Thread t = new Thread() {{System.out.println("Serival1 初始化");}};public static int num = 10; // 為啥這個沒能觸發初始化public static final int cnum2 = new Random().nextInt(10); }總結
以上是生活随笔為你收集整理的尚硅谷2020最新版宋红康JVM教程-中篇-第3章类的加载过程(类的生命周期)详解-4-过程三:Initialization(初始化)阶段的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaCard开发环境搭建
- 下一篇: 靶向抗体偶联-靶向EGFR抗体偶联药物技