JVM理解(一):JVM概述

JVM概述

什麼是JVM

JRE(JavaRuntimeEnvironment,Java運行環境),也就是Java平臺。所有的Java 程序都要在JRE下才能運行。

JDK(Java Development Kit)是程序開發者用來來編譯、調試java程序用的開發工具包。JDK的工具也是Java程序,也需要JRE才能運行。爲了保持JDK的獨立性和完整性,在JDK的安裝過程中,JRE也是 安裝的一部分。所以,在JDK的安裝目錄下有一個名爲jre的目錄,用於存放JRE文件。

JVM(JavaVirtualMachine,Java虛擬機)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺運行。使用JVM就是爲了支持與操作系統無關,實現跨平臺。 JVM屏蔽了與具體操作系統平臺相關的信息,使Java程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。JVM在執行字節碼時,實際上最終還是把字節碼解釋成具體平臺上的機器指令執行。

JVM執行方式

對於“Java是解釋執行”這句話,這個說法不太準確。我們開發的Java的源代碼,首先通過Javac編譯成爲字節碼(bytecode),然後,在運行時,通過 Java虛擬機(JVM)內嵌的解釋器將字節碼轉換成爲最終的機器碼。但是常見的JVM,比如我們大多數情況使用的Oracle JDK提供的Hotspot JVM,都提供了JIT(Just-In-Time)編譯器,也就是通常所說的動態編譯器,JIT能夠在運行時將熱點代碼編譯成機器碼,這種情況下部分熱點代碼就屬於編譯執行,而不是解釋執行了。

java執行過程如下圖所示:

JIT

Java JIT(just in time)即時編譯器是sun公司採用了hotspot虛擬機取代其開發的classic vm之後引入的一項技術,目的在於提高java程序的性能,改變人們“java比C/C++慢很多”這一尷尬印象。

說起來是編譯器,但此編譯器與通常說的javac那個編譯器不同,它其實是將字節碼編譯爲硬件可執行的機器碼的。

執行流程如下圖所示:

  1. 源代碼經javac編譯成字節碼,class文件

  2. 程序字節碼經過JIT環境變量進行判斷,是否屬於“熱點代碼”(多次調用的方法,或循環等)

  3. 如是,走JIT編譯爲具體硬件處理器(如sparc、intel)機器碼

  4. 如否,則直接由解釋器解釋執行

  5. 操作系統及類庫調用

  6. 硬件

對於解釋執行,不經過jit直接由解釋器解釋執行所有字節碼,執行效率不高。 而編譯執行不加篩選的將全部代碼進行編譯機器碼不論其執行頻率是否有編譯價值,在程序響應時間的限制下,編譯器沒法採用編譯耗時較高的優化技術(因爲JIT的編譯是首次運行或啓動的時候進行的!),所以,在純編譯執行模式下的java程序執行效率跟C/C++也是具有較大差距的。

因此,新版本的jvm默認都是採用混合執行模式。

Hot Spot 編譯

當 JVM 執行代碼時,它並不立即開始編譯代碼。這主要有兩個原因:
      首先,如果這段代碼只會被執行一次,那麼編譯就是在浪費精力。如果一段代碼被多次執行,那麼編譯就非常值得了。因此,編譯器具有的這種權衡能力會首先執行解釋後的代碼,然後再去分辨哪些方法會被頻繁調用來保證其本身的編譯。Java代碼開始都是被編譯器編譯成字節碼文件,然後字節碼文件會被交由 JVM 解釋執行,其實Java本身是一種半編譯半解釋執行的語言。Hot Spot VM 採用了 JIT compile 技術,將運行頻率很高的字節碼直接編譯爲機器指令執行以提高性能(以 method 爲翻譯單位,還會保存起來,第二次執行就不用翻譯了)直接執行。
      第二個原因是最優化,當 JVM 執行某一方法或遍歷循環的次數越多,就會更加了解代碼結構,那麼 JVM 在編譯代碼的時候就做出相應的優化。一個最重要的優化策略是編譯器可以決定何時從主存取值,何時向寄存器存值。

優化代碼緩存

當JVM編譯代碼時,它會將彙編指令集保存在代碼緩存。代碼緩存具有固定的大小,並且一旦它被填滿,JVM 則不能再編譯更多的代碼。如何確定到底需要多大的代碼緩存,通常的做法是將代碼緩存變成默認大小的兩倍或四倍。

編譯閾值(CompileThreshold

當 JVM 執行一個 Java 方法,它會檢查這兩個計數器的總和以決定這個方法是否有資格被編譯。如果有,則這個方法將排隊等待編譯。這種編譯形式一般被叫做標準編譯。但是如果方法裏有一個很長的循環或者是一個永遠都不會退出並提供了所有邏輯的程序會怎麼樣呢?這種情況下,JVM 需要編譯循環而並不等待方法被調用。所以每執行完一次循環,分支計數器都會自增和自檢。如果分支計數器計數超出其自身閾值,那麼這個循環(並不是整個方法)將具有被編譯資格。這種編譯叫做棧上替換(OSR),因爲即使循環被編譯了,這也是不夠的:JVM 必須有能力當循環正在運行時,開始執行此循環已被編譯的版本。標準編譯是被-XX:CompileThreshold=Nflag 的值所觸發。Client 編譯器模式下,N 默認的值 1500,而 Server 編譯器模式下,N 默認的值則是 10000。改變 CompileThreshold 標誌的值將會使編譯器相對正常情況下提前(或推遲)編譯代碼。在性能領域,改變 CompileThreshold 標誌是很被推薦且流行的方法。

熱點檢測

要想觸發JIT,首先需要識別出熱點代碼。目前主要的熱點代碼識別方式是熱點探測(Hot Spot Detection),有以下兩種:
1、基於採樣的方式探測(Sample Based Hot Spot Detection) :週期性檢測各個線程的棧頂,發現某個方法經常出險在棧頂,就認爲是熱點方法。好處就是簡單,缺點就是無法精確確認一個方法的熱度。容易受線程阻塞或別的原因干擾熱點探測。
2、基於計數器的熱點探測(Counter Based Hot Spot Detection)。採用這種方法的虛擬機會爲每個方法,甚至是代碼塊建立計數器,統計方法的執行次數,某個方法超過閥值就認爲是熱點方法,觸發JIT編譯。
     在HotSpot虛擬機中使用的是第二種——基於計數器的熱點探測方法,因此它爲每個方法準備了兩個計數器:方法調用計數器和回邊計數器。

  •      方法計數器。顧名思義,就是記錄一個方法被調用次數的計數器。
  •      回邊計數器。是記錄方法中的for或者while的運行次數的計數器。

JVM內存劃分

程序計數器

作用:

          是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。記錄當前線程所執行到字節碼行號,字節碼解釋器工作的時候是通過改變這個計數器的值來選取下一條需要執行字節碼的指令實現分支、循環、跳轉、異常處理、線程恢復等基礎功能。

      意義:

           JVM多線程是通過線程間輪流切換實現並分配處理器來實現的。其實並行實際上一個處理器只會執行一條線程中的指令,所以爲了保證各個線程指令安全、隔離執行,每個線程都會有獨立的私有的程序計數器。

     存儲內容:

         當線程中執行一個Java方法時,程序計數器中記錄是正在執行的線程的虛擬機字節碼指令地址。當線程中執行本地方法Native時,程序計數器中存儲的爲空(undefined)。

     異常處理:

        此區域在JVM上唯一一個不會出現OOM異常的。

Java虛擬機棧

 方法在執行時都會開闢一段內存區域用於存放方法運行時所需的數據,成爲棧幀,一個棧幀包含如:局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法的從調用到執行結束就對應着一個棧幀在虛擬機中入棧和出棧過程。如下圖:

                                                               

局部變量表主要存放了編譯器可知的各種數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)

異常處理:虛擬機棧會出現兩種異常:StackOverFlowError 和 OutOfMemoryError。

  • StackOverFlowError: 若Java虛擬機棧的內存大小不允許動態擴展,那麼當線程請求棧的深度超過當前Java虛擬機棧的最大深度的時候,就拋出StackOverFlowError異常。
  • OutOfMemoryError: 若 Java 虛擬機棧的內存大小允許動態擴展,且當線程請求棧時內存用完了,無法再動態擴展了,此時拋出OutOfMemoryError異常。

 

本地方法棧(Native Method Stack)

它和Java虛擬機棧是非常相似的,支持對本地方法的調用,也是每個線程都會創建一個。區別是,虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用於存放該本地方法的局部變量表、操作數棧、動態鏈接、出口信息。

在Oracle Hotspot JVM中,本地方法棧和Java虛擬機棧是在同一塊兒區域,這完全取決於技術實現的決定,並未在規範中強制。

方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種異常。

堆(Heap)

它是Java內存管理的核心區域,用來放置Java對象實例,幾乎所有創建的Java對象實例都是被直接分配在堆上。

Java 堆是垃圾收集器管理的主要區域,因此也被稱作GC堆(Garbage Collected Heap).從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集算法,所以Java堆還可以細分爲:新生代和老年代:再細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。

  • 堆是JVM中所有線程共享的,因此在其上進行對象內存的分配均需要進行加鎖,這也導致了new對象的開銷是比較大的
  • Sun Hotspot JVM爲了提升對象內存分配的效率,對於所創建的線程都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行的情況計算而得,在TLAB上分配對象時不需要加鎖,因此JVM在給線程的對象分配內存時會盡量的在TLAB上分配,在這種情況下JVM中分配對象內存的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間分配
  • TLAB僅作用於新生代的Eden Space,因此在編寫Java程序時,通常多個小的對象比大的對象分配起來更加高效。
  • 所有新創建的Object 都將會存儲在新生代Yong Generation中。如果Young Generation的數據在一次或多次GC後存活下來,那麼將被轉移到OldGeneration。新的Object總是創建在Eden Space。
  • 在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。

方法區(Method Area)

這也是所有線程共享的一塊內存區域,用於存儲所謂的元(Meta)數據,例如類結構信息,以及對應的運行時常量池、字段、方法代碼等。
由於早期的Hotspot JVM實現,很多人習慣於將方法區稱爲永久代(Permanent Generation)。Oracle JDK 8中將永久代移除,同時增加了元數據區(Metaspace)。

元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制,但可以通過以下參數來指定元空間的大小:

  -XX:MetaspaceSize,初始空間大小,達到該值就會觸發垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那麼在不超過MaxMetaspaceSize時,適當提高該值。
  -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

相對而言,垃圾收集行爲在這個區域是比較少出現的,但並非數據進入方法區後就“永久存在”了

直接內存(Direct Memory)區域

直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致OutOfMemoryError異常出現。

JDK1.4中新加入的 NIO(New Input/Output) 類,引入了一種基於通道(Channel) 與緩存區(Buffer) 的 I/O 方式,它可以直接使用Native函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作爲這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因爲避免了在 Java 堆和 Native 堆之間來回複製數據。

本機直接內存的分配不會收到 Java 堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

其他內存 

JVM本身是個本地程序,還需要其他的內存去完成各種基本任務,比如,JIT Compiler在運行時對熱點方法進行編譯,就會將編譯後的方法儲存在Code Cache裏面;GC等功能
需要運行在本地線程之中,類似部分都需要佔用內存空間。這些是實現JVM JIT等功能的需要,但規範中並不涉及。

如下圖是java是進程內存佔用,與規範中定義的JVM運行時數據區之間的差別,它可以看作是運行時數據區的一個超集。

運行時常量池、字符串常量池、class常量池

字符串常量池(string pool)

全局字符串池裏的內容是在類加載完成,經過驗證,準備階段之後在堆中生成字符串對象實例,然後將該字符串對象實例的引用值存到string pool中(記住:string pool中存的是引用值而不是具體的實例對象,具體的實例對象是在堆中開闢的一塊空間存放的。)。
在HotSpot VM裏實現的string pool功能的是一個StringTable類,它是一個哈希表,裏面存的是駐留字符串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字符串實例本身),也就是說在堆中的某些字符串實例被這個StringTable引用之後就等同被賦予了”駐留字符串”的身份。這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。

String 類型的常量池比較特殊。它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的 String 對象會直接存儲在常量池中。
  • 如果不是用雙引號聲明的 String 對象,可以使用 String 提供的 intern 方法。String.intern() 是一個 Native 方法,它的作用是:如果運行時常量池中已經包含一個等於此 String 對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,則在常量池中創建與此 String 內容相同的字符串,並返回常量池中創建的字符串的引用。
  • 儘量避免多個字符串拼接,因爲這樣會重新創建對象。如果需要改變字符串的話,可以使用 StringBuilder 或者 StringBuffer。

class常量池

class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用。

我們都知道,class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池(constant pool table),用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References)

  • 字面量就是我們所說的常量概念,如文本字符串、被聲明爲final的常量值等。
  • 符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指針,相對偏移量或是一個能間接定位到目標的句柄)

運行時常量池

運行時常量池(Run-Time Constant Pool),這是方法區的一部分。如果仔細分析過反編譯的類文件結構,你能看到版本號、字段、方法、超類、接口等各種信息,還有一項信息就是常量池。運行時常量池是在類加載完成之後,將每個class常量池中的符號引用值轉存到運行時常量池中,也就是說,每個class都有一個運行時常量池,類在解析之後,將符號引用替換成直接引用,與全局常量池中的引用值保持一致。

jvm在執行某個類的時候,必須經過加載、連接、初始化,而連接又包括驗證、準備、解析三個階段。而當類加載到內存中後,jvm就會將class常量池中的內容存放到運行時常量池中,由此可知,運行時常量池也是每個類都有一個。在上面我也說了,class常量池中存的是字面量和符號引用,也就是說他們存的並不是對象的實例,而是對象的符號引用值。而經過解析(resolve)之後,也就是把符號引用替換爲直接引用,解析的過程會去查詢全局字符串池,也就是我們上面所說的StringTable,以保證運行時常量池所引用的字符串與全局字符串池中所引用的是一致的。

運行時常量池相對於Class 文件常量池的另外一個重要特徵是具備動態性,Java 語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String 類的intern() 方法。

既然運行時常量池是方法區的一部分,自然會受到方法區內存的限制,當常量池無法再申請到內存時會拋OutOfMemoryError 異常

舉個例子:

String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3);//true
System.out.println(str2 == str4);//false
System.out.println(str4 == str5);//true

上面程序的首先經過編譯之後,在該類的class常量池中存放一些符號引用,然後類加載之後,將class常量池中存放的符號引用轉存到運行時常量池中,然後經過驗證,準備階段之後,在堆中生成駐留字符串的實例對象(也就是上例中str1所指向的”abc”實例對象),然後將這個對象的引用存到全局String Pool中,也就是StringTable中,最後在解析階段,要把運行時常量池中的符號引用替換成直接引用,那麼就直接查詢StringTable,保證StringTable裏的引用值與運行時常量池中的引用值一致,大概整個過程就是這樣了。

回到上面的那個程序,現在就很容易解釋整個程序的內存分配過程了,首先,在堆中會有一個”abc”實例,全局StringTable中存放着”abc”的一個引用值,然後在運行第二句的時候會生成兩個實例,一個是”def”的實例對象,並且StringTable中存儲一個”def”的引用值,還有一個是new出來的一個”def”的實例對象,與上面那個是不同的實例,當在解析str3的時候查找StringTable,裏面有”abc”的全局駐留字符串引用,所以str3的引用地址與之前的那個已存在的相同,str4是在運行的時候調用intern()函數,返回StringTable中”def”的引用值,如果沒有就將str2的引用值添加進去,在這裏,StringTable中已經有了”def”的引用值了,所以返回上面在new str2的時候添加到StringTable中的 “def”引用值,最後str5在解析的時候就也是指向存在於StringTable中的”def”的引用值,那麼這樣一分析之後,下面三個打印的值就容易理解了。

總結

  • 全局字符串常量池在每個VM中只有一份,存放的是字符串常量的引用值。
  • class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用。
  • 運行時常量池是在類加載完成之後,將每個class常量池中的符號引用值轉存到運行時常量池中,也就是說,每個class都有一個運行時常量池,類在解析之後,將符號引用替換成直接引用,與全局常量池中的引用值保持一致。

8種基本類型的包裝類和常量池

  • Java 基本類型的包裝類的大部分都實現了常量池技術,即 Byte、Short、Integer、Long、Character、Boolean;這5種包裝類默認創建了數值 [-128,127] 的相應類型的緩存數據,但是超出此範圍仍然會去創建新的對象。
  • 兩種浮點數類型的包裝類 Float、Double 並沒有實現常量池技術。

舉例如下:

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 輸出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 輸出false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 輸出false
  1. Integer i1=40;Java 在編譯的時候會直接將代碼封裝成 Integer i1=Integer.valueOf(40); 從而使用常量池中的對象。
  2. Integer i1 = new Integer(40) ;這種情況下會創建新的對象。

虛擬機對象創建過程

對象的創建

類加載檢查

虛擬機遇到一條 new 指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

分配內存

在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後便可確定,爲對象分配空間的任務等同於把一塊確定大小的內存從 Java 堆中劃分出來。分配方式有 “指針碰撞” 和 “空閒列表” 兩種,選擇那種分配方式由 Java 堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。 

內存分配併發問題

作爲虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機採用兩種方式來保證線程安全: 

  • CAS+失敗重試: CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操作的原子性。
  • TLAB: 爲每一個線程預先在 Eden 區分配一塊內存。JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,當對象大於TLAB 中的剩餘內存或 TLAB 的內存已用盡時,再採用上述的 CAS 進行內存分配。

初始化零值

內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象的實例字段在 Java 代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

設置對象頭

初始化零值完成之後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運行狀態的不同,如是否啓用偏向鎖等,對象頭會有不同的設置方式。

執行 init 方法

在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象創建纔剛開始,<init> 方法還沒有執行,所有的字段都還爲零。所以一般來說,執行 new 指令之後會接着執行 <init> 方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。

對象的內存佈局

在 Hotspot 虛擬機中,對象在內存中的佈局可以分爲3塊區域:對象頭、實例數據和對齊填充。

Hotspot虛擬機的對象頭包括兩部分信息,第一部分用於存儲對象自身的自身運行時數據(哈希碼、GC分代年齡、鎖狀態標誌等等),另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。

實例數據部分是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內容。

對齊填充部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位作用。 因爲 Hotspot 虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

對象的訪問定位

我們的Java程序通過棧上的 reference 數據來操作堆上的具體對象。對象的訪問方式有虛擬機實現而定,目前主流的訪問方式有使用句柄和直接指針兩種:

句柄

如果使用句柄的話,那麼 Java 堆中將會劃分出一塊內存來作爲句柄池,reference 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

直接指針

如果使用直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而 reference 中存儲的直接就是對象的地址。

使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而 reference 本身不需要修改。使用直接指針訪問方式最大的好處就是速度快,它節省了一次指針定位的時間開銷。

由於對象的訪問在Java 中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。就主要虛擬機Sun HotSpot 而言,它是使用第二種方式進行對象訪問的,但從整個軟件開發的範圍來看,各種語言和框架使用句柄來訪問的情況也十分常見。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章