摘要:被多次執(zhí)行的循環(huán)體。數(shù)組范圍檢查消除。這種安全檢查策略可以避免溢出。不過,虛擬機(jī)還是挺聰明的,它會(huì)根據(jù)運(yùn)行期收集到的信息來自動(dòng)選擇最優(yōu)方案。
1.解釋器與JIT編譯器
首先我們先來了解一下運(yùn)行在虛擬機(jī)之上的解釋器與JIT編譯器。
當(dāng)我們的虛擬機(jī)在運(yùn)行一個(gè)java程序的時(shí)候,它可以采用兩種方式來運(yùn)行這個(gè)java程序:
采用解釋器的形式,也就是說,在運(yùn)行.class運(yùn)行的時(shí)候,解釋器一邊把.class文件翻譯成本地機(jī)器碼,一邊執(zhí)行。顯然這種一邊解釋翻譯一邊執(zhí)行發(fā)方式,可以使我們立即啟動(dòng)和執(zhí)行程序,省去編譯的時(shí)間。不過由于需要一遍解釋翻譯,會(huì)讓程序的執(zhí)行速度比較慢。
采用JIT編譯器的方式:注意,JIT編譯器是把.class文件翻譯成本地機(jī)器碼,而javac編譯器是把.java源文件編譯成.class文件。如果采用JIT編譯器的方式則是在啟動(dòng)運(yùn)行一個(gè)程序的時(shí)候,先把.class文件全部翻譯成本地機(jī)器碼,然后再來執(zhí)行,顯然,這種方式在執(zhí)行的時(shí)候由于不用對.clasa文件進(jìn)行翻譯,所以執(zhí)行的速度會(huì)比較快。當(dāng)然,代價(jià)就是我們需要花銷一定的時(shí)間來把字節(jié)碼翻譯成本地機(jī)器碼。這樣,程序在啟動(dòng)的時(shí)候,會(huì)有更多的延遲。
這兩種方式可以說是各有優(yōu)勢,虛擬機(jī)(特指HotSpot虛擬機(jī))在執(zhí)行的時(shí)候,一般會(huì)采用兩種方式結(jié)合的策略。
也就是說,在程序執(zhí)行的時(shí)候,有些代碼采用解釋器的方式,有些代碼采用編譯器,稱之為即時(shí)編譯。一般我們會(huì)對熱點(diǎn)代碼采用編譯器的方式。
2.編譯對象與觸發(fā)條件上面已經(jīng)說了,運(yùn)行過程中,如果遇到熱點(diǎn)代碼就會(huì)觸發(fā)對該代碼進(jìn)行編譯,編譯成本地機(jī)器碼。
什么是熱點(diǎn)代碼?
熱點(diǎn)代碼主要有一下兩類:
被多次調(diào)用的方法。
被多次執(zhí)行的循環(huán)體。
不過這里需要注意的是,由于循環(huán)體是存在方法之中的,盡管編譯動(dòng)作是由循環(huán)體觸發(fā)的,但編譯器仍然會(huì)以這個(gè)方法來作為編譯的對象。
3.熱點(diǎn)探測判斷一段代碼是不是熱點(diǎn)代碼,是不是需要觸發(fā)即時(shí)編譯,這樣的行為我們稱之為熱點(diǎn)探測。熱點(diǎn)探測判定有以下兩種方式:
基于采樣的熱點(diǎn)探測:這種方式虛擬機(jī)會(huì)周期性著檢查各個(gè)線程的棧頂,如果發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂,那么這個(gè)方法就是熱點(diǎn)方法。可能有人會(huì)問,所謂經(jīng)常,那什么樣才算經(jīng)常,對于這個(gè)我只能告訴你,這個(gè)取決于你自己的設(shè)置,如果自己沒有進(jìn)行相應(yīng)的設(shè)置的話,就采用虛擬機(jī)的默認(rèn)設(shè)置。
基于計(jì)數(shù)器的熱點(diǎn)探測:這種方法我們會(huì)為每個(gè)方法設(shè)置一個(gè)計(jì)數(shù)器,統(tǒng)計(jì)方法被調(diào)用的次數(shù),如果到達(dá)一定的次數(shù),我們就把它當(dāng)作是熱點(diǎn)方法。
兩種方法的優(yōu)缺點(diǎn):
顯然第一種方法在實(shí)現(xiàn)上是比較簡單、高效的,但是缺點(diǎn)也很明顯,精確度不高,容易受到線程阻塞等別的外界因素的干擾。
第二種方式的統(tǒng)計(jì)結(jié)果會(huì)很精確,但需要為每個(gè)方法建立并維護(hù)一個(gè)計(jì)數(shù)器。實(shí)現(xiàn)上會(huì)相對復(fù)雜一點(diǎn)并且開銷也會(huì)大點(diǎn)。
不過,這里需要指出的是,我們的HotSpot虛擬機(jī)采用的是基于計(jì)數(shù)器的方式。
說明:虛擬機(jī)在執(zhí)行方法的時(shí)候,會(huì)先判斷該方法是否存在已經(jīng)編譯好的版本,如果存在,則執(zhí)行編譯好的本地機(jī)器碼,否則,采用一邊解釋一邊編譯的方式。4.編譯優(yōu)化技術(shù)
先看一段代碼:
int a = 1; if(false){ System.out.println("無用代碼"); } int b = 2;
對于這段代碼,我們都知道是if語句體里面的代碼是一定不可能會(huì)被執(zhí)行到的,也就是說,這實(shí)際上是一段一點(diǎn)用處也沒有的代碼,在執(zhí)行時(shí)只能浪費(fèi)判斷時(shí)間。
實(shí)際上,對于我們書寫的代碼,編譯器在編譯的時(shí)候是會(huì)進(jìn)行優(yōu)化的。對于上面的代碼,編譯優(yōu)化之后會(huì)變成這樣:
int a = 1; int b = 2;
那段無用的代碼會(huì)被消除掉。
各種編譯優(yōu)化策略我們剛才已經(jīng)說了,對于有些被多次調(diào)用的方法或者循環(huán)體,虛擬機(jī)會(huì)先把他們編譯成本地機(jī)器碼。由于這些熱點(diǎn)代碼都是一些會(huì)被多次重復(fù)執(zhí)行的代碼,為了使得編譯好的代碼更加完美,運(yùn)行的更快。編譯器做了很多的編譯優(yōu)化策略,例如上面的無用代碼消除就是其中的一種。
下面我們來講講大概都有那些優(yōu)化策略:
大概預(yù)覽一波:
公共子表達(dá)式消除。
數(shù)組范圍檢查消除。
方法內(nèi)聯(lián)。
逃逸分析。
(1).公共子表達(dá)式消除
含義:如果一個(gè)表達(dá)式 E 已經(jīng)計(jì)算過了,并且從先前的計(jì)算到現(xiàn)在 E 中的所有變量的值都沒有發(fā)生變化,那個(gè) E 的這次出現(xiàn)就成為了公共子表達(dá)式。對于這樣的表示式,沒有必要對它再次進(jìn)行計(jì)算了,直接沿用之前的結(jié)果就可以了。
我們來舉個(gè)例子。例如
int d = (c * b) * 10 + a + (a + b * c);
這段代碼到了即時(shí)編譯器的手里,它會(huì)進(jìn)行如下優(yōu)化:
表達(dá)式中有兩個(gè) b * c的表達(dá)式,并且在計(jì)算期間b與c的值并不會(huì)變。所以這條表達(dá)式可能會(huì)被視為:
int d = E * 10 + a+ (a + E);
接著繼續(xù)優(yōu)化成
int d = E * 11 + a + a;
接著
int d = E * 11 + 2a;
這樣,代碼在執(zhí)行的時(shí)候,就會(huì)節(jié)省了一些時(shí)間了。
(2).數(shù)組范圍檢查消除
我們知道,java是一門動(dòng)態(tài)安全的語言,對數(shù)組的訪問不像c/c++那樣,可以采用指針指向一塊可能不存在的區(qū)域。例如假如有一個(gè)數(shù)組arr[],在java語言中訪問數(shù)組arr[i]的時(shí)候,是會(huì)先進(jìn)行上下界范圍檢查的,即先檢查i是否滿足i >= 0 && i < arr.length這個(gè)條件。如果不滿足則會(huì)拋出相應(yīng)的異常。這種安全檢查策略可以避免溢出。但每次數(shù)組訪問都會(huì)進(jìn)行這樣一次檢查無疑在速度性能上造成一定的影響。
實(shí)際上,對于這樣一種情況,編譯器也是可以幫助我們做出相應(yīng)的優(yōu)化的。例如對于數(shù)組的下標(biāo)是一個(gè)常量的,如arr[2],只要在編譯期根據(jù)數(shù)據(jù)流分析來確定arr.length的值,并判斷下標(biāo)‘2’并沒有越界,這樣在執(zhí)行的時(shí)候就無需在判斷了。
更常見的情況是數(shù)組訪問發(fā)生在循環(huán)體中,并且使用循環(huán)變量來進(jìn)行數(shù)組的訪問,對于這樣的情況,只要編譯器通過數(shù)據(jù)流就可以判斷循環(huán)變量的取值范圍是否在[0, arr.length)之內(nèi),如果是,那么整個(gè)循環(huán)中就可以節(jié)省很多次數(shù)組邊界檢測判斷的操勞了。
對于這些安全檢查所消耗的時(shí)間,實(shí)際上,我們還可以采用另外一種策略--隱式異常處理。例如當(dāng)我們在訪問一個(gè)對象arr的屬性arr.value的時(shí)候,沒有優(yōu)化之前虛擬機(jī)是這樣處理的:
if(arr != null){ return arr.value; }else{ throw new NollPointException(); }
采用優(yōu)化策略之后編程這樣子:
try{ return arr.value; }catch(segment_fault){ uncommon_trap(); }
就是說,虛擬機(jī)會(huì)注冊一個(gè)Segment Fault信號的異常處理器(uncommon_trap()),這樣當(dāng)arr不為空的時(shí)候,對value的訪問可以省去對arr的判斷。代價(jià)就是當(dāng)arr為空時(shí),必須轉(zhuǎn)入到異常處理器中恢復(fù)并拋出NullPointException異常,這個(gè)過程會(huì)從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài)中處理,結(jié)束后在回到用戶態(tài),速度遠(yuǎn)比一次判斷空檢查慢。當(dāng)arr極少為null的時(shí)候,這樣做是值得的,但假如arr經(jīng)常為null時(shí),那么會(huì)得不償失。
不過,虛擬機(jī)還是挺聰明的,它會(huì)根據(jù)運(yùn)行期收集到的信息來自動(dòng)選擇最優(yōu)方案。
(3).方法內(nèi)聯(lián)
先看一段代碼
public static void f(Object obj){ if)(obj != null){ System.out.println("do something"); } } public static void test(String[] args){ Object obj = null; f(obj); }
對于這段代碼,如果把兩個(gè)方法結(jié)合在一起看,我們可以發(fā)現(xiàn)test()方法里面都是一些無用的代碼。因?yàn)閒(obj)這個(gè)方法的調(diào)用,沒啥卵用。但是如果不做內(nèi)聯(lián)優(yōu)化,后續(xù)盡管進(jìn)行了無用代碼的消除,也是無法發(fā)現(xiàn)任何無用代碼的,因?yàn)槿绻裦(Object obj)和test(String[] args)兩個(gè)發(fā)放分開看的話,我們就無法得只f(obj)是否有用了。
內(nèi)聯(lián)優(yōu)化后的代碼可以是這樣:
public static void f(Object obj){ if)(obj != null){ System.out.println("do something"); } } public static void test(String[] args){ Object obj = null; //該方法直接不執(zhí)行了 }
(4).逃逸分析
逃逸分析是目前Java虛擬機(jī)比較前沿的優(yōu)化技術(shù),它并非是直接優(yōu)化代碼,而是為其他優(yōu)化手段提供依據(jù)發(fā)分析技術(shù)。
逃逸分析主要是對對象動(dòng)態(tài)作用域進(jìn)行分析:當(dāng)一個(gè)對象在某個(gè)方法被定義后,它有可能被外部的其他方法所引用,例如作為參數(shù)傳遞給其他方法,稱之為方法逃逸,也有可能被外部線程訪問到,例如類變量,稱之為線程逃逸。
假如我們可以證明一個(gè)對象并不會(huì)發(fā)生逃逸的話,我們就可以通過一些方式對這個(gè)變量進(jìn)行一些高效的優(yōu)化了。如下所示:
1).棧上分配
我們都知道一個(gè)對象創(chuàng)建之后是放在堆上的,這個(gè)對象可以被其他線程所共享,并且我們知道在堆上的對象如果不再使用時(shí),虛擬機(jī)的垃圾收集系統(tǒng)就會(huì)對它進(jìn)行帥選并回收。但無論是回收還是帥選,都是需要花費(fèi)時(shí)間的。
但是假如我們知道這個(gè)對象不會(huì)逃逸的話,我們就可以直接在棧上對這個(gè)對象進(jìn)行內(nèi)存分配了,這樣,這個(gè)對象所占用的內(nèi)存空間就可以隨進(jìn)棧和出棧而自動(dòng)被銷毀了。這樣,垃圾收集系統(tǒng)就可以省了很多帥選、銷毀的時(shí)間了。
2).同步消除
線程同步本身是一個(gè)相對耗時(shí)的過程,如果我們能判斷這個(gè)變量不會(huì)逃出線程的話,那么我們就可以對這個(gè)變量的同步措施進(jìn)行消除了。
3).標(biāo)量替換
什么是標(biāo)量?
當(dāng)一個(gè)數(shù)據(jù)無法分解成更小的時(shí)候,我們稱之為變量,例如像int,long,char等基本數(shù)據(jù)類型。相對地,如果一個(gè)變量可以分解成更小的,我們稱之為聚合量,例如Java中的對象。
假如這個(gè)對象不會(huì)發(fā)生逃逸。
我們可以根據(jù)程序訪問的情況,如果一個(gè)方法只是用到一個(gè)對象里面的若干個(gè)屬性,我們在真正執(zhí)行這個(gè)方法的時(shí)候,我們可以不創(chuàng)建這個(gè)對象,而是直接創(chuàng)建它那幾個(gè)被使用到的變量來代替。這樣,不僅可以節(jié)省內(nèi)存以及時(shí)間,而且這些變量可以隨出棧入棧而銷毀。
不過,對于編譯器優(yōu)化的技術(shù)還有很多,上面這幾種算是比較典型的。
本次講解到這里。
完
參考書籍:深入Java虛擬機(jī)
如果你習(xí)慣在微信公眾號看技術(shù)文章
想要獲取更多資源的同學(xué)
歡迎關(guān)注我的公眾號:苦逼的碼農(nóng)
每周不定時(shí)更新文章,同時(shí)更新自己算法刷題記錄。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/76850.html
摘要:虛擬機(jī)在執(zhí)行程序的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。棧幀棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧的棧元素。棧幀的概念結(jié)構(gòu)如下運(yùn)行時(shí)數(shù)據(jù)區(qū)腦圖高 這里我們先說句題外話,相信大家在面試中經(jīng)常被問到介紹Java內(nèi)存模型,我在面試別人時(shí)也會(huì)經(jīng)常問這個(gè)問題。但是,往往都會(huì)令我比較尷尬,我還話音未落,面試者就會(huì)背誦一段(Java虛擬...
摘要:此時(shí),就出現(xiàn)了線程不安全問題了。因?yàn)榈某跏贾禃?huì)是因此,重排序是有可能導(dǎo)致線程安全問題的。真的能完全保證一個(gè)變量的線程安全嗎我們通過上面的講解,發(fā)現(xiàn)關(guān)鍵字還是挺有用的,不但能夠保證變量的可見性,還能保證代碼的有序性。 對于volatile這個(gè)關(guān)鍵字,相信很多朋友都聽說過,甚至使用過,這個(gè)關(guān)鍵字雖然字面上理解起來比較簡單,但是要用好起來卻不是一件容易的事。 這篇文章將從多個(gè)方面來講解vol...
摘要:我們找到了許多有趣的工具和組件用來檢測狀態(tài)的各個(gè)方面,其中一個(gè)就是在運(yùn)行期通過反射了解內(nèi)部機(jī)制。由于包含多種的實(shí)現(xiàn),就是供具體實(shí)現(xiàn)比如必須繼承的抽象類。調(diào)試器框架是可擴(kuò)展的,這意味著可以通過繼承這個(gè)抽象類來使用另一個(gè)調(diào)試器。 在日常工作中,我們都習(xí)慣直接使用或者通過框架使用反射。在沒有反射相關(guān)硬編碼知識的情況下,這是Java和Scala編程中使用的類庫與我們的代碼之間進(jìn)行交互的一種主要...
摘要:為了減少在中創(chuàng)建的字符串的數(shù)量,字符串類維護(hù)了一個(gè)字符串常量池。但是當(dāng)執(zhí)行了方法后,將指向字符串常量池中的那個(gè)字符串常量。由于和都是字符串常量池中的字面量的引用,所以。究其原因,是因?yàn)槌A砍匾4娴氖且汛_定的字面量值。 String,是Java中除了基本數(shù)據(jù)類型以外,最為重要的一個(gè)類型了。很多人會(huì)認(rèn)為他比較簡單。但是和String有關(guān)的面試題有很多,下面我隨便找兩道面試題,看看你能不能...
摘要:前言本文內(nèi)容基本摘抄自深入理解虛擬機(jī),以供復(fù)習(xí)之用,沒有多少參考價(jià)值。此區(qū)域是唯一一個(gè)在虛擬機(jī)規(guī)范中沒有規(guī)定任何情況的區(qū)域。堆是所有線程共享的內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。虛擬機(jī)上把方法區(qū)稱為永久代。 前言 本文內(nèi)容基本摘抄自《深入理解Java虛擬機(jī)》,以供復(fù)習(xí)之用,沒有多少參考價(jià)值。想要更詳細(xì)了解請參考原書。 第二章 1.運(yùn)行時(shí)數(shù)據(jù)區(qū)域 showImg(https://segment...
閱讀 3388·2023-04-26 00:57
閱讀 663·2021-10-08 10:05
閱讀 1407·2021-09-08 09:36
閱讀 4259·2021-08-12 13:31
閱讀 2592·2019-08-30 15:55
閱讀 2267·2019-08-30 15:55
閱讀 1069·2019-08-30 15:55
閱讀 2732·2019-08-29 13:17