1. 程式人生 > 其它 >ThinkPHP5.1處理ajax跨域問題

ThinkPHP5.1處理ajax跨域問題

一、類的生命週期

類的生命週期指的是:類從載入虛擬機器記憶體中開始,到卸載出記憶體為止。可以同一張圖概括:

注意:載入、驗證、準備、初始化和解除安裝必須按順序開始,而解析階段不一定,在某種情況下可以在初始化階段之後再開始。

二、類載入過程

Class檔案需要載入到虛擬機器之後才能執行和使用,系統載入Class型別的檔案的步驟如下,其中連線($link$)又可以分為:驗證->準備->解析。

                圖、類載入的全過程

注意:類載入和載入不一樣哈,載入只是類載入其中的一步。

2.1 載入

載入是類載入的第一步,主要完成一下三件事:

  1. 通過一個類的全限定名(包名+類名,eg.JVM.test2)
    來獲取定義此類的二進位制位元組流(Class檔案是一組以8位位元組為基礎單位的二進位制流)
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區(存的是類資訊..)的執行時資料結構。
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

虛擬機器規範上面這3點並不具體,因此是非常靈活的。比如:"通過全類名獲取定義此類的二進位制位元組流" 並沒有指明具體從哪裡獲取、怎樣獲取。除了從編譯好的 .class 檔案中讀取,還有以下幾種方式:

  1. 從 zip 包中讀取,如 jar、war 等
  2. 從網路中獲取,如Applet
  3. 通過動態代理生成代理類的二進位制位元組流
  4. 從資料庫中讀取(少見)

一個非陣列類的載入階段(載入階段獲取類的二進位制位元組流的動作)是可控性最強的階段,這一步我們可以使用系統提供的引導類載入器,也可以使用者自定義類載入器去控制位元組流的獲取方式(重寫一個類載入器的loadClass()方法)。但是對於陣列類,他們不通過類載入器建立,它由 Java 虛擬機器直接建立,而陣列類的元素型別最終是要靠類載入器去建立。(這裡先標註一下,引用型別和非引用型別的類載入器不一樣)

載入階段與連線階段的部分內容交叉進行,載入階段尚未完成,連線階段可能已經開始,但這兩個階段的開始時間仍然保持先後順序

2.2 驗證

驗證的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全

。(畢竟Class檔案並不一定要求用Java原始碼編譯而來,可以通過任何途徑產生,甚至可以用十六進位制編輯器直接編寫來產生Class檔案,所以Java程式碼無法做到的事情(eg、陣列越界)至少在語義上是可以表達出來的,所以驗證這一步很重要)。以下是驗證要做的事情:

2.3 準備

準備階段是正式為類變數(被$static$修飾符修飾的變數)分配記憶體並設定類變數初始值的階段,類變數所使用的記憶體將在方法區中進行分配。注意一下兩點:

  1. 準備階段僅是為類變數分配記憶體,不包括例項變數(例項變數會在物件例項化時隨著物件一塊分配在Java堆中)。
  2. 準備階段為類變數設定初始值,通常情況下指的是資料型別的預設零值。
    public static int value = 111;//準備階段為value類變數設定的初始值為0,初始化階段才會賦值111    

特殊情況:

public static final int value = 111;//準備階段 value 的值就被賦值為 111

  基本資料型別的零值如下:

2.4 解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位(類定義的成員變數)、類方法、介面方法、方法型別、方法控制代碼和呼叫限定符7類符號引用進行。下面來具體看一下二者關係:

  1. 符號引用:就是一組符號來描述目標,可以是任何字面量。符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案中,比如CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等型別的常量出現。符號引用與虛擬機器的記憶體佈局無關,引用的目標並不一定載入到記憶體中。
  2. 直接引用:就是直接指向目標的指標相對偏移量或者一個間接定位到目標的控制代碼
  3. 在程式實際執行時,只有符號引用是不夠的,舉個例子:在程式執行方法時,系統需要明確知道這個方法所在的位置。Java 虛擬機器為每個類都準備了一張方法表來存放類中所有的方法。當需要呼叫一個類的方法的時候,只要知道這個方法在方發表中的偏移量就可以直接呼叫該方法了。通過解析操作符號引用就可以直接轉變為目標方法在類中方法表的位置,從而使得方法可以被呼叫。
  4. 綜上,解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,也就是得到類或者欄位、方法在記憶體中的指標或者偏移量。

2.5 初始化(重點)

類初始化階段是類載入過程的最後一步,也是真正執行類中定義的Java程式程式碼(位元組碼),初始化階段是執行初始化方法$<clinit>()$方法的過程。

對於<clinit>()方法的呼叫,虛擬機器會自己確保其在多執行緒環境中的安全性。因為<clinit>()方法是帶鎖執行緒安全,所以在多執行緒環境下進行類初始化的話可能會引起死鎖,並且這種死鎖很難被發現。

public class Ten {
    public static class DeadLoopClass{
        static{
            if (true) {
                System.out.println(Thread.currentThread() + " init DeadLoopClass");
                while (true) {

                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + " start!");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over!");
            }
        };

        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}
Thread[Thread-0,5,main] start!
Thread[Thread-1,5,main] start!
Thread[Thread-0,5,main] init DeadLoopClass

Thread[Thread-0,5,main] 執行緒處於死迴圈,而Thread[Thread-1,5,main]一直在等Thread[Thread-0,5,main] 釋放鎖,所以出現了死鎖。這是因為虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖,同步。如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,知道活動執行緒執行完畢。(需要注意的是,其他執行緒雖然被阻塞,但如果執行<clinit>()方法的那條執行緒退出<clinit>()方法後,其他執行緒喚醒之後也不會再進入<clinit>()方法。因為同一個類載入器下,一個類只能初始化一次。)

虛擬機器規範則是嚴格規定了有且只有6種情況必須立即對類進行”初始化“:

  • 當遇到new、getstatic、putstatic或invokestatic這4條直接碼指令時,比如new例項化物件,讀取一個靜態欄位(未被final修飾,被final修飾的類變數在準備階段就已經賦值),呼叫一個類的靜態欄位時。
  1. 當JVM執行new指令時會初始化類,即當程式建立一個類的例項物件時。
  2. 當JVM執行getstatic指令時會初始化類,即程式訪問類的靜態變數。(不是靜態常量,常量會被載入到執行時常量池)
  3. 當JVM執行putstatic指令時會初始化類,即程式給類的靜態變數賦值
  4. 當JVM執行invokestatic指令時會初始化類,即程式呼叫類的靜態方法
  • 當使用java.lang.reflect包的方法對類進行反射呼叫時,如Class.forName() /*Class類中的靜態方法forName,直接獲取到一個類的Class檔案物件*/ 、Class類的newInstance()方法/*使用該類無參的建構函式建立物件*/等。如果該類沒有初始化,需要出發其初始化。
  • MethodHandleVarHandle可以看作是輕量級的反射呼叫機制,而要想使用這2個呼叫, 就必須先使用findStaticVarHandle來初始化要呼叫的類。
  • 初始化一個類,如果其父類還未初始化,則先觸發該父類的初始化。
  • 當虛擬機器啟動時,使用者需要定義一個要執行的主類(包含main方法的那個類)。即JVM會優先初始化包含main方法的那個類。
  • 當一個介面中定義了JDK8新加入的預設方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實現類發生了初始化,那該介面要在其之前被初始化。

注意:這裡對比一下<init>()和<clinit>()。

  1. <init>()是例項物件構造器,即在new一個物件時呼叫物件的類的constructor方法時才會執行<init>(),是對非靜態變數的初始化。通常一個類有多個例項物件的構造器。
  2. <clinit>()是Class類構造器,在類載入過程中初始化階段會呼叫,<clinit>()是對靜態變數和靜態程式碼塊進行初始化操作。通常一個類只對應一個,不帶引數且無返回值。
  3. class X {
    
       static Log log = LogFactory.getLog(); // <clinit>
    
       private int x = 1;   // <init>
    
       X(){
          // <init>
       }
    
       static {
          // <clinit>
       }
    
    }
  4. 其他關於<init>()和<clinit>()的詳解點這裡

<clinit>()的順序:

<clinit>() 方法是由編譯器自動收集類中的所有類變數的賦值動作語句和靜態塊(static {})中的語句合併產生的編譯器收集的順序由語句在原始檔中出現的順序所決定。靜態語句塊中只能訪問定義在靜態語句塊之前的變數,定義在它之後的變數(注意訪問和定義的區別),在前面的靜態語句塊中可以賦值,但不能訪問。下面看一個例子:

public class Nine {
    static{
        i = 0; //給後面定義的變數i賦值,可以正常編譯通過
        System.out.println(i);  //訪問定義在後面的變數i,會報錯
    }

    static int i = 1;

    public static void main(String[] args) {
        System.out.println("類變數i的值:" + Nine.i);
    }
}
java: 非法前向引用

虛擬機器會保證在子類的 <clinit>() 方法執行之前,父類的 <clinit>() 方法已經執行完畢。所以最先執行的<clinit>()肯定是java.lang.Object。由於父類的 <clinit>() 方法先執行,意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。看下面一個例子:

public class Ten {
    static {
        A = 2;
    }
    public static int A = 1;
}

public class Nine extends Ten{
    public static int B = A;

    public static void main(String[] args) {
        System.out.println("類變數B的值:" + Nine.B);
    }
}
類變數B的值:1

 

關於介面初始化

介面中不能使用靜態程式碼塊,但是仍然有變數初始化的賦值操作,因此介面與類一樣都會生成<clinit>()方法。但是介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。 只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法,除非是default修飾符修飾的方法。

注:介面中的成員變數都是static final型別的常量,因此在準備階段就已經初始化

三、類的解除安裝

類的生命週期的最後一步是解除安裝,解除安裝即該類的Class物件被GC。

解除安裝類需要滿足3個要求:

  1. 該類所有的例項物件都已被GC。
  2. 該類沒有其他任何地方的引用。
  3. 該類的類載入器的例項已被GC。

在JVM的生命週期,由JVM自帶的類載入器載入的類是不會被解除安裝的。但是由我們自定義的類載入器的載入的類是可能被解除安裝的。

因為JDK自帶的BootstrapClassLoader,ExtClassLoader,AppClassLoader負責載入jdk提供的類,所以他們(類載入的例項)肯定不會被回收,而我們自定義的類載入器的例項是可以被回收的,所以使用我們自定義載入器載入的類是可以被解除安裝掉的。