宋紅康老師視頻傳送門ˊᵕˋ
深刻理解java虛擬機電子書ˊᵕˋ
提取碼9q24java
jvm:跨語言的平臺python
jvm字節碼:咱們平時常說的java字節碼,指的是用java語言編成的字節碼。準確的說任何能在jvm平臺上執行的字節碼格式都是同樣的,因此應該統稱爲jvm字節碼c++
不一樣的編譯器能夠編譯出相同的字節碼文件,字節碼文件也能夠在不一樣的jvm運行。程序員
Java虛擬機與Java語言並無必然的聯繫,它只與特定的二進制文件格式一Class文件格式所關聯, Class文件中包含了Java 虛擬機指令集(或者稱爲字節碼、Bytecodes) 和符號表,還有一些其餘輔助信息。web
每一個進程對應一個jvm虛擬機實例,一個jvm實例就有一個運行時數據區(Runtime Data Area)算法
java編譯器輸入的指令流基本是一種基於棧的指令集架構,另一種指令集架構則是基於寄存器的指令集架構數據庫
基於棧式架構的特色(更少的指令集,更多的指令)編程
設計和實現更簡單,適用於資源受限的系統bootstrap
避開了寄存器的分配難題,使用零地址指令方式分配windows
指令流中的指令大部分是零地址指令,其執行過程依賴於操做棧,指令集更小,編譯器更容易實現
不須要硬件支持,可移植性更好,更好實現跨平臺
基於寄存器架構的特色(更少的指令,更多的指令集)
典型的應用是X86的二進制指令集,好比傳統的pc以及Anroid的Davlik虛擬機
指令集架構則徹底依賴硬件,可移植性差
性能優秀和執行更高效
花費更少的指令去完成一些操做
在大部分狀況下,基於寄存器的指令集每每都以一地址指令、二地址指令和三地址指令爲主,而基於棧式架構的指令集確是以零地址指令爲主
java虛擬機的啓動是經過引導類加載器建立引導類加載器建立一個初始類來完成的,這個類是由虛擬機的具體實現指定的
虛擬機的執行
一個運行中的java虛擬機有一個清晰的任務:執行java程序。
程序開始時虛擬機運行,程序結束時他就中止。
執行一個所謂的java程序的時候,真真正正在執行的是一個叫作java虛擬機的進程。
虛擬機的退出
有以下的幾種狀況:
程序正常執行結束
程序在執行過程當中遇到了異常或錯誤而異常終止
因爲操做系統出現錯誤而致使Java虛擬機進程終止:
某線程調用Runtime類或system類的exit方法,或Runt ime類的halt方法,而且Javll安全管理器也容許此次exit或halt操做。
除此以外,JNI ( Java Native Interface )規範描述了用JNI Invocation API來 加載或卸載Java虛 擬機時,Java虛擬機的退出狀況。
翻譯機:
解釋器:逐行解釋代碼,響應速度快,執行速度慢。
JIT:尋找熱點代碼,所有編譯,響應速度慢,執行速度快。
類加載器子系統負責從文件系統或者網絡中加載Class文件,class文件開頭有特定的文件標識(CAFEBABY)
ClassLoader只負責class文件的加載,至於它是否能夠運行,則由ExecutionEngine(執行引擎)決定
加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池信息(當常量池開始運行時就被稱爲運行時常量池),可能還包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)
1.經過一個類的全限定名獲取定義此類的二進制字節流
2.將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
3.在內存中生成一個表明這個類的java. lang.Class對象,做爲方法區這個類的各類數據的訪問入口
從本地系統中直接加載
經過網絡獲取,典型場景: Web Applet
從zip壓縮包中讀取,成爲往後jar、war格式的基礎
運行時計算生成,使用最多的是:動態代理技術
由其餘文件生成,典型場景: JSP應用
從專有數據庫中提取. class文件,比較少見
從加密文件中獲取,典型的防Class文件被反編譯的保護措施
驗證(Verify ) :
●目的在於確保class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。
主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。準備(Prepare) :
爲類變量分配內存而且設置該類變量的默認初始值,即零值。
● 這裏不包含用final修飾的static, 由於final在編譯的時候就會分配了,準備階段會顯式初始化;這裏不會爲實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到Java堆中。解析(Resolve) :
●將常量池內的符號引用轉換爲直接引用的過程。
事實上,解析操做每每會伴隨着JVM在執行完初始化以後再執行。
符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明肯定義在《java 虛擬機規範》的Class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT_ Class_ info、 CONSTANT_ Fieldref_ info、CONSTANT_ Methodref_ info等
將代碼中的靜態代碼塊和顯示初始化合並在一塊兒,構成
初始化階段就是執行類構造器方法
此方法不須要定義,是javac編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併而來。
構造器方法中指令按語句在源文件中出現的順序執行。
若該類具備父類,jvm會保證子類的
()執行前,父類的 ()已經執行完畢。 虛擬機必須保證一個類的
()方法在多線程下被同步加鎖
public static void main(String args[]){ private static int b = 1; static{ b = 2; num = 20; } private static int num = 10;//在linking中:num默認初始化零值,以後在initialization中初始化爲20,而後覆蓋爲10 }
分爲兩類:引導類加載器和自定義類加載器,直接或間接繼承Class Loader的類加載器都是自定義加載器(java虛擬機規範)
引導類:Bootstrap Class Loader(使用c/c++語言編寫,只負責加載java的核心類庫,如String)
自定義類(使用java語言編寫):Extension Class Loader(擴展類加載器)、System Class Loader(系統類加載器)........
這個類加載使用C/C++語言實現的,嵌套在JVM內部。
它用來加載Java的核心庫(JAVA_ HOME/jre/ lib/rt. jar、resources . jar或sun . boot .class. path路徑下的內容) ,用於提供JVM自身須要的類。
並不繼承自java. lang. ClassLoader,沒有父加載器。
加載擴展類和應用程序類加載器,並指定爲他們的父類加載器。出於安全考慮、Bootstrap啓動類 加載器只加載包名爲java、javax、sun等開頭的類。
Java語言編寫,由sun . mi sc. Launcher$ExtClassLoader實現。
派生於ClassLoader類。
父類加載器爲啓動類加載器。
從java. ext. dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。若是用戶建立的JAR放在此目錄下,也會自動由擴展類加載器加載。
java語言編寫,由sun . mi sc . Launcher$AppClassLoader實現。
派生於ClassLoader類。
父類加載器爲擴展類加載器。
它負責加載環境變量classpath或系統屬性java. class.path指定路徑下的類庫。
該類加載是程序中默認的類加載器,一- 般來講,Java應用的類都是由它來完成加載。
經過ClassLoader#getSystemClassLoader()方法能夠獲取到該類加載器。
在Java的平常應用程序開發中,類的加載幾乎是由,上述3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,來定製類的加載方式。
爲何要自定義類加載器?
隔離加載類。
修改類加載的方式。
擴展加載源。
防止源碼泄漏。
用戶自定義類加載器實現步驟:
1.開發人員能夠經過繼承抽象類java. lang. ClassLoader類的方式,實現本身的類加載器,以知足一些特殊的需求。
2.在JDK1.2以前,在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass ()方法,從而實現自定義的類加載類,可是在JDK1.2以後已再也不建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中。
3.在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承URLClassLoader類,這樣就能夠避免本身去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。
1.在JVM中表示兩個class對象是否爲同一個類存在兩個必要條件:
類的完整類名必須一致,包括包名。
加載這個類的ClassLoader (指ClassLoader實例對象)必須相同。.
即便兩個類對象來源於同一個Class文件,被同一個虛擬機所加載,但只要它們的ClassLoader不一樣,那這兩個類對象也是不同的
2.JVM必須知道-一個類型是由啓動加載器加載的仍是由用戶類加載器加載的。若是一個類型是由用戶類加載器加載的,那麼JVM會將這個類加載器的一個引用做爲類型信息的一部分保存在方法區中。當解析一個類型到另外一個類型的引用的時候,JVM須要保證這兩個類型的類加載器是相同的。
Java,虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象。並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。
工做原理
1)若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行;
2)若是父類加載器還存在其父類加.載器,則進一步向上委託,依次遞歸,請求最終將到達項層的啓動類加載器;
3)若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式。
優點:
避免類的重複加載
保護程序安全,防止核心API被隨意篡改
自定義String類,可是在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程當中會先加載jdk自帶的文件(rt.jar包中java \lang\String.class),報錯信息說沒有main方法,就是由於加載的是rt. jar包中的String類。這樣能夠保證對java核心源代碼的保護,這就是沙箱安全機制。
Java虛擬機定義了若千種程序運行期間會使用到的運行時數據區,其中有一些會隨着虛擬機啓動而建立,隨着虛擬機退出而銷燬。另一些則是與線程一 一對應的,這些與線程對應的數據區域會隨着線程開始和結束而建立和銷燬。
一個運行時數據區間對應一個Runtime Class(能夠經過getRuntime()獲取到),因此Runtime Class是單例的。(詳見javaSE api)
灰色的爲單獨線程私有的,紅色的爲多個線程共享的。即:
每一個線程:獨立包括程序計數器、棧、本地棧。
線程間共享:堆、堆外內存(永久代或元空間、代碼緩存)
做用:
PC寄存器用來存儲指向下一條指令的地址,也即將要執行的指令代碼。由執行引擎讀取下一條指令。
任什麼時候間一一個線程都只有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的Java方法的JVM指令地址,或者,若是是在執行native方法,則是未指定值(undefined) 。
使用PC寄存器存儲字節碼指令地址有什麼用呢?
爲何使用PC寄存器記錄當前線程的執行地址呢?
由於CPU須要不停的切換各個線程,這時候切換回來之後,就得知道接着從哪開始繼續執行。
JVM的字節碼解釋器就須要經過改變PC寄存器的值來明確下一條應該執行什麼樣的字節碼指令。
PC寄存器爲何會被設定爲線程私有?
咱們都知道所謂的多線程在一個特定的時間段內只會執行其中某一個線程的方法,CPU會不停地作任務切換,這樣必然致使常常中斷或恢復,如何保證分毫無差呢?爲了可以準確地記錄各個線程正在執行的當前字節碼指令地址,最好的辦法天然是爲每個線程都分配一個PC寄存器,這樣一來各個線程之間即可以進行獨立計算,從而不會出現相互干擾的狀況。因爲CPU時間片輪限制,衆多線程在併發執行過程當中,任何一個肯定的時刻,一個處理器或者多核處理器中的一個內核,只會執行某個線程中的一條指令。這樣必然致使常常中斷或恢復,如何保證分毫無差呢?每一個線程在建立後,都會產生本身的程序計數器和棧幀,程序計數器在各個線程之間互不影響。
Java虛擬機棧是什麼?
Java虛擬機棧(Java Virtual Machine Stack) ,早期也叫Java棧。每一個線程在建立時都會建立一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame) ,對應着一次次的Java方法調用。
是線程私有的
生命週期:生命週期和線程一致。
做用
主管Java程序的運行,它保存方法的局部變量(8種基本數據類型、對象的引用地址)、部分結果,並參與方法的調用和返回。
局部變量VS成員變量(或屬性)
基本數據變量VS引用類型變量(類、數組、接口)
可能出現的異常
在這個內存區域中,若是線程請求的棧幀深度大於虛擬機所容許的深度,將拋出StarkOverflowError異常;若是java虛擬機棧容量能夠動態擴展,當棧擴展是沒法申請到足夠的內存時會拋出OutOfMemoryError異常。
設置棧內存大小
能夠使用參數-Xss選徐來設置線程最大棧空間,棧的大小直接決定了函數調用的最大可達深度。
private int count = 0; public static void main(String args[]){ count++; main(rgs[]); } //最後報出StarkOverflowError
每一個線程都有本身的棧,棧中的數據都是以棧楨(stack Frame)的格式存在。
在這個線程上正在執行的每一個方法都各自對應一個棧楨(Stack Frame)
棧幀是一個內存區塊,是一個數據集,維繫着方法執行過程當中的各類數據信息。
JVM直接對Java棧的操做只有兩個,就是對棧幀的壓棧和出棧,遵循「先進後出」/「後進先出」原則。
在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀(Current Frame) ,與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)。
執行引擎運行的全部字節碼指令只針對當前棧幀進行操做。
若是在該方法中調用了其餘方法,對應的新的棧幀會被建立出來,放在棧的頂端,成爲新的當前幀。
不一樣線程中所包含的棧幀是不容許存在相互引用的,即不可能在一個棧幀之中引用另一個線程的棧幀。
若是當前方法調用 了其餘方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀從新成爲當前棧幀。
Java方法有 兩種返回函數的方式,一種是正常的函數返回,使用return指令; 另一種是拋出異常。無論使用哪一種方式,都會致使棧幀被彈出。
就是指棧幀中的方法返回地址、動態連接和附加信息
每一個棧楨中存儲着:
局部變量表(Local Variables)
操做數棧(operand stack) (或表達 式棧)
動態連接(Dynamic Linking) ( 或指向運行時常量池的方法引用)
方法返回地址(Return Address) ( 或方法正常退出或者異常退出的定義)
一些附加信息
關於jclasslib操做在視頻的p49
其大小在Class反編譯文件中以locals查看,在jclasslib中的misc下
局部變量表也被稱之爲局部變量數組或本地變量表。
定義爲一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各種基本數據類型、對象引用(reference) ,以及,returnAddress類型。
因爲局部變量表是創建在線程的棧上,是線程的私有數據,所以不存在數據安全問題。
局部變量表所需的容量大小是在編譯期肯定下來的,並保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的。.
方法嵌套調用的次數由棧的大小決定。通常來講,棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以知足方法調用所需傳遞的信息增大的需求。進而函數調用就會佔用更多的棧空間,致使其嵌套調用次數就會減小。
局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機經過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束後,隨着方法棧幀的銷燬,局部變量表也會隨之銷燬。
參數值的存放老是在局部變量數組的index0開始,到數組長度-1的索引結束。
局部變量表,最基本的存儲單元是Slot (變量槽)
局部變量表中存放編譯期可知的各類基本數據類型(8種),引用類型(reference),returnAddress類型的變量。
在局部變量表裏,32位之內的類型只佔用一個slot (包括returnAddress類型),64位的類型(long和double)佔用兩個slot。
byte、short、char在存儲前被轉換爲int,boolean 也被轉換爲int,0表示false,非0表示true。
long和double則佔據兩個Slot。
棧幀中的局部變量表中的槽位是能夠重用的,若是一個局部變量過了其作用域,那麼在其做用域以後申明的新的局部變量就頗有可能會複用過時局變量的槽位,從而達到節省資源的目的。
參數表分配完畢以後,再根據方法體內定義的變量的順序和做用域分配。咱們知道類變量表有兩次初始化的機會,第一次是在「準備階段」,執行系統初始化,對類變量設置零值,另外一次則是在「初始化」階段,賦予程序員在代碼中定義的初始值。
和類變量初始化不一樣的是,局部變量表不存在系統初始化的過程,這意味着一旦定義了局部變量則必須人爲的初始化,不然沒法使用。
ps:變量的分類:
按照數據類型分:基本數據類型 & 引用數據類型
按照類中聲明的位置分:
成員變量
在使用前,都經歷過默認初始化賦值
有static修飾:類變量-->在linking的prepare階段給其默認賦值,在init階段顯示賦值
無static修飾:實例變量-->在對象建立時會在堆中分配內存,進行默認賦值
局部變量
在使用時必須進行顯示賦值,不然編譯不經過
如: public static void mian(String args[]){ int i; System.out.println(i);//變量i未初始化 }
在棧幀中,與性能調優關係最爲密切的部分就是前面提到的局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。
操做數棧,主要用於保存計算過程的中間結果,同時做爲計算過程當中變量臨時的存儲空間。
操做數棧就是JVM執行引擎的一個 工做區,當一個方法剛開始執行的候,一個新的棧幀也會隨之被建立出來,這個方法的操做數棧是空的。
每個操做數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,爲max_ stack的值。
棧中的任何一個元素都是能夠任意的Java數據類型。
32bit的類型佔用一個棧單位深度
64bit的類型佔用兩個棧單位深度操做數棧並不是採用訪問索引的方式來進行數據訪問的,而是隻能經過標準的入棧(push) 和出棧(pop) 操做來完成一次數據訪問。
若是被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操做數棧中,並更新PC寄存器中下一條須要執行的字節碼指令。
操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載過程當中的類檢驗階段的數據流分析階段要再次驗證。
另外,咱們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操做數棧。
Public static void main(String args[]){ int i = 1; int j = 2; int l = i+j; System.out.println(l); }
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 //stack即爲操做數棧深度,若是此方法未用靜態修飾,則局部變量表長度爲5,索引0是this,在此方法中,因爲是main方法,因此索引0是args 0: iconst_1//int i-->初始化int類型常量,壓入操做數棧中<--棧頂(深度1) 1: istore_1//出棧,存入局部變量表,索引位置爲1 2: iconst_2//itn j-->初始化int類型常量,壓入操做數棧中<--棧頂(深度1) 3: istore_2//出棧,存入局部變量表,索引位置爲2 4: iload_1//得到局部變量表中索引爲1的值,壓入操做數棧中(深度1) 5: iload_2//得到局部變量表中索引爲2的值,壓入操做數棧中(深度2) 6: iadd //取出操做數棧中的值,交由執行引擎執行求和操做,其求和的值再次壓入操做數棧中 7: istore_3//出棧,將其存入局部變量表中索引爲3的位置 8: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3//得到局部變量表中索引爲3的值,壓入操做數棧中,執行輸出操做 12: invokevirtual #22 // Method java/io/PrintStream.println:(I)V 15: return
前面提過,基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操做的時候必然須要使用更多的入棧和出棧指令,這同時也就意味着將須要更多的指令分派(instruction dispatch) 次數和內存讀/寫次數。
因爲操做數是存儲在內存中的,所以頻繁地執行內存讀/寫操做必然會影響執行速度。爲了解決這個問題,Hotspot JVM的設計者們提出了棧頂緩存(ToS,Top-of-Stack Cashing) 技術,將棧頂元索所有緩存在物理CPU的寄存器中,以此下降對內存的讀/寫次數,提高執行引擎的執行效率。
每個棧幀內部都包含一個指向運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前方法的代碼可以實現動態連接(Dynamic Linking) 。好比: invokedynamic指令。
在Java源文件被編譯到字節碼文件中時,全部的變量和方法引用都做爲符號引用(Symbolic Reference) 保存在class文件的常量池裏。好比:描述一個方法調用了另外的其餘方法時,就是經過常量池中指向方法的符號引用來表示的,那麼動態連接的做用就是爲了將這些符號引用轉換爲調用方法的直接引用。
在棧楨中存在有常量池,當線程開始運行時,棧楨中的常量池會載入到方法區中,也就是其中的運行時常量池,每個棧楨的常量池中的符號引用均可以引用運行時常量池中的方法,這樣一來,就節省了內存空間。
在JVM中,將符號引用轉換爲調用方法的直接引用與方法的綁定機制相關。
●靜態連接:
當一個字節碼文件被裝載進JVM內部時,若是被調用的目標方法在編譯可知,且運行期保持不變時。這種狀況下將調用方法的符號引用轉換爲直接引用的過程稱之爲靜態連接。●動態連接:
若是被調用的方法在編譯期沒法被肯定下來,也就是說,只可以在程序運行期將調用方法的符號引用轉換爲直接引用,因爲這種引用轉換過程具有動態性,所以也就被稱之爲動態連接。
對應的方法的綁定機制爲:早期綁定(Early Binding)和晚期綁定(Late Binding) 。綁定是一個字段、方法或者類在符號引用被替換爲直接引用的過程,這僅僅發生一次。
●早期綁定:
早期綁定就是指被調用的目標方法若是在編譯期可知,且運行期保持不變時,便可將這個方法與所屬的類型進行綁定,這樣一來,因爲明確了被調用的目標方法到底是哪-一個,所以也就能夠使用靜態連接的方式將符號引用轉換爲直接引用。●晚期綁定:
若是被調用的方法在編譯期沒法被肯定下來,只可以在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之爲晚期綁定。
隨着高級語言的橫空出世,相似於Java同樣的基於面向對象的編程語言現在愈來愈多,儘管這類編程語言在語法風格上存在必定的差異,可是它們彼此之間始終保持着一個共性,那就是都支持封裝、繼承和多態等面向對象特性,既然這一類的編程語言具有多態特性,那麼天然也就具有早期綁定和晚期綁定兩種綁定方式。
Java中任何一個普通的方法其實都具有虛函數(晚期綁定)的特徵,它們至關於C++語言中的虛函數(C++中則須要使用關鍵字virtual來顯式定義)。若是在Java程序中不但願某個方法擁有虛函數的特徵時,則能夠使用關鍵字final來標記這個方法。
非虛方法:
●若是方法在編譯期就肯定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱爲非虛方法。
●靜態方法、私有方法、final方法、實例構造器、父類方法都是非虛方法。
●其餘方法稱爲虛方法。
子類對象的多態性的使用前提:①類的繼承關係②方法的重寫
虛擬機中提供瞭如下幾條方法調用指令:
普通調用指令:
- invokestatic: 調用靜態方法,解析階段肯定惟一方法版本
- invokespecial: 調用
方法、私有及父類方法,解析階段肯定惟一方法版本 - invokevirtual: 調用全部虛方法
- invokeinterface:調用接口方法
動態調用指令:
invokedynamic: 動態解析出須要調用的方法,而後執行前四條指令固化在虛擬機內部,方法的調用執行不可人爲干預,而invokedynamic指令則支持由用戶肯定方法版本。其中invokestatic指令和invokespecial指令調用的方法稱爲非虛方法,其他的(final修飾的除外)稱爲虛方法。
在class文件中,若使用了Lambda表達式來定義匿名方法,其在字節碼中就會以invokedynamic修飾。
JVM字節碼指令集一直比較穩定,直到Java7中才增長了一個invokedynamic指令,這是Jaya爲了實現「動態類型語言」支持而作的一種改進。
可是在Java7中並無提供直接生成invokedynamic指令的方法,須要藉助ASM這種底層字節碼工具來產生invokedynamic指令。直到Java8的Lambda表達式的出現,invokedynamic指令的生成,在Java中才有了直接的生成方式。
Java7中增長的動態語言類型支持的本質是對Java虛擬機規範的修改,而不是對Java語言規則的修改,這一塊相對來說比較複雜,增長了虛擬機中的方法調用,最直接的受益者就是運行在Java平臺的動態語言的編譯器。
動態類型語言和靜態類型語言二者的區別就在於對類型的檢查是在編譯期仍是在運行期,知足前者就是靜態類型語言,反之是動態類型語言。
說的再直白一點就是,靜態類型語言是判斷變量自身的類型信息:動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值纔有類型信息,這是動態語言的一個重要特徵。
靜態類型語言:Java: String info = "abcd";
動態類型語言:Js: var info="abcd" var info=1
動態類型語言:python: info=100.1
做用:存放調用該方法的pc寄存器的值。(只保存正常退出的方法的pc寄存器的值)
一個方法的結束,有兩種方式:
正常執行完成
出現未處理的異常,非正常退出
不管經過哪一種方式退出,在方法退出後都返回到該方法被調用的位置。方法正常退出時,調用者的pc計數器的值做爲返回地址,即調用該方法的指令的下一條指令的地址。而經過異常退出的,返回地址是要經過異常表來肯定,棧幀中通常不會保存這部分信息。
本質上,方法的退出就是當前棧幀出棧的過程。此時,須要恢復上層方法的局部變量表、操做數棧、將返回值壓入調用者棧幀的操做數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
正常完成出口和異常完成出口的區別在於:經過異常完成出口退出的不會給他的上層調用者產生任何的返回值。
當一個方法開始執行後,只有兩種方式能夠退出這個方法:
執行引擎遇到任意一 一個方法返回的字節碼指令(return),會有返回值傳遞給上層的方法調用者,簡稱正常完成出口;
一個方法在正常調用完成以後究竟須要使用哪個返回指令還須要根據方法返回值的實際數據類型而定;
在字節碼指令中,返回指令包含ireturn (當返回值是boolean、 byte、char、short和int類型時使用)、lreturn(long類型)、 freturn(float類型)、 dreturn(double類型)以及areturn(引用類型:String、Date),另外還有一個return指令供聲明爲void的方法、實例初始化方法、類和接口的初始化方法使用。
class字節碼中的異常處理表 Exception table from to target type 4 16 19 any//在4-16行中的任何異常,由19行來處理 19 21 19 any
本質上,方法的退出就是當前棧幀出棧的過程。此時,須要恢復上層方法的局部變量表、操做數棧、將返回值壓入調用者棧幀的操做數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
正常完成出口和異常完成出口的區別在於:經過異常完成出口退出的不會給他的上層調用者產生任何的返回值。
棧幀中還容許攜帶與Java虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息。(不必定有)
Java虛擬機棧用於管理Java方法的調用,而本地方法棧用於管理本地方法的調用。
本地方法棧,也是線程私有的。
容許被實現成固定或者是可動態擴展的內存大小。(在內存溢出方面是相同的)
若是線程請求分配的棧容量超過本地方法棧容許的最大容量,Java 虛擬機將會拋出一個stackoverflowError 異常。
若是本地方法棧能夠動態擴展,而且在嘗試擴展的時候沒法申請到足夠的內存,或者在建立新的線程時沒有足夠的內存去建立對應的本地方法棧,那麼Java虛擬機將會拋出一個outofMemoryError 異常。
本地方法是使用C語言實現的。
它的具體作法是Native Method Stack中 登記native方法,在Execution Engine執行時加載本地方法庫。
當某個線程調用一一個本地方法時,它就進入了一個全新的而且再也不受虛擬機限制的世界。它和虛擬機擁有一樣的權限。
本地方法能夠經過本地方法接0來訪問虛擬機內部的運行時數據區。
它甚至能夠直接使用本地處理器中的寄存器
直接從本地內存的堆中分配任意數量的內存。
並非全部的JVM都支持本地方法。由於Java虛擬機規範並無明確要求本地方法棧的使用語言、具體實現方式、數據結構等。若是JVM產品不打算支持native方法,也能夠無需實現本地方法棧。
在Hotspot JVM中,直接將本地方法棧和虛擬機棧合二爲一。
本地方法接口&本地方法庫
簡單地講,一個Native Method就是一個Java調用非Java代碼的接口。一個Native Method是這樣-一個Java方法:該方法的實現由非Java語言實現,好比C。這個特徵並不是Java所特有,不少其它的編程語言都有這一機制,好比在C++中,你能夠用extern "C"告知C++編譯器去調用 一個c的函數。
"A native method is a Java method whose implementation is provided by non-java code."
在定義一個native method時, 並不提供實現體(有些像定義一個Javainterface),由於其實現體是由非java語言在外面實現的。本地接口的做用是融合不一樣的編程語言爲Java所用,它的初衷是融合C/C++程序。
一個JVM實例只存在-一個堆內存,堆也是Java內存管理的核心區域。Java堆區在JVM啓動的時候即被建立,其空間大小也就肯定了。是JVM管理的最大一塊內存空間。
堆內存的大小是能夠調節的。
《Java虛擬機規範》規定,堆能夠處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的。
全部的線程共享Java堆,在這裏還能夠劃分線程私有的緩衝區(Thread Local Allocation Buffer, TLAB) 。
《Java虛擬機規範》中對Java堆的描述是:全部的對象實例以及數組都應當在運行時分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated )
我要說的是:「幾乎」全部的對象實例都在這裏分配內存。——從實際使用角度看的。
數組和對象可能永遠不會存儲在棧上,由於棧幀中保存引用,這個引用指向對象或者數組在堆中的位置。
在方法結束後,堆中的對象不會立刻被移除,僅僅在垃圾收集的時候纔會被移除。
堆,是GC ( Garbage Collection, 垃圾收集器)執行垃圾回收的重點區域。
Java 7及以前堆內存邏輯上分爲三部分:新生區+養老區+永久區 ➢Young Generation Space 新生區 Young/New 又被劃分爲Eden區和Survivor區 ➢Tenure generation space 養老區 Old/ Tenure ➢Permanent Space 永久區 Perm Java 8及以後堆內存邏輯上分爲三部分:新生區+養老區+元空間 ➢Young Generation Space 新生區 Young/New 又被劃分爲Eden區和Survivor區 ➢Tenure generation space 養老區 Old/Tenure ➢Meta Space 元空間 Meta
Java堆區用於存儲Java對象實例,那麼堆的大小在JVM啓動時就已經設定好了,能夠經過選項」-Xmx"和」-Xms"來進行設置。
「-Xms"用於表示堆區的起始內存,等價於-XX: InitialHeapSize
「-Xmx" 則用於表示堆區的最大內存, 等價於-XX :MaxHeapSize一旦堆區中的內存大小超過「-Xmx"所指定的最大內存時,將會拋出OutOfMemoryError異常。
一般會將-Xms 和- -Xmx兩個參數配置相同的值,其目的是爲了可以在java垃圾回收機制清理完堆區後不須要從新分隔計算堆區的大小,從而提升性能。
1.設置堆空間大小的參數
-Xms. 用來設置堆空間(年輕代+老年代)的初始內存大小
-X是jvm的運行參數
ms是memory start
-Xmx用來設置堆空間(年輕代+老年代)的最大內存大小
2.默認堆空間的大小
初始內存大小:物理電腦內存大小/ 64
最大內存大小:物理電腦內存大小/ 4
3.手動設置: -Xms600m -Xmx600m
開發中建議將初始堆內存和最大的堆內存設置成相同的值。
4.查看設置的參數:
方式一(cmd): jps / jstat -gc進程id
方式二(編譯器中設置): -XX: +PrintGCDetails
public class OOMTest{ public static void main(String args[]){ ArrayList<Picture> list = enw ArrayList<>(); while(true){ list.add(new Picture(new Random().nextInt(1026*1024))); } } } //最後報出OOM
存儲在JVM中的Java對象能夠被劃分爲兩類:
- 一類是生命週期較短的瞬時對象,這類對象的建立和消亡都很是迅速
- 另一類對象的生命週期卻很是長,在某些極端的狀況下還可以與JVM的生命週期保持一致。
Java堆區進一步細分的話, 能夠劃分爲年輕代(YoungGen) 和老年代(0ldGen)
其中年輕代又能夠劃分爲Eden空間、Survivor0空間和Survivor1空間(有時也叫作from區、to區)
默認-XX: NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
能夠修改-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5
在一樣內存下,佔比少的部分GC也就更頻繁。
在HotSpot中,Eden空間和另外兩個Survivor空間缺省所佔的比例是8:1:1
固然開發人員能夠經過選項「-XX: SurvivorRatio"調整這個空間比例。好比-XX: SurvivorRatio=8.
幾乎全部的Java對象都是在Eden區被new出來的。
絕大部分的Java對象的銷燬都在新生代進行了。
IBM公司的專門研究代表,新生代中80%的對象都是「朝生夕死」的。
能夠使用選項」-Xmn"設置新生代最大內存大小
這個參數通常使用默認值就能夠了,通常開發中設置好了堆的大小和比例就等因而肯定了新生代的大小。
ps:JVM規範中提到,新生代中各內存比例是8:1:1,可是實際運行中倒是6:1:1.緣由是其有一個自適應的內存分配策略(此策略是默認使用的)
能夠手動設置"-XX: SurvivorRatio = 8"來實現8:1:1
-XX: -UseAdaptivesizePolicy :關閉自適應的內存分配策略( 暫時用不到)
爲新對象分配內存是一件很是嚴謹和複雜的任務,JVM的設計者們不只須要考慮內存如何分配、在哪裏分配等問題,而且因爲內存分配算法與內存回收算法密切相關,因此還須要考慮GC執行完內存回收後是否會在內存空間中產生內存碎片。
針對倖存者s0,s1區的總結:複製以後有交換,誰空誰是to.
關於垃圾回收:頻繁在新生區收集,不多在養老區收集,幾乎不在永久區/元空間收集。
JVM在進行GC時,並不是每次都對,上面三個內存區域(指Eden s0 s1 )一塊兒回收的,大部分時候回收的都是指新生代。
針對HotSpotVM的實現,它裏面的GC按照回收區域又分爲兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
部分收集:不是完整收集整個Java堆的垃圾收集。其中又分爲:
新生代收集(Minor GC / Young GC) :只是新生代的垃圾收集
老年代收集(MajorGC/0ldGC):只是老年代的垃圾收集。
目前,只有CMS GC會有單獨收集老年代的行爲。
注意,不少時候Major GC會和Full GC混淆使用,須要具體分辨是老年代回收仍是整堆回收。
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。
目前,只有G1 GC會有這種行爲
整堆收集(Fu1l GC): 收集整個java堆和方法區的垃圾收集。
觸發機制
當年輕代空間不足時,就會觸發Minor GC,這裏的年輕代滿指的是Eden代滿,Survivor滿不會引起GC。(每次 Minor GC會清理年輕代的內存。)
由於Java 對象大多都具有朝生夕滅的特性,因此Minor GC很是頻繁,通常回收速度也比較快。這必定義既清晰又易於理解。
Minor GC會引起STW, 暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行。
觸發機制:
指發生在老年代的GC,對象從老年代消失時,咱們說「Major GC"或「Fu11 GC」發生了。
出現了Major GC,常常會伴隨至少一次的Minor GC (但非絕對的,在Paral1elScavenge收集器的收集策略裏就有直接進行MajorGC的策略選擇過程)。
也就是在老年代空間不足時,會先嚐試觸發Minor GC。若是以後空間還不足,則觸發Major GC。
Major GC的速度- °般會比Minor GC慢10倍以上,STW的時間更長。
若是Major GC後,內存還不足,就報00M了。
觸發機制
(後面細講)
觸發Fu1l GC執行的狀況有以下五種:
(1)調用System.gc()時,系統建議執行Fu11 GC,可是沒必要然執行。
(2)老年代空間不足。
(3)方法區空間不足。
(4)經過Minor GC後進入老年代的平均大小大於老年代的可用內存。
(5)由Eden區、survivor space0 (From Space)區向survivor space1 (To Space)區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小。
說明: full gc是開發或調優中儘可能要避免的。這樣暫時時間會短一-些。
不分代就不能正常工做了嗎?
其實不分代徹底能夠,分代的惟一理由就是優化Gc性能。若是沒有分代,那全部的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的全部區域進行掃描。而不少對象都是朝生夕死的,若是分代的話,把新建立的對象放到某一地方,當GC的時候先把這塊存儲「朝生夕死」對象的區域進行回收,這樣就會騰出很大的空間出來。
通常狀況
若是對象在Eden出生並通過第一次MinorGC 後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,並將對 象年齡設爲1。對象在Survivor區中每熬過一 次MinorGC ,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲,其實每一個JVM、 每一個GC都有所不一樣)時,就會被晉升到老年代中。
對象晉升老年代的年齡閾值,能夠經過選項-XX :MaxTenuringThreshold來設置。
針對不一樣年齡段的對象分配原則以下所示:
優先分配到Eden
大對象直接分配到老年代
儘可能避免程序中出現過多的大對象
長期存活的對象分配到老年代
動態對 象年齡判斷
若是Survivor區中相同年齡的全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
空間分配擔保
-XX: HandlePromotionFai lure
why?
堆區是線程共享區域,任何線程均可以訪問到堆區中的共享數據
因爲對象實例的建立在JVM中很是頻繁,所以在併發環境下從堆區中劃份內存空間是線程不安全的
爲避免多個線程操做同一地址,須要使用加鎖等機制,進而影響分配速度。
what?
從內存模型而不是垃圾收集的角度。對Eden區域繼續進行劃分,JVM爲每一個線程分配了-一個私有緩存區域,它包含在Eden空間內。
多線程同時分配內存時,使用TLAB能夠避免一系列的非線程安全問題,同時還可以提高內存分配的吞吐量,所以咱們能夠將這種內存分配方式稱之爲快速分配策略。
據我所知全部OpenJDK衍生出來的JVM都提供了TLAB的設計。
儘管不是全部的對象實例都可以在TLAB中成功分配內存,但JVM確實是將TLAB做爲內存分配的首選。
在程序中,開發人員能夠經過選項「-XX :UseTLAB」設置是否開啓TLAB空間。
默認狀況下,TLAB空間的內存很是小,僅佔有整個Eden空間的1號,固然咱們能夠經過選項「-XX:TLABWasteTargetPercent」設置TLAB空間所佔用Eden空間的百分比大小。
一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試着經過使用加鎖機制確保數據操做的原子性,從而直接在Eden空間中分配內存。.
測試堆空間經常使用的jvm參數:
-XX: +PrintFlagsInitial :查看全部的參數的默認初始值
-XX: +PrintFlagsFinal : 查看全部的參數的最終值(可能會存在修改,再也不是初始值)
具體查看某個參數的指令: jps: 查看當前運行中的進程
cmd中查看:jinfo -flag survivorRatio 進程id
-Xms:初始堆空間內存(默認爲物理內存的1/64)
-Xmx:最大堆空間內存(默認爲物理內存的1/4)
-Xmn:設置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結構的佔比
-XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
-XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡
-Xx: +PrintGCDetails:輸出詳細的GC處理日誌
打印gc簡要信息:⑧-XX:+PrintGC ② - verbose:gc
-XX:HandlePromotionFailure:是否設置空間分配擔保
在發生Minor Gc之 前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代全部
對象的總空間。
若是大於,則這次Minor GC是安全的若是小於,則虛擬機會查看-XX : HandlePromot ionFai lure設置值是否容許擔保失敗。
若是HandlePromotionFailure=true, 那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小。
v若是大於,則嘗試進行一次Minor GC, 但此次Minor GC依然是有風險的;
V若是小於,則改成進行一次Full GC。
V若是HandlePromot ionFailure=false,則改成進行一-次Full GC。
在JDK6 Update24之 後(JDK7) ,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察OpenJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,可是在代碼中已經不會再使用它。JDK7以後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。
如何快速的判斷是否發生了逃逸分析, 就看new的對象實體是否有可能在方法外被調用。
逃逸分析設置(默認啓用): -XX: -DoEscapeAnalysis
隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化,全部的對象都分配到堆上也漸漸變得不那麼「絕對」了。
在Java虛擬機中,對象是在Java堆中分配內存的,這是一個廣泛的常識。可是,有一種特殊狀況,那就是若是通過逃逸分析(Escape Analysis) 後發現,一個對象並無逃逸出方法的話,那麼就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最多見的堆外存儲技術。
此外,前面提到的基於OpenJDK深度定製的TaoBaoVM,其中創新的GCIH (GC invisible heap) 技術實現off - heap,將生命週期較長的Java對象從heap中移至heap外,而且GC不能管理GCIH內部的Java對象,以此達到下降GC的回收頻率和提高GC的回收效率的目的。
如何將堆上的對象分配到棧,須要使用逃逸分析手段。
這是一種能夠有效減小Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。
經過逃逸分析,Java Hotspot編 譯器可以分析出一一個新的對象的引用的使用範圍從而決定是否要將這個對象分配到堆上。
逃逸分析的基本行爲就是分析對象動態做用域:
當一個對象在方法中被定義後,對象只在方法內部使用,則認爲沒有發生逃逸。
當一個對象在方法中被定 義後,它被外部方法所引用, 則認爲發生逃逸。例如做爲調用參數傳遞到其餘地方中。
沒有發生逃逸的對象,則能夠分配到棧上,隨着方法執行的結束,棧空間就被移除。
//此對象的做用空間只在main方法內部,因此沒有發生逃逸 public static void main(){ V v = new v(); v = null; }
//StringBuffer對象被return出去,可能會被其餘方法調用,因此此對象發生了逃逸 public StringBuffer strBfer(){ StringBuffer sb = new StringBuffer(); sb.append(s1); return sb } //通過優化後,返回了一個String的一個新對象,StringBuffer沒有被return出去,因此沒有發生逃逸 public StringBuffer strBfer(){ StringBuffer sb = new StringBuffer(); sb.append(s1); return sb.toString; }
使用逃逸分析,編譯器能夠對代碼作以下優化:
1.棧上分配。將堆分配轉化爲棧分配。若是一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象多是棧分配的候選,而不是堆分配。
2.同步省略。若是一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操做能夠不考慮同步。
3.分離對象或標量替換。有的對象可能不須要做爲一個連續的內存結構存在也能夠被訪問到,那麼對象的部分(或所有)能夠不存儲在內存,而是存儲在CPU寄存器中。
JIT編譯器在編譯期間根據逃逸分析的結果,發現若是一一個對象並無逃逸出方法的話,就可能被優化成棧.上分配。分配完成後,繼續在調用棧內執行,最後線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了。
常見的棧上分配的場景
在逃逸分析中,已經說明了。分別是給成員變量賦值、方法返回值、實例引用傳遞。
//測試代碼 //-Xms=1G -Xmx=1G -XX:+PrintGCDetails //-Xms=1G -Xmx=1G -XX: -DoEscapeAnalysis -Xx:+PrintGCDetails public void main(Strign[] args){ long start = System.currenTimeMillis(); for(int i = 0;i<1000000;i++){ alloc(); } long end = System.currenTimeMillis(); System.out.println("話費的時間爲:"+(end-start)); try{ Thread.sleep(1000000); }catch(Exception e){ e.pringtSackTrace; } } private static void alloc(){ User user = new User(); } static class User{ }
線程同步的代價是至關高的,同步的後果是下降併發性和性能。
在動態編譯同步塊的時候,JIT編譯器能夠藉助逃逸分析來判斷同步塊所使用的鎖對象是否只可以被一個線程訪問而沒有被髮布到其餘線程。若是沒有,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提升併發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除。
//以下代碼 public void sy(){ Object hollis = new Object(); synchronized(hollis){ System.out.println(hollis); } } //因爲對象並無發生逃逸,逃逸分析會自動同步省略,須要主意的一點是,同步省略是在加載到內存以後發生的,在編譯期間仍是能夠看見同步(monitorenter&monitorexitr)的字節碼
標量替換參數設置:
-XX:+EliminateAllocations:開啓了標量替換(默認打開),容許將對象打散分配在棧上。
標量(scalar)是指一個沒法再分解成更小的數據的數據。Java中的原始數據類型就是標量。
相對的,那些還能夠分解的數據叫作聚合量(Aggregate) ,Java中的對象就是聚合量,由於他能夠分解成其餘聚合量和標量。
在JIT階段,若是通過逃逸分析,發現-一個對象不會被外界訪問的話,那麼通過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換。,
//標量抽象概念 class user{//聚合量 String name;//標量 String age; Accont Acc; } class Accont{//聚合量 double balance;//標量 }
//標量替換 public static void main(String[] args){ alloc(); } public static alloc(){//point對象做用域只在alloc方法內,未發生逃逸 point p = new point(1,2); System.out.println(p.a+"--"p.b); } class point{ private int a; private int b; } //標量替換優化後------------------------------------- //能夠看到,Point這個聚合量通過逃逸分析後,發現他並無逃逸,就被替換成兩個聚合量了。那麼標量替換有什麼好處呢?就是能夠大大減小堆內存的佔用。由於一旦不須要建立對象了, 那麼就再也不須要分配堆內存了 。 public static alloc(){ int a = 1; int b = 2; System.out.println(p.a+"--"p.b); }
堆、棧與方法區的交互關係
Person person = new Person(); 方法區 java棧 java堆
方法區在哪裏?
《Java虛擬機規範》中明確說明:「儘管全部的方法區在邏輯上是屬於堆的一部分,但.一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。」但對於HotspotJVM而言,方法區還有一個別名叫作Non-Heap (非堆),目的就是要和堆分開。
因此,方法區看做是一塊獨立於Java堆的內存空間。
方法區基本理解
方法區 (Method Area) 與Java堆同樣,是各個線程共享的內存區域。
方法區在JVM啓動的時候被建立,而且它的實際的物理內存空間中和Java堆區--樣均可以是不連續的。
方法區的大小,跟堆空間同樣,能夠選擇固定大小或者可擴展。方法區的大小決定 了系統能夠保存多少個類,若是系統定義了太多的類,致使方法區溢出,虛擬機一樣會拋出內存溢出錯誤: java. lang . OutOfMemoryError :PermGen space或者java. lang.OutOfMemoryError: Metaspace
加載大量的第三方的jar包; Tomcat部署的工程過多(30-50個) ;大量動態的生成反射類
關閉JVM就會釋放這個區域的內存。
Hostspot中方法區的演進
到了JDK 8, 終於徹底廢棄了永久代的概念,改用與JRockit、J9同樣在本地內存中實現的元空間(Metaspace) 來代替。
元空間的本質和永久代相似,都是對JVM規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機設置的內存中,而是使用本地內存。
永久代、元空間兩者並不僅是名字變了,內部結構也調整了。
根據《Java虛擬機規範》的規定,若是方法區沒法知足新的內存分配需求時,將拋出OOM異常。
元數據區大小能夠使用參數-XX:MetaspaceSize和-XX: MaxMetaspaceSize指定,替代上述原有的兩個參數。
默認值依賴於平臺。windows下,-XX:MetaspaceSize是21M,-XX :MaxMetaspaceSize的值是-1,即沒有限制。
與永久代不一樣, 若是不指定大小,默認狀況下,虛擬機會耗盡全部的可用系統內存。若是元數據區發生溢出,虛擬機同樣會拋出異常OutOfMemoryError: Metaspace
-XX:MetaspaceSize: 設置初始的元空間大小。對於一個64位的服務器端JVM來講,其默認的-XX :MetaspaceSize值爲21MB。這就是初始的高水位線,一旦觸及這個水位線,FullGC將會被觸發並卸載沒用的類(即這些類對應的類加載器再也不存活),而後這個高水位線將會重置。新的高水位線的值取決於GC後釋放了多少元空間。若是釋放的空間不足,那麼在不超過MaxMetaspaceSize時,適當提升該值。若是釋放空間過多,則適當下降該值。
若是初始化的高水位線設置太低,上述高水位線調整狀況會發生不少次。經過垃圾回收器的日誌能夠觀察到Full GC屢次調用。爲了不頻繁地GC,建議將-XX :MetaspaceSize設置爲一個相對較高的值。
一、要解決OOM異常或heap space的異常,一 般的手段是首先經過內存映像分析工具(如Eclipse Memory Analyzer) 對dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是要先分清楚究竟是出現了內存泄漏(Memory Leak)仍是內存溢出(Memory Overflow)。
二、若是是內存泄漏,可進一步經過工具查看泄漏對象到GC Roots的引用鏈。因而就能找到泄漏對象是經過怎樣的路徑與GCRoots相關聯並致使垃圾收集器沒法自動回收它們的。掌握了泄漏對象的類型信息,以及GC Roots引用鏈的信息,就能夠比較準確地定位出泄漏代碼的位置。
三、若是不存在內存泄漏,換句話說就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx 與-Xms) ,與機器物理內存對比看是否還能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。
ps:內存泄漏->指在堆中存有被過多被引用的無效內存,進而致使內存溢出
對每一個加載的類型( 類clas三、接口interface.枚舉enum、註解annotation),JVM必須在方法區中存儲如下類型信息:
這個類型的完整有效名稱(全名=包名.類名)
這個類型直接父類的完整有效名(對於interface或是java. lang .object, 都沒有父類)
這個類型的修飾符(public, abstract, final的某個子集)
這個類型直接接口的一個有序列表.
JVM必須在方法區中保存類型的全部域的相關信息以及域的聲明順序。
域的相關信息包括:
域名稱、域類型、域修飾符(public, private,protected, static, final, volatile, transient的某個子集)
JVM必須保存全部方法的一下信息,同域信息同樣包括聲明順序:
方法名稱.
方法的返回類型(或void)
方法參數的數量和類型(按順序)
方法的修飾符(public, private, protected, static, final,synchronized, native, abstract的- 一個子集)
方法的字節碼(bytecodes)、操做數棧、局部變量表及大小( abstract和native方法除外)
異常表( abstract和native方法除外)
每一個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、
被捕獲的異常類的常量池索引
常量池,能夠看作是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。
一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant Pool Table) ,包括各類字面量和對類型、域和方法的符號引用。
常量池的做用
一個java源文件中的類、接口,編譯後產生一個字節碼文件。而Java中的字節碼須要數據支持,一般這種數據會很大以致於不能直接存到字節碼裏,換另外一種方式,能夠存到常量池,這個字節碼包含了指向常量池的引用。在動態連接的時候會用到運行時常量池,以前有介紹。
public class SimpleClass{ public void test(){ System.out.println("helloword!"); } }
雖然只有194字節,可是裏面卻使用了String、System、 PrintStream及Object等結構。這裏代碼量其實已經很小了。若是代碼多,引用到的結構會更多! 這裏就須要常量池了!
常量池裏有什麼?
數量值、字符串值、類引用、字段引用、方法引用
運行時常量池( Runtime Constant Pool) 是方法區的一部分。
常量池表(Constant Pool Table) 是Class文件的一部分,用於存放編譯期生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。運行時常量池,在加載類和接口到虛擬機後,就會建立對應的運行時常量池。
JVM爲每一個已加載的類型(類或接口)都維護-一個常量池。池中的數據項像數組項-同樣,是經過索引訪問的。
運行時常量池中包含多種不一樣的常量,包括編譯期就已經明確的數值字面量,也包括到運行期解析後纔可以得到的方法或者字段引用。此時再也不是常量池中的符號地址了,這裏換爲真實地址。
運行時常量池,相對於Class文件常量池的另外一重要特徵是:具有動態性。
String. intern()運行時常量池相似於傳統編程語言中的符號表(symbol table) ,可是它所包含的數據卻比符號表要更加豐富一些。
當建立類或接口的運行時常量池時,若是構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。
爲何要替換永久代?
1.爲永久代設置空間大小是很難肯定的。
在某些場景下,若是動態加載類過多,容易產生Perm區的0OM。好比某個實際web.工程中,由於功能點比較多,在運行過程當中,要不斷動態加載不少類,常常出現致命錯誤。
而元空間和永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制。
2.對永久代進行調優是很困難的。
StringTable爲何要調整?
jdk7中將StringTable放到了堆空間中。由於永久代的回收效率很低,在full gc的時候纔會觸發。而full gc是老年代的空間不足、永久代不足時纔會觸發。這就致使String Table回收效率不高。而咱們開發中會有大量的字符串被建立,回收效率低,致使永久代內存不足。放到堆裏,能及時回收內存。
有些人認爲方法區(如HotSpot虛擬機中的元空間或者永久代)是沒有垃圾收集行爲的,其實否則。《Java虛擬機規範》對方法區的約束是很是寬鬆的,提到過能夠不要求虛擬機在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區類型卸載的收集器存在(如JDK11時期的zGC收集器就不支持類卸載)
通常來講這個區域的回收效果比較難使人滿意,尤爲是類型的卸載,條件至關苛刻。可是這部分區域的回收有時又確實是必要的。之前Sun公司的Bug列表中,曾出現過的若干個嚴重的Bug;就是因爲低版本的HotSpot虛擬機對此區域未徹底回收而致使內存泄漏。方法區的垃圾收集主要回收兩部份內容:常量池中廢棄的常量和再也不使用的類型。
方法區的垃圾收集主要回收兩部份內容:常量池中廢棄的常量和再也不使用的類型。
先來講說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明爲final的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
一、類和接口的全限定名
二、字段的名稱和描述符
三、方法的名稱和描述符HotSpot,虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就能夠被回收。
回收廢棄常量與回收Java堆中的對象很是相似。
斷定一個常量是否「廢棄」仍是相對簡單,而要斷定一個類型是否屬於「再也不被使用的類」的條件就比較苛刻了。須要同時知足下面三個條件:
該類全部的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
加載該類的類加載器已經被回收,這個條件除非是通過精心設計的可替換類加載器的場景,如OSGi、 JSP的重加載等,不然一般是很難達成的。
該類對應的java.1ang. Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。
Java虛擬機被容許對知足.上述三個條件的無用類進行回收,這裏說的僅僅是「被容許」,而並非和對象同樣,沒有引用了就必然會回收。關因而否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還能夠使用-verbose: class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看 類加載和卸載信息
在大量使用反射、動態代理、CGLib等字節碼框架,動態生成JSP以及oSGi這類頻繁自定義類加載器的場景中,一般都須要Java虛擬機具有類型卸載的能力,以保證不會對方法區形成過大的內存壓力。
對象實例化的過程
⑧加載類元信息
②爲對象分配內存
⑧處理併發問題-
④屬性的默認初始化(零值初始化)設置對象頭的信息
⑥屬性的顯式初始化、代碼塊中初始化、構造器中初始化
虛擬機遇到一條new指令 ,首先去檢查這個指令的參數能香在Metaspace的常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已經被加載、解析和初始化。( 即判斷類元信息是否存在)。若是沒有,那麼在雙親委派模式下,使用當前類加載器以ClassLoader+包名+類名爲Key進行查找對應的.Class文件。若是沒有找到文件,則拋出ClassNotFoundException異常,若是找到,則進行類加載,並生成對應的Class類對象
首先計算對象佔用空間大小,接着在堆中劃分一塊內存給新對象。若是實例成員變量是引用變量,僅分配引用變量空間便可,即4個字節大小。
若是內存規整
若是內存是規整的,那麼虛擬機將採用的是指針碰撞法( Burmp The Pointer )來爲對象分配內存。
意思是全部用過的內存在一邊,空閒的內存在另一邊,中間放着-一個指針做爲分界點的指示器,分配內存就僅僅是把指針向空閒那邊挪動一段與對象大小相等的距離罷了。若是垃圾收集器選擇的是Serial、ParNew這種基於壓縮算法的,虛擬機採用這種分配方式。通常使用帶有compact (整理)過程的收集器時,使用指針碰撞。
若是內存不規整
虛擬機須要維護一個列表
若是內存不是規整的,已使用的內存和未使用的內存相互交錯,那麼虛擬機將採用的是空閒列表法來爲對象分配內存。
意思是虛擬機維護了一個列表,記錄上哪些內存塊是可用的,再分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的內容。這種分配方式成爲「空閒列表( Free List ) "。
選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有
壓縮整理功能決定。
採用CAS失敗重試、區域加鎖保證更新的原子性
每一個線程預先分配一塊TLAB一一 經過-XX:+/-UseTLAB參數來設定
默認初始化
全部屬性設置默認值,保證對象實例字段在不賦值時能夠直接使用
將對象的所屬類(即類的元數據信息)、對象的HashCode和對象的GC信息、 鎖信息等數據存儲在對象的對象頭中。這個過程的具體設置方式取決於JVM實現。
類(方法區)->對象(java堆)->方法(java棧)
在Java程序的視角看來,初始化才正式開始。初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變量。
所以通常來講(由字節碼中是否跟隨有invokespecel指令所決定), new指令以後會接着就是執行方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底建立出來。
public class Customers{ publc void Customer(){ static{ name="匿名客戶"; } int id=1001; Acct acct = new acct(); } } ------------------------------------------------------------- @Setter @Getter public class Acct{ private int userCode; private int money; public acct(){ } } ------------------------------------------------------------------ public class test{ public void main(String[] args){ Customers customers = new Customers(); } }
JVM是如何經過棧幀中的對象引用訪問到其內部的對象實例的呢?
定位,經過棧上reference訪問
優勢:
reference中存儲穩定句柄地址,對象被移動(垃圾收集時移動對象很廣泛)時只會改變句柄中實例數據指針便可, reference自己不須要被修改。
缺點:
佔用較多內存,效率較低
優勢
節省空間,速度快,效率高
缺點
數據在移動時reference地址也要修改
不是虛擬機運行時數據區的一部分, 也不是《Java虛擬機規範》中定義的內存區域。
直接內存是在Java堆外的、直接向系統申請的內存區間。
來源於NIO,經過存在堆中的DirectByteBuffer操做Naltive內存
一般,訪問直接內存的速度會優於Java堆。 即讀寫性能高。
所以出於性能考慮,讀寫頻繁的場合可能會考慮使用直接內存。
Java的NIO庫容許Java程序使用直接內存,用於數據緩衝區
IO與NIO
IO:byte[ ] / char[ ] Stream
NIO:buffer Channel
public class test{ public void main(String[] args){ //直接分配本地內存空間 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER); System.out.println("直接內存分配完畢,請求指示! "); Scanner scanner = new Scanner(System. in); scanner.next(); system.out.println("直接內存開始釋放! "); byteBuffer = nu1l; System.gc(); scanner.next(); } }
讀寫文件
須要與磁盤交互須要由用戶態切換到內核態,在內核態時,須要內存如圖的操做。使用IO,見圖。這裏須要兩分內存存儲重複數據,效率低。
使用NIO時,如右圖。操做系統劃出的直接緩存區能夠被java代碼直接訪問,只有一份。NIO適合對大文件的讀寫操做。
也可能致使OutOfMemoryError異常
因爲直接內存在Java堆外,所以它的大小不會直接受限於-Xmx指定的最大堆大小,可是系統內存是有限的,Java堆和直接內存的總和依然受限於操做系統能給出的最大內存。
缺點
分配回收成本較高
不受JVM內存回收管理直接內存大小能夠經過MaxDirectMemorySize設置,若是不指定,默認與堆的最大值-Xmx參數值一致
執行引擎是Java虛擬機核心的組成部分之一。
「虛擬機」是一個相對於「物理機」的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接創建在處理器、緩存、指令集和操做系統層面上的,而虛擬機的執行引擎則是由軟件自行實現的,所以能夠不受物理條件制約地定製指令集與執行引擎的結構體系,可以執行那些不被硬件直接支持的指令集格式。
JVM的主要任務是負責裝載字節碼到其內部,但字節碼並不可以直接運行在操做系統之,上,由於字節碼指令並不是等價於本地機器指令,它內部包含的僅僅只是一些可以被JVM所識別的字節碼指令、符號表,以及其餘輔助信息。
那麼,若是想要讓一個Java程序運行起來,執行引擎(Execution Engine)的任務就是將字節碼指令解釋/編譯爲對應平臺上的本地機器指令才能夠。簡單來講,JVM中的執行引擎充當了將高級語言翻譯爲機器語言的譯者。
執行 引擎在執行的過程當中究竟須要執行什麼樣的字節碼指令徹底依賴於PC寄存器。
每當執行完一項指令操做後,PC寄存器就會更新下一.條須要被執行的指令地址。
固然方法在執行的過程當中,執行引擎有可能會經過存儲在局部變量表中的對象引用準肯定位到存儲在Java堆區中的對象實例信息,以及經過對象頭中的元數據指針定位到目標對象的類型信息。
爲何說java是半解釋型半半編譯型語言?
其實就是由於java的執行引擎既有解釋器又有編譯器。
JVM設計者們的初衷僅僅只是單純地爲了知足Java程序實現跨平臺特性,所以避免採用靜態編譯的方式直接生成本地機器指令,從而誕生了實現解釋器在運行時採用逐行解釋字節碼執行程序的想法。
解釋器真正意義上所承擔的角色就是一一個運行時「翻譯者」,將字節碼文件中的內容「翻譯」爲對應平臺的本地機器指令執行。
當一條字節碼指令被解釋執行完成後,接着再根據PC寄存器中記錄的下一條須要被執行的字節碼指令執行解釋操做。
字節碼解釋器在執行時經過純軟件代碼模擬字節碼的執行,效率很是低下。
而模板解釋器將每--條字節碼和一個模板函數相關聯,模板函數中直接產生這
條字節碼執行時的機器碼,從而很大程度上提升瞭解釋器的性能。在HotSpot VM中,解釋器主要由Interpreter模塊 和Code模塊構成。
Interpreter模塊:實現瞭解釋器的核心功能
Code模塊:用於管理HotSpot VM在運行時生成的本地機器指令
因爲解釋器在設計和實現上很是簡單,所以除了Java語言以外,還有許多高級語言一樣也是基於解釋器執行的,好比Python、 Perl、 Ruby等。可是在今天,基於解釋器執行已經淪落爲低效的代名詞,而且時常被一些C/C++程序員所調侃。
爲了解決這個問題,JVM平臺支持--種叫做即時編譯的技術。即時編譯的目的是避免函數被解釋執行,而是將整個函數體編譯成爲機器碼,每次函數執行時,只執行編譯後的機器碼便可,這種方式能夠使執行效率大幅度提高。
不過不管如何,基於解釋器的執行模式仍然爲中間語言的發展作出了不可磨滅的貢獻。
第一種是將源代碼編譯成字節碼文件,而後在運行時經過解釋器將字節碼文件轉爲機器碼執行
第二種是編譯執行(直接編譯成機器碼)。現代虛擬機爲了提升執行效率,會使用即時編譯技術(JIT, Just In Time) 將方法編譯成機器碼後再執行
HotSpotVM是目前市面上高性能虛擬機的表明做之一。它採用解釋器與即時編譯器並存的架構。在Java虛擬機運行時候解釋器和即時編譯器可以相互協做,各自取長補短,盡力去選擇最合適的方式來權衡編譯本地代碼的時間和直接解釋執行代碼的時間。
在今天,Java程序的運行性能早已脫胎換骨,已經達到了能夠和C/C++程序一較高下的地步,
有些開發人員會感受到詫異,既然HotSpot VM中已經內置JIT編譯器了,那麼爲何還須要再使用解釋器來「拖累」程序的執行性能呢?好比JRockit VM內部就不包含解釋器字節碼所有都依靠即時編譯器編譯後執行。
首先明確:
當程序啓動後,解釋器能夠立刻發揮做用,省去編譯的時間,當即執行。編譯器要想發揮做用,把代碼編譯成本地代碼,須要必定的執行時間。但編譯爲本地代碼後,執行效率高。
因此:
儘管JRockit VM中程序的執行性能會很是高效,但程序在啓動時必然須要花費更長的時間來進行編譯。對於服務端應用來講,啓動時間並不是是關注重點,但對於那些看中啓動時間的應用場景而言,或許就須要採用解釋器與即時編譯器並存的架構來換取一-個平衡點。在此模式下,當Java虛擬器啓動時,解釋器能夠首先發揮做用,而沒必要等待即時編譯器所有編譯完成後再執行,這樣能夠省去許多沒必要要的編譯時間。隨着時間的推移,編譯器發揮做用,把愈來愈多的代碼編譯成本地代碼,得到更高的執行效率。
同時,解釋執行在編譯器進行激進優化不成立的時候,做爲編譯器的「逃生門」。
當虛擬機啓動的時候,解釋器能夠首先發揮做用,而沒必要等待即時編譯器所有編譯完成再執行,這樣能夠省去許多沒必要要的編譯時間。而且隨着程序運行時間的推移,即時編譯器逐漸發揮做用,根據熱點探測功能,將有價值的字節碼編譯爲本地機器指令,以換取更高的程序執行效率。
注意解釋執行與編譯執行在線上環境微妙的辯證關係。機器在熱機狀態能夠承受的負載要大於冷機狀態。若是以熱機狀態時的流量進行切流,可能使處於冷機狀態的服務器因沒法承載流量而假死。
在生產環境發佈過程當中,以分批的方式進行發佈,根據機器數量劃分紅多個批次,每一個批次的機器數至多佔到整個集羣的1/8.曾經有這樣的故障案例:某程序員在發佈平臺進行分批發布,在輸入發佈總批數時,誤填寫成分爲兩批發布。若是是熱機狀態,在正常狀況下一半的機器能夠勉強承載流量,但因爲剛啓動的JVM均是解釋執行,尚未進行熱點代碼統計和JIT動態編譯,致使機器啓動以後,當前1/2發佈成功的服務器立刻所有宕機,此故障說明了JIT的存在。-阿里團隊
一個被屢次調用的方法,或者是一一個方法體內部循環次數較多的循環體均可以被稱之爲「熱點代碼」,所以均可以經過JIT編譯器編譯爲本地機器指令。因爲這種編譯方式發生在方法的執行過程當中,所以也被稱之爲棧上替換,或簡稱爲OSR (On Stack Replacement)編譯。
一個方法究竟要被調用多少次,或者-一個循環體究竟須要執行多少次循環才能夠達到這個標準?必然須要一個明確的閾值,JIT編譯器纔會將這些「熱點代碼」編譯爲本地機器指令執行。這裏主要依靠熱點探測功能。
目前HotSpot VM所採用的熱點探測方式是基於計數器的熱點探測。採用基於計數器的熱點探測,HotSpot VM將會爲每個方 法都創建2個不一樣類型的計數器,分別爲方法調用計數器(Invocation Counter) 和回邊計數器(Back Edge Counter)。
方法調用計數器用於統計方法的調用次數
回邊計數器則用於統計循環體執行的循環次數
這個計數器就用於統計方法被調用的次數,它的默認閾值在Client模式下是1500次,在Server 模式下是10000 次。超過這個閾值,就會觸發JIT編譯。
這個閥值能夠經過虛擬機參數- XX: CompileThreshold來人爲設定。
當一個方法被調用時,會先檢查該方法是否存在被JIT 編譯過的版本,若是存在,則優先使用編譯後的本地代碼來執行。若是不存在已被編譯過的版本,則將此方法的調用計數器值加1, 而後判斷方法調用計數器與回邊計數器值之和是否超過方法調用計數器的閾值。若是已超過閾值,那麼將會向即時編譯器提交一個該方法的代碼編譯請求。
若是不作任何設置,方法調用計數器統計的並非方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間以內方法被調用的次數。當超過必定的時間限度, 若是方法的調用次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會被減小一半,這個過程稱爲方法調用計數器熱度的衰減(Counter Decay) ,而這段時間就稱爲此方法統計的半衰週期(Counter Half Life Time)
進行熱度衰減的動做是在虛擬機進行垃圾收集時順便進行的,能夠使用虛擬機參數-XX:-UseCounterDecay來關閉熱度衰減,讓方法計數器統計方法調用的絕對次數,這樣,只要系統運行時間足夠長,絕大部分方法都會被編譯成本地代碼。
另外,能夠使用-XX:CounterHalfLifeTime 參數設置半衰週期的時間,單位是秒。
它的做用是統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向後跳轉的指令稱爲「回邊」 (Back Edge)。顯然,創建回邊計數器統計的目的就是爲了觸發OSR編譯。
缺省狀況下HotSpot VM是採用解釋器與即時編譯器並存的架構,固然開發人員能夠根據具體的應用場景,經過命令顯式地爲Java虛擬機指定在運行時究竟是徹底採用解釋器執行,仍是徹底採用即時編譯器執行。以下所示:
-Xint: 徹底採用解釋器模式執行程序;(cmd中:java -Xint -verson 或者VM options中設置)
-Xcomp: 徹底採用即時編譯器模式執行程序。若是即時編譯出現問題,解釋器會介入執行。
-Xmixed: 採用解釋器+即時編譯器的混合模式共同執行程序。
在HotSpot VM中內嵌有兩個JIT編譯器,分別爲Client Compiler和ServerCompiler,但大多數狀況下咱們簡稱爲C1編譯器和C2編譯器。開發人員能夠經過以下命令顯式指定Java虛擬機在運行時到底使用哪種即時編譯器,在64位系統中默認使用c2編譯器,以下所示:
client:指定Java虛擬機運行在Client模式下,並使用C1編譯器;
C1編譯器會對字節碼進行簡單和可靠的優化,耗時短。以達到更快的編譯速度。
server:指定Java,虛擬機運行在Server模式下,並使用C2編譯器。
C2進行耗時較長的優化,以及激進優化。但優化的代碼執行效率更高。
C1和C2編譯器不一樣的優化策略:
在不一樣的編譯器上有不一樣的優化策略,C1編譯器上主要有方法內聯,去虛擬化、冗餘消除。
方法內聯:將引用的函數代碼編譯到引用點處,這樣能夠減小棧幀的生成,減小參數傳遞以及跳轉過程
去虛擬化:對惟一的實現類進行內聯
冗餘消除:在運行期間把一些不會 執行的代碼摺疊掉
C2的優化主要是在全局層面,逃逸分析是優化的基礎。基於逃逸分析在C2.上有以下幾種優化:
標量替換:用標量值代替聚合對象的屬性值
棧上分配:對於未逃逸的對象分配對象在棧而不是堆
同步消除:清除同步操做,一般指synchronized
分層編譯(Tiered Compilation) 策略:程序解釋執行(不開啓性能監控)能夠觸發C1編譯,將字節碼編譯成機器碼,能夠進行簡單優化,也能夠加上性能監控,C2編譯會根據性能監控信息進行激進優化。不過在Java7版本以後,一旦開發人員在程序中顯式指定命令「-server」時,默認將會開啓分層編譯策略,由C1編譯器和C2編譯器相互協做共同來執行編譯任務。
自JDK10起,HotSpot又 加入-一個全新的即時編譯器: Graal編譯 器編譯效果短短几年時間就追評了C2編譯器。將來可期。目前,帶着「實驗狀態"標籤,須要使用開關參數-XX: +UnlockExper imentalVMOptions -XX: +UseJVMCICompiler去激活,才能夠使用。
String:字符串,使用一對""引發來表示。
String s1 = "hello";//字面量的定義方式
String s2 = new String ("hello") ;
String聲 明爲final的,不可被繼承String實現了Serializable接口:表示字符串是支持序列化的。
實現了Comparable接口:表示String能夠比較大小
String在jdk8及之前內部定義了final char[ ] value用於存儲字符串數據。jdk9時改成byte[ ]
String:表明不可變的字符序列。簡稱:不可變性。
當對字符串從新賦值時,須要重寫指定內存區域賦值,不能使用原有的value進行賦值。
當對現有的字符串進行鏈接操做時,也須要從新指定內存區域賦值,不能使用原有的value進行賦值。
當調用String的replace ()方法修改指定字符或字符串時,也須要從新指定內存區域賦值,不能使用原有的value進行賦值。經過字面量的方式(區別於new)給-一個字符串賦值,此時的字符串值聲明在字符串常量池中。
字符串常量池中是不會存儲相同內容的字符串的。
String的String Pool1是一 個固定大小的Hashtable,默認值大小長度是1009。若是放進String Poo1的String很是多, 就會形成Hash衝突嚴重,從而致使鏈表會很長,而鏈表長了後直接會形成的影響就是當調用string. intern時性能會大幅降低。
使用-XX:StringTableSi ze可設置StringTable的長度
在jdk6中stringTable是固定的,就是1009的長度,因此若是常量池中的字符串過多就會致使效率降低很快。StringTableSize 設置沒有要求
在jdk7中,StringTable的長度默認值是60013
在jdk8中,StringTable1009是可設置的最小值。
在Java語言中有8種基本數據類型和一種比較特殊的類型String。這些類型爲了使它們在運行過程當中速度更快、更節省內存,都提供了-種常量池的概念。
常量池就相似一個Java系統級別提供的緩存。8種基本數據類型的常量池都是系統協調的,String類 型的常量池比較特殊。它的主要使用方法有兩種。
直接使用雙引號聲明出來的String對象會直接存儲在常量池中。
好比: String info = "atguigu. com";
若是不是用雙引號聲明的String對象,能夠使用String提供的intern()方法。這個後面重點談
在jdk7以前,字符串常量池是在永久代中的,jdk8以後都將其存放在堆空間
StringTable爲何要調整?
permSize默認比較小
永久代垃圾回收頻率低
常量與常量的拼接結果在常量池,原理是編譯期優化
常量池中不會存在相同內容的常量。
只要其中有一一個是變量,結果就在堆中(非字符串常量池)。變量拼接的原理是str ingBui lder
若是拼接的結果調用intern()方法,則主動將常量池中尚未的字符串對象放入池中,並返回此對象地址。
以下的s1 + s2的執行細節: (變量s是臨時定義的)
①StringBuilder s = new StringBuilder();
②s.append("a");
③s.append("b") ;
④s.tostring() --> 約等於new string("ab");
補充:在jdk5. 0以後使用的是stringBuilder,在jdk5. 0以前使用的是StringBuffer
字符串拼接操做不必定使用的是stringBuilder,若是拼接符號左右兩邊都是字符串常量或常量引用,則仍然使用編譯期優化,即非stringBuilder的方式。
改進的空間:在實際開發中,若是基本肯定要前先後後添加的字符串長度不高於某個限定值highLevel的狀況下,建議使用StringBuilder的構造器:
StringBuilder s = new stringBuilder(highLevel);//new char[highLevel]
若是不是用雙引號聲明的String對象,能夠使用str ing提供的intern方法: intern方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。
好比: String myInfo = new String("I love atguigu") .intern() ;
也就是說,若是在任意字符串上調用String. intern方法,那麼其返回結果所指向的那個類實例,必須和直接以常量形式出現的字符串實例徹底相同。所以,下列表達式的值一定是true:
("a" + "b" + "c") . intern() == "abc"
通俗點講,Interned string就 是確保字符串在內存裏只有一'份拷貝,這樣能夠節約內存空間,加快字符串操做任務的執行速度。注意,這個值會被存放在字符串內部池 (String Intern Pool)。
new String( "ab")會建立幾個對象?
看字節碼,就知道是兩個。
一個對象是: new關鍵字在堆空間建立的
另外一個對象是:字符串常量池中的對象。
字節碼指令:ldc
new String("a")+new Sting("b")會建立幾個對象?
0 new #2 <java/lang/StringBuilder>//第一個對象,new StringBulder() 3 dup 4 invokespecial #3 <java/lang/StringBuilder.<init>> 7 new #4 <java/lang/String>//第二個對象,new String("a") 10 dup 11 ldc #5 <a>//第三個對象 字符串常量池中新建的"a" 13 invokespecial #6 <java/lang/String.<init>> 16 invokevirtual #7 <java/lang/StringBuilder.append> 19 new #4 <java/lang/String>//第四個對象,new String("b") 22 dup 23 ldc #8 <b>//第五個對象,字符串常量池中的"b" 25 invokespecial #6 <java/lang/String.<init>> 28 invokevirtual #7 <java/lang/StringBuilder.append> 31 invokevirtual #9 <java/lang/StringBuilder.toString>//深刻剖析toString,返回一個new String,第六個對象.注意,字節碼中沒有使用ldc,因此字符串常量池中沒有"ab" 34 astore_1 35 return
關於intern()的難題
public void mian(String args[]){ String s1 = new String("1"); s.intern(); String s2 = "1" System.out.println(s1==s2);//false String s3 = new String("1")+new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3==s4);//jdk6中爲false,jdk7之後爲true } 詳見視頻P127&P128
總結String的intern()的使用:
jdk1.6中,將這個字符串對象嘗試放入串池。
若是串池中有,則並不會放入。返回已有的串池中的對象的地址
若是沒有,會把此對象複製一份,放入串池,並返回串池中的對象地址Jdk1.7起,將這個字符串對象嘗試放入串池。
若是串池中有,則並不會放入。返回已有的串池中的對象的地址
若是沒有,則會把對象的引用地址複製一份,放入串池,並返回串池中的引用地址
PS:new String()在字符串常量池中存放的是實體,不是new String()地址的引用,因此new String對象的地址與其在常量池中存放的地址不相等;intern在字符串常量池中存放的是其堆地址的引用。