爲什麼要有JVM?
JVM就是Java運行虛擬機,那麼虛擬機又分爲系統虛擬機和程序虛擬機,而JVM是屬於程序虛擬機,所以不要看到是虛擬機就誤認爲JVM是系統虛擬機。
JVM是幫助Java程序開發者在開發過程中無需考慮無用的資源需要進行回收,避免內存溢出等問題且實現在不同平臺上運行Java程序。
如: 開餐館,你每天要把店鋪的垃圾拉到垃圾廠去,如果你不拉或忘記拉,越積越多垃圾會堆滿你的店鋪,甚至還會堆到別的店鋪去,不止你自己的店鋪無法營業,別人的店鋪也無法營業,隨着時間的累積,整條街的店鋪都給垃圾堆滿,都無法營業。
在看到這種情況房東就有意見了,房東說:”你們每個月都給我多交一些錢,我解決垃圾這個問題。(JVM運行也要資源)”
房東會在街道上擺放上大的垃圾桶,定時的檢查,如果垃圾桶滿了,先拉到一個集中的地方,等這個區域的垃圾慢慢的差不多滿了,就把這些垃圾拉到垃圾廠進行處理。
當這個時候我覺得房租太貴了,我搬去別的地方,那麼又是一個新的房東,這個房東對垃圾處理這個問題就不一樣了,他可能要求你必須要買我的袋子裝着的,我纔會去處理這個垃圾。
相關的管理者看到這個情況,馬上說:“接下來,垃圾的這個問題,由我們安排的人來統一處理,以後不管你搬去哪裏,只要你到我們的官網上填一份表格就行了。(運行環境)”
如果沒有JVM,可以腦補一下。
JVM是什麼?
在弄清楚JVM是什麼之前,先弄清楚JDK、JRE是什麼?
JDK就是開發的工具包,包括了JRE。
JRE是Java運行環境,包括了JVM和Java核心類庫
JVM就是Java程序運行平臺,擁有自己的指令集,抽象操作系統和CUP結構、內存結構,在運行時操作不同的內存區域。
Java程序編譯後的文件是*.class文件,*.class文件是按照Java標準編譯的文件,JVM是實現了Java制定的標準,因此JVM是可以運行Java程序的,而JVM是一個虛擬出來的機器,通過自定義的執行引擎、接口等實現方式與實際機器各種交互,使得Java程序在運行過程與實際機器無耦合,從而實現跨平臺。
如: 以上的例子,相關管理者只管理各自的區域垃圾問題,各自的管理者都使用不同的顏色,導致每次只要有分店在別的地點開張,那麼這個分店就要換垃圾袋顏色,否則管理者不承認這個垃圾是他管的。
生產垃圾袋的廠家覺得這樣也不好,回收回來的袋子還要做分類,不利於他回收袋子,於是和各個店家商量都只用一種顏色的袋子,廠家也只生產這個顏色的袋子,不管管理者,廠家願意這樣做,店家也願意用。
於是生產廠家就只生產一種顏色的袋子,一種袋子到處通用(Compile Once,Run Everywhere.)。
作用
給Java程序提供一個獨立的運行環境
特點
無需依賴於任何系統、平臺之上。
優點
跨平臺
可擴展性強
……
按照無需依賴任何系統,獨立運行環境大家可以試下這個思路去分析,這裏就不寫那麼多了。
注:每個人的學習方式不一樣,以上只是提供一個思路而已。
缺點
JVM是一個程序虛擬機,但始終還是要運行在操作系統之上的,初始化的時候需要與操作系統建立各種交互,導致啓動時間長,與操作系統交互導致資源的消耗……
相當於一個蘋果放在一個盤子上,盤子放了一些水,而蘋果在放進去的時候,盤子的水會高漲,如果超過盤子就會溢出,所以有一個盤子的要求,要麼就對放入的蘋果大小,質量進行控制,以達到要求,另外蘋果的靈活性也無法自由的變化形狀,所以放入佔用了一定的位置導致能放的東西越來越少,盤子放入更多的東西的時候會越來越擠……
至於其他的缺點,大家可以試下這個思路去做分析。
注:每個人的學習方式不一樣,以上只是提供一個思路而已。
主流JVM
名稱 |
研發者 |
特點 |
Hotspot |
Longview Technologies開發,然後被sun收購 |
性能出色,複雜度有點高 |
JRockit |
BEA公司,被oracle收購 |
任務控制能力出色,合併到Hotspot |
J9 |
IBM研發 |
IBM內部使用,往往需要和IBM套件共同使用 |
Harmony |
IBM和Intel研發的,捐給Apache作爲孵化項目 |
Apache退出了JCP之後,慢慢的就沒有什麼商用了 |
注:接下來講的是Hotspot虛擬機,但虛擬機基本差不多,但JRockit是沒有解釋器的,這些區別自己去了解。
探索JVM內部
編譯器
爲什麼要有編譯器?
那麼我們先來假設下沒有編譯器的情況吧
如果沒有編譯器,我們現在編寫一個程序,這個程序是在windows上編寫的,開發人員的本地測試也是在windows進行測試的,但環境部署上去的機器是Liunx時,這個時候兩個操作系統的機制以及執行的字節碼可能不一樣,如( / )在Liunx和windows的表示都是不一樣的,所以這種差別是使得開發人員很痛苦,難道每一次部署的系統環境不一樣時或開發的系統環境不一樣的時候就要寫不同的代碼嗎?
而編譯器的作爲就是將開發人員寫的代碼編譯成爲一份是由JVM專門識別的一個字節碼,直接由JVM進行運行,不在與操作系統有關。
是什麼?
將開發人員寫的*.java的源代碼編譯成字節碼(*.class),這種字節碼也可以叫做JVM的機器語言
執行過程
符號表:就是由符號地址和符號的信息所組成的表格,符號表其實就是記錄編譯的時候讀取的信息。
詞法分析:源代碼的字符流,轉換爲標記的集合(字段標記,方法的標記等),並檢查詞法是否是正確的。
語法分析:是將詞法分析後的這個標記的集合轉換爲一個樹狀結構的表現形式,並檢查語法等是否是正確的。
註解處理:就是處理語法分析之後的這個樹狀結構的內容,註解處理時是可以對內容進行增刪改查的,如果對這個語法分析後的樹狀結構數據進行更改了,那麼編譯器將回到解析和填充符號表的過程中重新處理。比如:標識這個值是a和b的變量相加得來的,大家去看看*.class文件的內容就知道了
語義分析:對語法分析之後的這個樹狀結構進行讀取,並且對其上下文的聯繫是否合理進行上下文的分析,類型是否匹配、方法是否有返回值、將判斷泛型等編譯成簡單的語法結構等……
字節碼生成:將各個步驟的所產生的信息及存儲在符號表內的信息進行轉換爲字節碼,寫出爲*.class文件。
如:現在商家想要得到垃圾袋,要去管理者那申請,先填寫申請單,管理者要製作這些申請單,管理者會先記下大概要填寫的幾個模塊(詞法分析),在各個模塊中將要填寫的內容寫上去,在形成一個樹狀化的展示形式申請單(基本信息– 名字)(語法分析),對一些要填寫的地方進行註解(註解處理),這個時候申請單就做好了,給到各個商家,商家填寫完成後,要檢查商家填寫的內容是否有錯誤(語義分析),最後沒有問題了,那麼根據申請單信息進行審覈,審覈通過了則給袋子給商家。
*.class文件內容:
結構信息:文件版本號、各個部分數量、大小等信息。
元數據:常量的信息、繼承的類、實現的接口、聲明的信息、常量池等信息。
方法信息:語句和表達式對應信息,字節碼、異常處理表、求值棧和局部變量的大小,求值棧的類型記錄等信息
類裝載系統
爲什麼要有類裝載機制?
類加載系統-圖一
類加載系統-圖二
大家看下類加載系統-圖一和類加載系統-圖二,作者建立了一個類,這個類是和Java自身提供的java.lang.String是一樣的包名和類名。
可以想象一下如果這個類成功執行了,那麼接下來如果有別的類在引用的時候,應該先引用作者寫的這個,還是引用Java自身的java.lang.String類?
如:以上的例子大家都知道,垃圾袋的顏色已經統一成一模一樣了,有一天A商家在門口放了兩袋垃圾袋,一袋是裝着打碎的瓷盤碎片,一袋是裝着廚房的垃圾,都綁着。
馬上就要下班了,員工準備拿垃圾袋去扔,由於垃圾袋綁着,這位員工趕着去約會,直接往垃圾袋隨時一抓,好了,結果大家猜到了,於是在第二天員工聰明瞭向商家要求,由於垃圾袋顏色一樣,要求垃圾袋貼上小紙條,標明哪個垃圾袋是裝什麼的,這樣做的之後,提高了安全性,而且又能夠保證在做垃圾分類的時候可以保證各個垃圾袋放的東西是按照垃圾分類的要求放的。
是什麼?
類裝載系統是由數個加載器組成的,負責將class文件信息加載進內存,存放在數據區-方法區內。
執行過程
加載:通過完全限定名查找到這個類的字節碼文件,將其靜態的存儲結構轉化虛擬機的方法區運行時數據結構,並生成一個代表這個類的對象,這個就是爲什麼可以進行反射操作。
類加載過程:
爲什麼要這樣加載?
當類在加載的時候,如類加載系統-圖一,定義一個java.lang.String是一樣的包名和類名,而類加載的時候是通過限定名去查找這個類字節碼文件的,那樣就出現了相同的內容,那麼則出現了衝突,破壞了Java內部的完整性以及一致性。
在類加載系統-圖二中我們可以看到,類加載器是有父類的,所以在↑查找的時候,是查找類是否已經在啓動等過程中,已啓動的加載器加載了,如果查找到的所有加載器都沒有加載,那麼則向下查找,哪個加載器是可以加載到這個類的,而這個也叫做雙親委派機制,而Tomcat、Jboss都依照Java規範有着實現了加載器。
雙親委派機制:可以理解爲不止是坑爹的,且還是坑到爺一代的,在接到一個類加載的請求的時候,會先問他爹加載了沒有,他爹會問他爺加載了沒有,他爺也沒有加載,那麼就會給回去,最後就只好自己加載了,也就是隻有父類無法完成的任務才自己完成。
驗證:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證,目的在於確保字節碼文件內的信息符合虛擬機的要求並不會破壞到虛擬機的內容(虛擬機的一致性,完整性)。
準備:爲類變量(靜態變量)分配對應的內存,並設置這些類變量的值,這些內存是分配方法區的內存,但設置的這些類變量的值,具體要看是否有final修飾符,如果沒有那麼則無論值是多少都是爲0,如果修飾符有final,那麼設置的這個值則就是類變量的值。
解析:將符號引用轉化爲直接引用,符號引用就是通過對應的符號找到目標對象,符號可以是字面量,符號引用和虛擬機內存的佈局是無關的,因符號引用的對象可以不加載到內存裏,直接引用就是存在於內存中的,是有指針可以指向到的。
虛擬機沒有規定解析的時間,只需要在anew arry、check cast、get field、instance of、invoke interface、invoke special、invoke static、invoke virtual、multi anew array、new、put field和put static這13個用於操作符號引用指令執行之前,對符號引用進行解析。
所以虛擬機會判斷是在類被加載器加載的時候對符號引用進行解析還是等符號引用在要被使用前去解析,可以通過看上面的13個指令去得到答案。
解析的東西主要是類、接口、字段、類方法、接口方法這五類進行解析。
無非就是不管你這個類是接口還是實現類,還是什麼,只要你是個類,那麼就解析你裏面的所有內容。
初始化:類初始化的時候就是觸發到了new、getstatic、putstatic或invokestatic這4條指令的時候,也就是通常在開發過程中new對象的時候,讀取或設置一個的靜態字段,以及調用靜態方法,被final修飾與已被編譯器把結果放入常量池的靜態字段除外。
初始化一個類的時候其父類還沒初始化,也會觸發其父類初始化。
但JVM最先初始化的是,main()方法這個類。
如:現在衛生局要檢查衛生了,衛生局通過信息文檔的省、市、區、詳細地址這個信息知道了接下來要檢查衛生的店在哪裏,檢查的結果是要寫在對應的檢查結果文檔上的,所以要將這個信息文檔上的信息(地址、營業執照等)轉化爲檢查結果文檔的格式,並生成一個這個要檢查的店的專門一個檢查結果的基本文檔,接下來要先在系統上記錄什麼時候去檢查,驗證這些信息有沒有輸入錯誤,接下來要根據這家店的情況,準備下檢查的固定事項(final),以及一些可能臨時的事項(類變量),檢查人員要準備出發了,要先把檢查結果文檔下載下來,打印成文件出來便於做記錄,接下來檢查人員到了店面進行檢查,檢查完了之後將記錄下來的內容結果上傳到對應的系統上。
解釋器
爲什麼要有?
我們都知道JVM是爲了實現跨平臺,寫一次到處跑的實現理念,但不同的機器可能因爲生產廠家或操作系統等原因有着不一樣的標準,那麼其機器底層執行的指令等可能各有區別。
是什麼?
將要執行的字節碼轉換爲機器碼,而這個解釋是一句一句的解釋,這個也說明了Java是解釋性語言。
即時編譯器(JIT)
爲什麼要有?
剛剛我們說到了解釋器,其實JIT和解釋器做的事情是一樣的,但如果每次都要進行一句句的解釋,那麼效率太低。
是什麼?
JIT和解釋器做的事情是一樣的,都是爲了將要執行字節碼轉換爲機器碼,而不一樣的是,JIT類似在編程的過程中將經常使用的數據放到緩存中,所以JIT會把經常使用的字節碼,如:循環等高頻率使用方法,它是以方法爲單位一次性將整個方法的字節碼編譯爲機器碼。
而對於一個方法是否是經常使用,會通過探測熱點的方式。
既然是探測熱點的方式,這裏提一個最基本的思路,用一個計數器,但達到相應的閾值的時候就判定是熱點代碼,但是維護比較麻煩,接下來我們會提到內存哪一塊是線程獨有,哪一塊是線程共享,這裏就會存在問題,技術有優點也有缺點。
執行引擎
爲什麼要有?
當編譯器轉換爲機器碼了之後總要有東西去告知底層操作系統或某個操作者,接下來要做什麼。
是什麼?
執行引擎主要還是告知底層操作要做的事情,只是一個概念上的詞,上面說到的編譯器也可以理解爲是有一個編譯執行引擎,所以這個只是概念上的東西。
本地接口
爲什麼要有?
這其中是有歷史原因吧,因爲Java在問世的時候,C語言的程序是主流的,那麼多程序是使用C語言,那麼Java必然不可避免的要與C語言的程序進行交互,且Java是無法對操作系統底層進行訪問和操作的,但是可以通過本地接口調用其他語言的實現實現對底層進行訪問的操作。
是什麼?
就是爲了融合不同的變成語言的程序爲Java語言的程序所用,所以在在內存中開闢了一塊專門的處理標記是native的代碼,。
目前這種方法的使用越來越少了,除非是直接和硬件交互的,因爲現在基本通過Socket等通信方式實現程序直接的交互。
垃圾回收系統
爲什麼要有?
程序的運行的過程中,有一些是隻運行一次或數次之後就不再運行了,而隨着運行的時間增長,在系統中堆積的越來越多,最終超過系統的極限程序就停止了運行了。
站在用戶的使用角度來看,用一下就不能用,或進行一些數據運行的時候就忽然不能用了,作者相信這個是沒有用戶是可以容忍的。
是什麼?
將在一段時間不再使用,或在系統內部不再活躍的時候,則在系統中釋放掉這部分的信息。
運行時數據區
指令區:是線程獨有的
虛擬機棧:也可以叫棧內存,是在線程創建的時候創建,也就說明了,一個線程是有一個獨有的棧,虛擬機棧的生命週期是隨着線程的結束而釋放內存,對於棧而言不存在垃圾回收的問題,只要線程結束,那麼生命週期和線程是一致的,是棧會存儲基本類型變量、實例方法、引用類型變量,都是在棧內存中分配,就是線程執行獨有的方法的時候,會將方法區的對應的對象,類信息copy需要的部分信息到棧內存中,執行每一個方法的時候可以理解爲是一個棧幀,具體看個人怎麼去理解JVM棧內存,棧是遵循一個LIFO的一個原則,而棧一般是由三個部分組成,局部變量表,棧數據區,操作數棧。
局部變量表:存儲報錯的行數和方法使用到的局部變量等
操作數棧:保存計算過程中的結果,且作爲計算過程中的變量臨時存儲空間。
棧數據區:除了局部變量和操作數棧,棧還需要一些數據來支持常量池的解析,這裏的棧數據區就是保存常量池的指針、方法返回地址等,另外發送異常和處理異常代碼等,所以棧數據區還有一個異常處理表。
棧執行過程:當一個線程在執行某個方法的時候,就是在棧內運行的,而棧是遵循LIFO的原則,那麼就可以解釋當A方法調用B方法的時候,只有B方法執行完了,那麼A方法纔會繼續向下執行,那麼在調用的時候是先調用A方法,那麼A方法是先進棧,而B是後進棧的,B方法執行完了,彈出棧,繼續A方法,A方法執行完了,那麼則出棧。
本地方法棧:用於本地方法調用,允許java調用本地方法,具體可以看本地接口上述,本地接口如同一個大的存儲每一個對象,而本地方法棧就是存儲這個對象要執行的方法信息。
程序計數器/PC寄存器:指向方法區中的方法字節碼(用於存儲指向下一條指令的地址,也就是要指向的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,如果執行的方法是本地方法,寄存器值爲undefined,如果不是那麼寄存器會存放當前環境指針、程序計數器、操作棧指針、計算的變量指針等信息。
數據區:是所有線程共享的
方法區:保存類的元結構信息、運行時常量池、靜態變量、常量、字節碼、在類/實例/接口初始化用到的特殊方法等,方法區是可以調節大小的,且方法區是可以會進行垃圾回收的,所以可以理解方法區是一塊邏輯區域。
堆:存儲Java對象和數組等,但這個對象在堆中的首地址會在棧存儲,堆內存的大小是可以調節的。
堆可以說是Java一塊比較特殊的區域,因所有的Java對象實例都存儲在這個地方,那麼Java實例多了,則需要回收,那麼也不可能每一次使用完這個實例之後就把這個實例在內存馬上銷燬,如果過多幾秒時候又使用到了呢?
所以針對這種情況,Java堆的設計就有點特殊了。
堆 = 新生代 + 老年代 + 永久代(特殊)
新生代 = Eden + sv0+ sv1
新生代:主要存儲一個新的對象,通過不同的垃圾回收策略進行計算什麼時候進入老年代,什麼時候進行回收,具體看垃圾回收機制。
TLAB(Thread-local allocation buffer)區:是一個線程獨有的區域,
是爲了加速內存分配而存在的,也就是線程獨享的緩衝區,避免多線程問題,沒有鎖的問題,也就沒有了鎖對資源的開銷,提高對象分配使用,但這個區域是隻存儲小對象的,無法進入TLAB區的對象會直接進入堆,而TLAB區對對象是否進入的條件是按照對象的大小是否小於整個空間的64/1,這是一個默認的比例值,這個比例值是可以調整的。
老年代:存儲在新生代達到一定閾值(默認15),則進入老年代。
永久代:這是一個非常特殊的區域,這個是屬於Hotspot這個虛擬機比較獨有的,不同的虛擬機實現不一樣,但如J9、JRockit是沒有永久代的概念,永久代只是一個概念上的意義,永久代也會發生垃圾回收,但條件比較苛刻,稍後會在JDK6、7、8的區別中講到,而永久代在JDK8時就給移除了,改爲元空間。
直接內存/堆外內存:不屬於JVM運行時數據區的,應用於NIO中,直接內存是跳過了Java堆,來提升內存的訪問速度,其實就是使用通道和緩衝區的方式進行IO交互的方式,在操作的過程中如果沒有設置這一塊的堆空間大小,會引起OOM,可以通過-XX:MaxDirectMemonrySize進行設置,如果不配置默認爲最大的堆空間大小。
注:直接內存也會觸發GC的。
思考:Java爲什麼要這樣設計JVM?爲什麼還要區分線程共享數據,和線程獨有數據?
作者的思考思路,集中式有集中式的好,分佈式有分佈式的好,具體看其語言定位與發展。
Java運行過程
垃圾回收機制
爲什麼要進行垃圾回收?垃圾是什麼?已經在上面講了。
如何判定對象是可回收的?
如同這段代碼,在方法內執行完了之後,這個test1對象在內存中則是爲null了,因方法結束了,也沒有其他引用了,在進行對象的引用查找時,則查找不到任何的引用,所以爲null,那麼則判定這個對象是不可達的,可以進行回收。
垃圾回收級別:作者將其分爲三個級別,初級回收(minor GC),二級回收(major GC),完全回收(Full GC)。
初級回收(minor GC):當有新的對象要進入新生代的Eden區時, Eden區的空間不足以存放這個對象,則發生初級回收,而活躍的對象會先存放到新生代的sv區域,並記錄年齡+1,當達到閾值(默認15)時就進入老年代。
二級回收(major GC):新生代對象達到閾值或新生代的eden區無法裝入大對象時也會進入老年代,但老年代的空間不足以存放這個對象,則會二級回收。
完全回收(Full GC):就是當老年代的空間不足以存放新對象時或永久代的內存不足以存放內容時等。
那麼完全回收和二級回收的區別在哪裏?
JVM是有自動調節功能的,會根據程序在運行中進行調節的,所以何時觸發完全回收,那麼具體要看JVM的策略,但如果進行了完全回收之後還是出現空間不足以存放,那麼則會出現OOM。
算法
主要說幾個主流的垃圾回收算法的思想。
引用計數算法:計數器計算引用的次數,達到閾值就進入老年代,次數爲0,則進行回收,對資源消耗嚴重,每次引用都要進行計算,但精確。
標記清除算法:分爲標記和清除階段,對標記的對象進行清除,清除後導致內存空間不連續,因而產生空間碎片。
對象何時標記清除?
就是一個樹狀結構,根節點向下查找是否可以查找到這個對象,查找不到的對象,則標記清除。
複製算法:將內存區域分爲兩塊,假設現在在使用A區域,這時要進行垃圾回收,把A區域正在使用的對象複製到B區域去,清除A區域所有對象,反覆的如此進行,完成垃圾收集。
複製算法:將內存區域分爲兩塊,假設現在在使用A區域,這時要進行垃圾回收,把A區域正在使用的對象複製到B區域去,清除A區域所有對象,反覆的如此進行,完成垃圾收集,主要用於新生代。
標記壓縮算法:將存活的對象進行壓縮,放到一個區域後,在進行垃圾回收,就是結合了標記清除算法和複製算法,主要用於老年代。
爲什麼複製算法和標記壓縮算法主要的應用地方不一致?
新生代GC頻繁,老年代對象大多數都是穩定的狀態,對象多、耗時長。
分代算法:按照對應的策略將內存分爲N塊區域,根據策略的規定將不同的對象放入不同的區域,控制回收的空間,而不是每次都針對整個空間進行回收,減少GC停頓時間。
如:有個城市是這樣規劃的女孩子做針線活比較厲害,則把女孩子放到針線區,男孩子力氣比較大則放到搬運區,小孩子喜歡玩,放到遊樂區,遊樂區的人慢慢多了,那麼就只對遊樂區進行人行疏導,而不需要整個城市都需要進行疏導,對遊樂區進行疏導的時候也不會影響到別的區的運行。
分區算法:將內存分爲N塊獨立空間,每次只控制回收多少空間,而不是每次都針對整個空間進行回收,減少GC停頓時間。
分代算法和分區算法區別:分代就是根據對象的特點進行劃分,分區就是不管你是什麼對象,控制每次回收多少個空間。
注:GC停頓就是把在進行垃圾回收的時候,會掛起正在運行的線程,使得其不在產生新的垃圾,回收完了之後才重新運行這些線程。
回收器
注:使用參數和設置線程數這些,讀者請自己去找文檔。
串行收集器:單線程進行垃圾回收,適用於並行能力不強的計算機(CPU),可以在新生代和老年代中使用,根據作用於不同的堆空間分爲新生代串行回收器和老年代串行回收器。
Serial回收器:採用複製算法,在進行垃圾回收的時候其他線程會給掛起,直到垃圾回收完成(俗稱:STW,全世界停止),開啓後年輕代和老年代都採用這個回收器。
並行收集器:在串行的基礎改爲多線程並行進行垃圾回收,適用於並行能力強的計算機。
ParNew回收器:適用於新生代的垃圾回收器,只是進行簡單的串行多線程化,回收策略和算法和串行是一樣的。
ParallelGC回收器:適用於新生代,採用了複製算法的收集器,在進行垃圾回收的時候會進入STW,直到垃圾回收完成,ParallelGC是非常關注系統吞吐量。
ParallelOldGC回收器:適用於老年代, ParallelGC回收器一樣,但採用標記壓縮算法實現
CMS回收器:應用於老年代的多線程回收器,採用標記清除算法,主要關注系統停頓時間,是目前主流的回收器,CMS的整個回收過程分爲,初始標記、併發標記、重新標記、併發清除四個步驟,在初始標記和重新標記時會進入STW,在併發標記和併發清除的過程中是不會進入STW的,而是應用程序可以不停的工作,但CMS在回收的過程中要保證內存有足夠資源,CMS回收時機是達到閾值後,觸發回收,老年代默認閾值是68%。
如果在CMS回收過程中,內存不足,那麼則觸發老年代的串行回收器,且CMS無法處理浮動垃圾(第一次告訴GC不使用,標記吧,留待第二次GC回收,第二次GC回收時告訴GC,我現在又要用了,但GC還是回收了。)
注:可以通過參數設置CMS回收多少次進行碎片整理和壓縮。
G1回收器:採用了分區算法,獨特的回收策略的多線程回收器,區分新生代和老年代依然有Eden、Sv0、Sv1區,不要求整個Eden區或新生代,老年代空間是連續的,G1的出現主要爲了替代CMS,CMS採用標記清除導致出現空間碎片,對CPU資源的要求等,G1回收器是可以應用到新生代和老年代,但還是無法解決浮動垃圾等問題。
JVM與多線程
注:這裏不談論過多的多線程的內容,未來作者會單獨對多線程進行撰寫。
JVM與多線程-圖一
JVM與多線程-圖二
多線程爲什麼需要鎖?
在上面的時候就解釋了,JVM的數據區內的方法區和堆是線程共享的,在JVM與多線程-圖一說明了方法區與堆是共享的,JVM與多線程-圖二則說明了線程的棧是獨有的,方法是在棧中運行的,當兩個線程互相搶佔CPU資源,會導致執行順序不可控,促使執行結果是不可控的。
多線程鎖:大體上線程併發常見的鎖有,自旋鎖、偏向鎖、輕量鎖、重量鎖。
自旋鎖:當前線程不會進入阻塞等待鎖的狀態,而是會通過循環的方式嘗試獲取到鎖。
偏向鎖:某個線程一直在執行某一段代碼的時候,獲取到鎖一次,之後就默認是自動獲取到鎖了,是一種提高性能的方式。
輕量鎖:當前線程還是處於偏向鎖的狀態,當有別的線程在訪問時則會升級爲輕量鎖,其他線程可以通過自旋鎖進行獲取。
重量鎖:A線程在處於輕量級鎖時,B線程通過自旋的方式去嘗試獲取到鎖,當達到自旋的閾值時還沒有獲取到鎖,B線程則會進入阻塞狀態,A線程的鎖就變爲重量鎖。
JVM調試參數
Java8:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
以下展示的是Java7常用的:
行爲參數
指令 |
描述 |
-XX:-AllowUserSignalHandlers |
如果應用程序安裝了信號處理程序,請不要抱怨。(只適用於Solaris和Linux)。 |
-XX:AltStackSize=16384 |
備用信號棧大小(以Kbytes表示)。(僅與Solaris相關,從5.0刪除)。 |
-XX:-DisableExplicitGC |
在默認情況下,調用System.gc()是啓用的(-XX:- disableitgc)。使用-XX:+ disableitgc來禁用對System.gc()的調用。注意,JVM仍然在必要時執行垃圾收集。 |
-XX:+FailOverToOldVerifier |
當新的類型檢查失敗時,故障轉移到舊的驗證器。(介紹6)。 |
-XX:+HandlePromotionFailure |
最年輕的一代收集不需要保證所有的活物體都能得到充分的推廣。(在1.4.2更新中引入)[5.0和更早:false。] |
-XX:+MaxFDLimit |
將文件描述符的數量增加到最大值。(Solaris。) |
-XX:PreBlockSpin=10 |
自旋計數變量使用-XX:+ usesping。在輸入操作系統線程同步代碼之前,控制最大的自旋迭代。(1.4.2中介紹)。 |
-XX:-RelaxAccessControlCheck |
在驗證器中放鬆訪問控制檢查。(介紹6)。 |
-XX:+ScavengeBeforeFullGC |
在完整的GC之前進行年輕一代GC。(介紹1.4.1)。 |
-XX:+UseAltSigs |
使用替代信號代替SIGUSR1和SIGUSR2,用於VM內部信號。(在1.3.1更新中引入,1.4.1。與Solaris。) |
-XX:+UseBoundThreads |
將用戶級線程綁定到內核線程。(與Solaris。) |
-XX:-UseConcMarkSweepGC |
爲老一代人使用併發的標記-清除集合。1.4.1(介紹) |
-XX:+UseGCOverheadLimit |
使用一個策略,在拋出OutOfMemory錯誤之前,限制在GC中使用的VM時間的比例。(介紹6)。 |
-XX:+UseLWPSynchronization |
使用基於lwp的而不是基於線程的同步。(介紹1.4.0。與Solaris。) |
-XX:-UseParallelGC |
使用並行垃圾收集來清除垃圾。1.4.1(介紹) |
-XX:-UseParallelOldGC |
爲完整的集合使用並行垃圾收集。啓用此選項將自動設置-XX:+UseParallelGC。(在5.0更新中引入) |
-XX:-UseSerialGC |
使用串行垃圾收集。(5.0中引入的)。 |
-XX:-UseSpinning |
在進入操作系統線程同步代碼之前,允許在Java監視器上進行簡單的旋轉。(只適用於1.4.2和5.0)[1.4.2,多處理器Windows平臺:true] |
-XX:+UseTLAB |
使用線程本地對象分配(在1.4.0中引入,在此之前被稱爲UseTLE)[1.4.2和更早的,x86或與-客戶端:false] |
-XX:+UseSplitVerifier |
使用具有StackMapTable屬性的新類型檢查器。(5.0中引入的。)(5.0:假) |
-XX:+UseThreadPriorities |
使用本機線程優先級。 |
-XX:+UseVMInterruptibleIO |
在OS_INTRPT中,線程中斷之前或與EINTR之間的I/O操作結果。(介紹了6。與Solaris。) |
G1垃圾回收器參數
指令 |
描述 |
-XX:+UseG1GC |
使用垃圾優先(G1)收集器。 |
-XX:MaxGCPauseMillis=n |
設置最大GC暫停時間的目標。這是一個軟目標,JVM將盡最大努力實現它。 |
-XX:InitiatingHeapOccupancyPercent=n |
啓動一個併發GC循環的(整個)堆佔用率。它是由GCs使用的,它基於整個堆的佔用而觸發一個併發的GC循環,而不僅僅是一代(例如G1)。0的值表示“持續GC循環”。默認值是45。 |
-XX:NewRatio=n |
新舊一代的比例。默認值是2。 |
-XX:SurvivorRatio=n |
伊甸園/倖存者空間大小的比率。默認值是8。 |
-XX:MaxTenuringThreshold=n |
保持閾值的最大值。默認值是15。 |
-XX:ParallelGCThreads=n |
設置在垃圾收集器的並行階段中使用的線程數。默認值隨JVM運行的平臺而異。 |
-XX:ConcGCThreads=n |
併發垃圾收集器將使用的線程數。默認值隨JVM運行的平臺而異。 |
-XX:G1ReservePercent=n |
設置保留爲假上限的堆數量,以減少升級失敗的可能性。默認值是10。 |
-XX:G1HeapRegionSize=n |
在G1中,Java堆被細分爲一致大小的區域。這設置了每個子分區的大小。該參數的默認值是根據堆大小確定的。最小值爲1Mb,最大值爲32Mb。 |
性能參數
指令 |
描述 |
-XX:+AggressiveOpts |
打開在即將發佈的版本中默認爲默認的點性能編譯器優化。(在5.0更新中引入) |
-XX:CompileThreshold=10000 |
編譯前的方法調用/分支數量[-客戶端:1,500] |
-XX:LargePageSizeInBytes=4m |
設置用於Java堆的大頁面大小。(引入1.4.0更新1)[amd64: 2m] |
-XX:MaxHeapFreeRatio=70 |
在GC之後最大百分比的堆釋放,以避免收縮。 |
-XX:MaxNewSize=size |
新生成的最大大小(以字節爲單位)。自1.4以來,MaxNewSize被計算爲NewRatio的函數。(1.3.1 Sparc:32 m;1.3.1 x86:2.5。] |
-XX:MaxPermSize=64m |
永久世代的規模。[5.0和更新:64位虛擬機的比例增加了30%;1.4 amd64:96;1.3.1客戶:32 m。) |
-XX:MinHeapFreeRatio=40 |
在GC後,堆的最小百分比以避免擴展。 |
-XX:NewRatio=2 |
新舊一代的比例。[Sparc客戶:8;x86 - server:8;x86客戶:12。]-客戶端:4 (1.3)8 (1.3.1+),x86: 12] |
-XX:NewSize=2m |
新生成的默認大小(以字節爲單位)[5.0和更新:64位虛擬機的比例增加了30%;x86:1米;x86, 5.0及以上:640k] |
-XX:ReservedCodeCacheSize=32m |
保留代碼緩存大小(以字節爲單位)——最大的代碼緩存大小。[Solaris 64位,amd64和-server x86: 2048m;在1.5.0_06和更早的版本中,Solaris 64位和amd64: 1024m。 |
-XX:SurvivorRatio=8 |
eden/倖存者空間尺寸的比例[Solaris amd64: 6;Sparc在1.3.1:25;其他Solaris平臺在5.0和更早:32] |
-XX:TargetSurvivorRatio=50 |
清除後使用的倖存者空間的期望百分比。 |
-XX:ThreadStackSize=512 |
線程堆棧大小(以Kbytes表示)。(0表示使用默認棧大小)[Sparc: 512;Solaris x86: 320(在5.0和更早之前是256);Sparc 64位:1024;Linux amd64: 1024(5.0或更早時爲0);所有其他0。) |
-XX:+UseBiasedLocking |
使偏向鎖。有關更多細節,請參見此調優示例。(在5.0更新中引入)[5.0:false] |
-XX:+UseFastAccessorMethods |
使用得到<原始>字段的優化版本。 |
-XX:-UseISM |
使用的共享內存。不接受非solaris平臺。)有關細節,請參見親密共享內存。 |
-XX:+UseLargePages |
使用大頁面內存。(在5.0更新中引入)有關詳細信息,請參見Java對大內存頁的支持。 |
-XX:+UseMPSS |
使用多個頁面大小來支持堆的w/4mb頁面。不要用“主義”來代替“主義”的需要。(在1.4.0版本中引入,與Solaris 9和更新版本相關)[1.4.1和更早:false] |
-XX:+UseStringCache |
啓用通常分配的字符串的緩存。 |
-XX:AllocatePrefetchLines=1 |
使用JIT編譯代碼中生成的預取指令,在最後一個對象分配之後加載的緩存行數。如果最後一個分配的對象是一個實例,如果它是一個數組,默認值是1。 |
-XX:AllocatePrefetchStyle=1 |
爲預取指令生成的代碼樣式。 0 -無預取指令產生*d*, 1 -每次分配後執行預取指令, 2 -在執行預取指令時,使用TLAB分配水印指針到gate。 |
-XX:+UseCompressedStrings |
對可以表示爲純ASCII的字符串使用一個字節[]。(引入Java 6更新21性能版本) |
-XX:+OptimizeStringConcat |
儘可能優化字符串連接操作。(Java 6更新20) |
日誌參數
指令 |
描述 |
-XX:-CITime |
打印時間花在JIT編譯器上。(介紹1.4.0)。 |
-XX:ErrorFile=./hs_err_pid<pid>.log |
如果發生錯誤,將錯誤數據保存到該文件。(介紹6)。 |
-XX:-ExtendedDTraceProbes |
啓用performance-impacting dtrace探測。(介紹了6。與Solaris。) |
-XX:HeapDumpPath=./java_pid<pid>.hprof |
用於堆轉儲的目錄或文件名路徑。可控的。(1.4.2更新12,5.0更新7) |
-XX:-HeapDumpOnOutOfMemoryError |
當java.lang時將堆轉儲到文件中。拋出OutOfMemoryError。可控的。(1.4.2更新12,5.0更新7) |
-XX:OnError="<cmd args>;<cmd args>" |
在致命錯誤上運行用戶定義的命令。(在1.4.2更新中介紹) |
-XX:OnOutOfMemoryError="<cmd args>; |
當第一次拋出OutOfMemoryError時,運行用戶定義的命令。(介紹1.4.2更新12,6) |
-XX:-PrintClassHistogram |
在Ctrl-Break上打印類實例的直方圖。可控的。(1.4.2中介紹)。jmap -histocommand提供了等價的功能。 |
-XX:-PrintConcurrentLocks |
打印java.util。在Ctrl-Break線程轉儲中併發鎖。可控的。(介紹6)。jstack -lcommand提供了等價的功能 |
-XX:-PrintCommandLineFlags |
在命令行上出現的打印標誌。(5.0中引入的)。 |
-XX:-PrintCompilation |
在編譯方法時打印消息。 |
-XX:-PrintGC |
在垃圾收集中打印消息。可控的。 |
-XX:-PrintGCDetails |
在垃圾收集中打印更多的細節。可控的。(介紹1.4.0)。 |
-XX:-PrintGCTimeStamps |
在垃圾收集中打印時間戳。管理(介紹1.4.0)。 |
-XX:-PrintTenuringDistribution |
打印任期年齡信息。 |
-XX:-PrintAdaptiveSizePolicy |
允許打印關於自適應生成規模的信息。 |
-XX:-TraceClassLoading |
跟蹤加載的類。 |
-XX:-TraceClassLoadingPreorder |
跟蹤所有已加載的類(未加載)。(1.4.2中介紹)。 |
-XX:-TraceClassResolution |
跟蹤常量池的決議。(1.4.2中介紹)。 |
-XX:-TraceClassUnloading |
跟蹤卸貨的類。 |
-XX:-TraceLoaderConstraints |
加載器約束的跟蹤記錄。(介紹6)。 |
-XX:+PerfDataSaveToFile |
在退出時保存jvmstat二進制數據。 |
-XX:ParallelGCThreads=n |
在年輕和舊的並行垃圾收集器中設置垃圾收集線程的數量。默認值隨JVM運行的平臺而異。 |
-XX:+UseCompressedOops |
允許使用壓縮指針(對象引用表示爲32位的偏移量,而不是64位指針)以優化64位性能,Java堆大小小於32gb。 |
-XX:+AlwaysPreTouch |
在JVM初始化期間預觸摸Java堆。因此,堆的每一頁都是在初始化過程中,而不是在應用程序執行期間遞增的。 |
-XX:AllocatePrefetchDistance=n |
設置對象分配的預取距離。在這個距離(以字節爲單位),在最後一個分配對象的地址之外,以新對象的值寫入內存。每個Java線程都有自己的分配點。默認值隨JVM運行的平臺而異。 |
-XX:InlineSmallCode=n |
僅當生成的本機代碼大小小於這個時,內聯一個以前編譯的方法。默認值隨JVM運行的平臺而異。 |
-XX:MaxInlineSize=35 |
一個方法的最大字節碼大小。 |
-XX:FreqInlineSize=n |
最大字節碼大小的經常執行的方法被內聯。默認值隨JVM運行的平臺而異。 |
-XX:LoopUnrollLimit=n |
使用服務器編譯器中間表示節點的展開循環體的計數小於該值。服務器編譯器使用的限制是這個值的函數,而不是實際值。默認值隨JVM運行的平臺而異。 |
-XX:InitialTenuringThreshold=7 |
設置在並行的年輕收集器中用於自適應GC分級的初始閾值。招貼閾值是指一個物體在被提升到舊的或終身的一代之前,在年輕的集合中存活的次數。 |
-XX:MaxTenuringThreshold=n |
設置在自適應GC分級中使用的最大閾值。當前最大的值是15。並行收集器的默認值爲15,CMS的默認值爲4。 |
-Xloggc:<filename> |
日誌GC詳細輸出到指定的文件。詳細輸出由正常的詳細GC標誌控制。 |
-XX:-UseGCLogFileRotation |
啓用GC日誌旋轉,需要-Xloggc。 |
-XX:NumberOfGClogFiles=1 |
設置旋轉日誌時要使用的文件數量,必須是>= 1。旋轉的日誌文件將使用以下命名方案,<filename>。0,<文件名>。1,…,<文件名> .n-1。 |
-XX:GCLogFileSize=8K |
日誌文件的大小將會被旋轉,必須是>= 8K。 |
JDK6、7、8的JVM區別
1.6
1.7
1.8
可以看到1.6到1.7可以說變化並不大,但到了1.8時,大家可以發現非常大了,出現了元空間的區域,並這個區域是在本地內存中的,且這個區域是存儲類的元數據信息的,類的常量、方法等。
看起來元空間似乎和之前的方法區/永久代沒有什麼區別,元空間是使用本地內存的,受制於本地內存大小,在沒有通過(MaxMetaspaceSize)VM參數設置時,會根據程序的運行時間動態調控大小。
那麼也就不會再出現OOM的情況了,但元空間只是爲了解決OOM的問題嗎?
爲什麼要有元空間?
永久代主要是用於存儲類的信息,但很難確定類的大小,所以在指定的時候就有點困難,容易造成OOM,另外一個原因就是Hotspot和JRockit的合併,JRockit是沒有永久代的。
爲什麼Hotspot要和JRockit合併?
必然合併肯定是要實現互補的,JRockit的任務控制、垃圾收集算法、監控等能力都是比較優秀的,而Hotspot在性能優勢也就使得其比較複雜,所以結合雙方等各個優點進行合併,形成更強大的JVM。
另外Hotspot和JRockit都是Oracle旗下的。
元空間帶來的影響?
有部分的數據移到堆,所以在1.8的時候會發現堆的空間會增加的比以往快,由於是使用本地內存,如果吞吐量大的時候,會帶來大量的交換區交換。
元空間是否有垃圾回收?
當然也會有垃圾回收,不可能說應用程序不用這個類了,這個類失效了,還要一直保留着這些信息,這個是絕對不合理的。
元空間垃圾回收觸發時機?
上面我們提到,元空間是存儲類的元數據信息的,類加載器加載類的信息到元空間中,當這個類不在有引用時,這個類的信息就會給回收了。
調試JVM
爲什麼這裏寫的是調試,而不是優化。
免得誤人子弟,優化這個問題,是要根據不同的應用程序進行優化,而調試也是一個比較大的話題吧,具體的調試的參數還是要根據應用程序而定。
這裏分享下作者的思路:程序的定位(吞吐量等),程序的運行,位置定位。
-
1. 系統內存泄漏時會先定位是程序的哪裏的代碼導致的內存泄漏,如果是NIO導致的內存泄漏,則可能是堆外內存泄漏,如果遞歸循環則有可能導致的是棧,先定位到位置,之後在進行參數的調試,一步一步的確認位置。
-
2. 程序在運行的過程中,不斷的越來越慢,而應用程序的吞吐量是比較大(這個時候我們還是要先定位到位置,如果是產生大量的對象,而這些對象的使用次數也不多,當有相當一部分在很多時候達到了進入老年代的條件,從而進入了老年代,但進入老年代後呈現就不在使用這個對象,我們都知道老年代的對象比較穩定,回收的不多,那麼處理的時間長,所以對老年代的回收時間會比較久),那麼可以通過調整進入老年代的條件,儘量使得對象在新生代時就給回收了,並減少GC次數。
注:JDK7版本後(包含),部分JVM參數已經是擁有自動化調整的能力,如TLAB區域,除非是對系統等各個方面熟悉,否則建議不要亂調參數。
可以寫一個遞歸方法造成內存泄漏在程序啓動時配置導出dump文件參數,可以使用Eclipse的MAT插件查看dump文件,或jvisualvm等工具查看。
爲什麼要學JVM?
學習了JVM後,我們來看個問題,爲什麼學JVM?
以下只是作者的個人觀點:
工作:JVM是作爲一名Java程序員所必備瞭解的過程,但隨着工作的年限的增長,可能接觸到的項目越來越多,而項目本身業務的複雜性可能會出現一次性加載的東西太多,導致內存出現泄漏,而我們當我們沒有去了解JVM的時候,會認爲是硬件問題,或許上百度查一下知道是內存泄漏,要把堆調大,但如果是堆外內存泄漏呢?那麼當應用程序吞吐量大的時候,是否可以通過調整進入老年代的條件而利用好內存空間呢?
面試:現在很多公司在面試的時候都會問關於JVM的內容。