1. 程式人生 > 其它 >H5 uni.uploadFile後臺接收不到檔案的解決方案

H5 uni.uploadFile後臺接收不到檔案的解決方案

類載入機制

Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這個過程被稱作虛擬機器的類載入機制
與那些在編譯時需要進行連線的語言不同,在Java語言裡面,型別的載入、連線和初始化過程都是在程式執行期間完成的,這種策略讓Java語言進行提前編譯會面臨額外的困難,
也會讓類載入時稍微增加一些效能開銷,但是卻為Java應用提供了極高的擴充套件性和靈活性,Java天生可以動態擴充套件的語言特性就是依賴執行期動態載入和動態連線這個特點實現的
例如,編寫一個面向介面的應用程式,可以等到執行時再指定其實際的實現類,使用者可以通過Java預置的或自定義類載入器,
讓某個本地的應用程式在執行時從網路或其他地方上載入一個二進位制流作為其程式程式碼的一部分。
這種動態組裝應用的方式目前已廣泛應用於Java程式之中,從最基礎的Applet、JSP到相對複雜的OSGi技術,都依賴著Java語言執行期類載入才得以誕生。

類載入時機

一個型別從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期將會經歷載入(Loading)、驗證(Verification)、準備(Preparation)、
解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)七個階段,其中驗證、準備、解析三個部分統稱為連線(Linking)。
如圖1-1所示:

圖1-1 Class生命週期

載入、驗證、準備、初始化和解除安裝這五個階段的順序是確定的,型別的載入過程必須按照這種順序按部就班地開始,
而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結特性(也稱為動態繫結或晚期繫結)


這些階段通常都是互相交叉地混合進行的,會在一個階段執行的過程中呼叫、啟用另一個階段。

關於在什麼情況下需要開始類載入過程的第一個階段“載入”,《Java虛擬機器規範》中並沒有進行強制約束,這點可以交給虛擬機器的具體實現來自由把握。
但是對於初始化階段,《Java虛擬機器規範》則是嚴格規定了有且只有六種情況必須立即對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始)

  1. 遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果型別沒有進行過初始化,則需要先觸發其初始化階段。
    能夠生成這四條指令的典型Java程式碼場景有:
    1. 使用new關鍵字例項化物件的時候
    2. 讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候
    3. 呼叫一個型別的靜態方法的時候
  2. 使用java.lang.reflect包的方法對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需要先觸發其初始化
  3. 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
  4. 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類
  5. 當使用JDK 7新加入的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、
    REF_newInvokeSpecial四種類型的方法控制代碼,並且這個方法控制代碼對應的類沒有進行過初始化,則需要先觸發其初始化
  6. 當一個介面中定義了JDK 8新加入的預設方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實現類發生了初始化,那該介面要在其之前被初始化

類載入過程

“載入”(Loading)階段是整個“類載入”(Class Loading)過程中的一個階段,在載入階段,Java虛擬機器需要完成以下三件事情:

  • 通過一個類的全限定名來獲取定義此類的二進位制位元組流
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

《Java虛擬機器規範》對這三點要求其實並不是特別具體,留給虛擬機器實現與Java應用的靈活度都是相當大的。
例如“通過一個類的全限定名來獲取定義此類的二進位制位元組流”這條規則,它並沒有指明二進位制位元組流必須得從某個Class檔案中獲取,確切地說是根本沒有指明要從哪裡獲取、如何獲取。
Java發展歷程中,充滿創造力的開發人員則在這個舞臺上玩出了各種花樣,許多舉足輕重的Java技術都建立在這一基礎之上,例如:

  • 從ZIP壓縮包中讀取,這很常見,最終成為日後JAR、EAR、WAR格式的基礎
  • 從網路中獲取,這種場景最典型的應用就是Web Applet
  • 執行時計算生成,這種場景使用得最多的就是動態代理技術,在java.lang.reflect.Proxy中,就是用ProxyGenerator.generateProxyClass()來為特定介面生成形式為“*$Proxy”的代理類的二進位制位元組流
  • 由其他檔案生成,典型場景是JSP應用,由JSP檔案生成對應的Class檔案
  • 從資料庫中讀取,這種場景相對少見些,例如有些中介軟體伺服器(如SAP Netweaver)可以選擇把程式安裝到資料庫中來完成程式程式碼在叢集間的分發
  • 可以從加密檔案中獲取,這是典型的防Class檔案被反編譯的保護措施,通過載入時解密Class檔案來保障程式執行邏輯不被窺探

驗證

驗證是連線階段的第一步,這一階段的目的是確保Class檔案的位元組流中包含的資訊符合《Java虛擬機器規範》的全部約束要求,保證這些資訊被當作程式碼執行後不會危害虛擬機器自身的安全。
驗證階段是非常重要的,這個階段是否嚴謹,直接決定了Java虛擬機器是否能承受惡意程式碼的攻擊,從程式碼量和耗費的執行效能的角度上講,驗證階段的工作量在虛擬機器的類載入過程中佔了相當大的比重。
驗證階段大致上會完成四個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證和符號引用驗證

準備

準備階段是正式為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都應當在方法區中進行分配
關於準備階段,首先這時候進行記憶體分配的僅包括類變數,而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中
其次是這裡所說的初始值通常情況下是資料型別的零值。真正進行賦值是發生在初始化階段。

圖1-2 基本型別的零值

上面提到在“通常情況”下初始值是零值,那言外之意是相對的會有某些“特殊情況”:如果類欄位的欄位屬性表中存在ConstantValue屬性,
那在準備階段變數值就會被初始化為ConstantValue屬性所指定的初始值,假設類變數value的定義修改為:
public final static int PRIORITY = 1;
編譯時將會為value生成ConstantValue屬性,在準備階段虛擬機器就會根據Con-stantValue的設定將value賦值為1。

解析

解析階段是Java虛擬機器將常量池內的符號引用替換為直接引用的過程,符號引用在Class檔案中以CONSTANT_Class_info、CONSTANT_Fieldref_info、
CONSTANT_Methodref_info等型別的常量出現。符號引用與直接引用的區別如下:

  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
    符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定是已經載入到虛擬機器記憶體當中的內容。各種虛擬機器實現的記憶體佈局可以各不相同,
    但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在《Java虛擬機器規範》的Class檔案格式中。
  • 直接引用(Direct References):直接引用是可以直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的控制代碼
    直接引用是和虛擬機器實現的記憶體佈局直接相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。
    如果有了直接引用,那引用的目標必定已經在虛擬機器的記憶體中存在。

解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符這7類符號引用進行,
分別對應於常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、
CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info和CONSTANT_InvokeDynamic_info 8種常量型別。

初始化

類的初始化階段是類載入過程的最後一個步驟,幾個類載入的動作裡,除了在載入階段使用者應用程式可以通過自定義類載入器的方式區域性參與外,
其餘動作都完全由Java虛擬機器來主導控制。直到初始化階段,Java虛擬機器才真正開始執行類中編寫的Java程式程式碼,將主導權移交給應用程式。

初始化階段就是執行類構造器<clinit>()方法的過程。<clinit>()並不是程式設計師在Java程式碼中直接編寫的方法,它是Javac編譯器的自動生成物。

  • <clinit>()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序決定的,
    靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。如下程式碼所示:
public class Test {
 static {
 i = 0; // 給變數複製可以正常編譯通過
 System.out.print(i); // 這句編譯器會提示“非法向前引用”
 }
 static int i = 1;
}
  • <clinit>()方法與類的建構函式(即在虛擬機器視角中的例項構造器方法)不同,它不需要顯式地呼叫父類構造器,Java虛擬機器會保證在子類的<clinit>()方法執行前,
    父類的<clinit>()方法已經執行完畢。因此在Java虛擬機器中第一個被執行的<clinit>()方法的型別肯定是java.lang.Object。也就意味著父類中定義的靜態語句塊要優先於子類的變數賦值操作。

  • <clinit>()方法對於類或介面來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法

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

  • Java虛擬機器必須保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖同步,如果多個執行緒同時去初始化一個類,那麼只會有其中一個執行緒去執行這個類的<clinit>()方法,
    其他執行緒都需要阻塞等待,直到活動執行緒執行完畢<clinit>()方法。如果在一個類的<clinit>()方法中有耗時很長的操作,那就可能造成多個程序阻塞。 需要注意,其他執行緒雖然會被阻塞,
    但如果執行<clinit>()方法的那條執行緒退出<clinit>()方法後,其他執行緒喚醒後則不會再次進入<clinit>()方法。同一個類載入器下,一個型別只會被初始化一次。