JVM原理與深度調(diào)優(yōu)
什么是jvm
jvm是java虛擬機(jī) 運(yùn)行在用戶態(tài)、通過應(yīng)用程序?qū)崿F(xiàn)java代碼跨平臺(tái)、與平臺(tái)無關(guān)、實(shí)際上是"一次編譯,到處執(zhí)行"
1.從微觀來說編譯出來的是字節(jié)碼!去到哪個(gè)平臺(tái)都能用,只要有那個(gè)平臺(tái)的JDK就可以運(yùn)行!字碼好比是一個(gè)人,平臺(tái)好比為國家,JDK好比這個(gè)國家的語言!只要這個(gè)人(字節(jié)碼)有了這個(gè)國家的語言(JDK)就可以在這個(gè)國家(平臺(tái))生活下去。
2.JDK 是整個(gè)Java的核心,包括了Java運(yùn)行環(huán)境(Java Runtime Envirnment),一堆Java工具和Java基礎(chǔ)的類庫(rt.jar)。
3.Java虛擬機(jī)(JVM)一種用于計(jì)算機(jī)設(shè)備的規(guī)范,可用不同的方式(軟件或硬件)加以實(shí)現(xiàn)。編譯虛擬機(jī)的指令集與編譯微處理器的指令集非常類似。Java虛擬機(jī)包括一套字節(jié)碼指令集、一組寄存器、一個(gè)棧、一個(gè)垃圾回收堆和一個(gè)存儲(chǔ)方法域。
4.java編譯出來的是一種“java字節(jié)碼”,由虛擬機(jī)去解釋執(zhí)行。而c和c++則編譯成了二進(jìn)制,直接交由操作系統(tǒng)執(zhí)行。
5.所謂的一次編譯、到處執(zhí)行,即只需在一個(gè)地方編譯,在其他各個(gè)平臺(tái)下都可以執(zhí)行。
6.與平臺(tái)無關(guān)指的是JAVA只運(yùn)行在自己的JVM上,不需要依賴任何其他的底層類,所以和操作系統(tǒng)沒有任何聯(lián)系,平臺(tái)是說運(yùn)行的系統(tǒng)
內(nèi)存結(jié)構(gòu)圖
class文件
class文件徑打破了C或者C++等語言所遵循的傳統(tǒng),使用這些傳統(tǒng)語言寫的程序通常首先被編譯,然后被連接成單獨(dú)的、專門支持特定硬件平臺(tái)和操作系統(tǒng)的二進(jìn)制文件。通常情況下,一個(gè)平臺(tái)上的二進(jìn)制可執(zhí)行文件不能在其他平臺(tái)上工作。而Java class文件是可以運(yùn)行在任何支持Java虛擬機(jī)的硬件平臺(tái)和操作系統(tǒng)上的二進(jìn)制文件。
執(zhí)行過程
執(zhí)行過程簡介
當(dāng)編譯和連接一個(gè)C++程序時(shí),所獲得的可執(zhí)行二進(jìn)制文件只能在指定的硬件平臺(tái)和操作系統(tǒng)上運(yùn)行,因?yàn)檫@個(gè)二進(jìn)制文件包含了對目標(biāo)處理器的機(jī)器語言。而Java編譯器把Java源文件的指令翻譯成字節(jié)碼,這種字節(jié)碼就是Java虛擬機(jī)的“機(jī)器語言”。
與普通程序不同的是,Java程序(class文件)并不是本地的可執(zhí)行程序。當(dāng)運(yùn)行Java程序時(shí),首先運(yùn)行JVM(Java虛擬機(jī)),然后再把Java class加載到JVM里頭運(yùn)行,負(fù)責(zé)加載Java class的這部分就叫做Class Loader。
JVM中的ClassLoader
JVM本身包含了一個(gè)ClassLoader稱為Bootstrap ClassLoader,和JVM一樣,BootstrapClassLoader是用本地代碼實(shí)現(xiàn)的,它負(fù)責(zé)加載核心JavaClass(即所有java.*開頭的類)。
另外JVM還會(huì)提供兩個(gè)ClassLoader,它們都是用Java語言編寫的,由BootstrapClassLoader加載;其中Extension ClassLoader負(fù)責(zé)加載擴(kuò)展的Javaclass(例如所有javax.*開頭的類和存放在JRE的ext目錄下的類)ApplicationClassLoader負(fù)責(zé)加載應(yīng)用程序自身的類。
當(dāng)運(yùn)行一個(gè)程序的時(shí)候,JVM啟動(dòng),運(yùn)行bootstrapclassloader,該ClassLoader加載java核心API(ExtClassLoader和AppClassLoader也在此時(shí)被加載),然后調(diào)用ExtClassLoader加載擴(kuò)展API,最后AppClassLoader加載CLASSPATH目錄下定義的Class,這就是一個(gè)程序最基本的加載流程。
第一個(gè)Class文件、通過javac編譯成字節(jié)碼、字節(jié)碼之后有個(gè)ClassLoader叫類加載器,因?yàn)閖ava.class文件到JVM內(nèi)部運(yùn)行起來需要有個(gè)裝載過程、從物理的文件到內(nèi)存的結(jié)構(gòu)、比如加載、連接、初始化。
linux應(yīng)用程序有個(gè)進(jìn)程地址空間,對進(jìn)程地址空間的解釋:
linux采用虛擬內(nèi)存管理技術(shù),每一個(gè)進(jìn)程都有一個(gè)3G大小的獨(dú)立的進(jìn)程地址空間,這個(gè)地址空間就是用戶空間。每個(gè)進(jìn)程的用戶空間都是完全獨(dú)立、互不相干的。進(jìn)程訪問內(nèi)核空間的方式:系統(tǒng)調(diào)用和中斷。
創(chuàng)建進(jìn)程等進(jìn)程相關(guān)操作都需要分配內(nèi)存給進(jìn)程。這時(shí)進(jìn)程申請和獲得的不是物理地址,僅僅是虛擬地址。
實(shí)際的物理內(nèi)存只有當(dāng)進(jìn)程真的去訪問新獲取的虛擬地址時(shí),才會(huì)由“請頁機(jī)制”產(chǎn)生“缺頁”異常,從而進(jìn)入分配實(shí)際頁框的程序。該異常是虛擬內(nèi)存機(jī)制賴以存在的基本保證,它會(huì)告訴內(nèi)核去為進(jìn)程分配物理頁,并建立對應(yīng)的頁表,這之后虛擬地址才實(shí)實(shí)在在的映射到了物理地址上。
Linux操作系統(tǒng)采用虛擬內(nèi)存技術(shù),所有進(jìn)程之間以虛擬方式共享內(nèi)存。進(jìn)程地址空間由每個(gè)進(jìn)程中的線性地址區(qū)組成,而且更為重要的特點(diǎn)是內(nèi)核允許進(jìn)程使用該空間中的地址。通常情況況下,每個(gè)進(jìn)程都有唯一的地址空間,而且進(jìn)程地址空間之間彼此互不相干。但是進(jìn)程之間也可以選擇共享地址空間,這樣的進(jìn)程就叫做線程。
基本上所有l(wèi)inux應(yīng)用程序都會(huì)遵循這個(gè)規(guī)泛、有棧、有堆、對于JVM來說、也是遵循這個(gè)規(guī)則、只不過在這個(gè)規(guī)則上做了一些改進(jìn)
通過類加載器把Class文件裝載進(jìn)內(nèi)存空間、裝進(jìn)來以后只是你的字節(jié)碼,然后你需要去運(yùn)行、怎么去運(yùn)行呢 ?圖中類加載器子系統(tǒng)下面都是運(yùn)行區(qū)
內(nèi)存空間里有:
1.方法區(qū):被裝載的class的信息存儲(chǔ)在Methodarea的內(nèi)存中。當(dāng)虛擬機(jī)裝載某個(gè)類型時(shí),它使用類裝載器定位相應(yīng)的class文件,然后讀入這個(gè)class文件內(nèi)容并把它傳輸?shù)教摂M機(jī)中。
2.Heap(堆):一個(gè)Java虛擬實(shí)例中只存在一個(gè)堆空間。
3.JavaStack(java的棧):虛擬機(jī)只會(huì)直接對棧執(zhí)行兩種操作:以幀為單位的壓?;虺鰲?,java棧有個(gè)核心的數(shù)據(jù)、先進(jìn)后出
4.Nativemethodstack(本地方法棧):通過字面意思、基本是調(diào)用系統(tǒng)本地的一些方法、一般在底層封裝好了、直接調(diào)用
5.地址、在這里邊是一個(gè)指針的概念、比如從變量到對象怎么做引用、就是地址
6.計(jì)數(shù)器:主要做字節(jié)碼解析的時(shí)候要記住它的位置、可以理解為一個(gè)標(biāo)記
7.執(zhí)行引擎:數(shù)據(jù)、字節(jié)碼做一些業(yè)務(wù)處理、最終達(dá)到想要的結(jié)果
8.本地方法接口:基本是底層系統(tǒng)、比如IO網(wǎng)絡(luò)、調(diào)用操作系統(tǒng)本身
9.本地方法庫:為了兼容、實(shí)現(xiàn)跨平臺(tái)有不同的庫 、兼容平臺(tái)性
額外數(shù)據(jù)信息指的是本地方法接口和本地方法庫
JMM
java的內(nèi)存模型
大家可能聽過一個(gè)詞、叫線程安全、在寫高并發(fā)的時(shí)候就會(huì)有線程安全問題、java里邊為什么會(huì)出現(xiàn)線程安全問題呢、因?yàn)橛蠮MM的存在、它會(huì)把內(nèi)存分為兩個(gè)區(qū)域(一個(gè)主內(nèi)存、一個(gè)是工作內(nèi)存)工作內(nèi)存是每個(gè)java棧所私有的
因?yàn)橐\(yùn)行速度快、需要把主內(nèi)存的數(shù)據(jù)放到本地內(nèi)存中、然后進(jìn)行計(jì)算、計(jì)算完以后再把數(shù)據(jù)回顯回去
JMM有兩個(gè)區(qū)域、主內(nèi)存和棧內(nèi)存、
java線程可能不止一個(gè)、可能有多個(gè)棧、現(xiàn)在需要三個(gè)線程同時(shí)做個(gè)運(yùn)算、主內(nèi)存初始值x=0 需要把x=0都要裝載在自己的內(nèi)存里邊去、相當(dāng)于有一個(gè)
副本、現(xiàn)在初始值和三個(gè)棧都是x=0
現(xiàn)在需要做運(yùn)算
- x=x+1
- x=x-1
- x=0
我們的期望值是x=0,如果是單個(gè)線程跑沒問題 、取回x=0、運(yùn)算x=+1、回顯進(jìn)來主內(nèi)存就是1 、棧1是1,運(yùn)算x=-1、回顯進(jìn)來主內(nèi)存就是0、棧1是0
如果多個(gè)線程同時(shí)執(zhí)行、結(jié)果是不可預(yù)期的、正因?yàn)橛羞@種結(jié)構(gòu)的存在、當(dāng)執(zhí)行x=+1、棧1是x=1 、棧2來不及執(zhí)行、棧1就已經(jīng)把x=1寫到主內(nèi)存了 、棧2跟棧3拿過去之后初始值就不是0、可能就是1了 、這樣程序就寫亂了
所以在java中就出現(xiàn)了很多鎖、來確保線程安全
運(yùn)行時(shí)數(shù)據(jù)區(qū)
PC寄存器----線程私有
PC寄存器也叫程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的信號(hào)指示器。
每一條JVM線程都有自己的PC寄存器
在任意時(shí)刻,一條JVM線程只會(huì)執(zhí)行一個(gè)方法的代碼。該方法稱為該線程的當(dāng)前方法(Current Method)
如果該方法是java方法,那PC寄存器保存JVM正在執(zhí)行的字節(jié)碼指令的地址
如果該方法是native,那PC寄存器的值是undefined。
此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
Java虛擬機(jī)棧 ----線程私有
與PC寄存器一樣,java虛擬機(jī)棧(Java Virtual Machine Stack)也是線程私有的。每一個(gè)JVM線程都有自己的java虛擬機(jī)棧,這個(gè)棧與線程同時(shí)創(chuàng)建,它的生命周期與線程相同。
虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完成的過程就對應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
JVM stack 可以被實(shí)現(xiàn)成固定大小,也可以根據(jù)計(jì)算動(dòng)態(tài)擴(kuò)展。
如果采用固定大小的JVM stack設(shè)計(jì),那么每一條線程的JVM Stack容量應(yīng)該在線程創(chuàng)建時(shí)獨(dú)立地選定。JVM實(shí)現(xiàn)應(yīng)該提供調(diào)節(jié)JVM Stack初始容量的手段。
如果采用動(dòng)態(tài)擴(kuò)展和收縮的JVM Stack方式,應(yīng)該提供調(diào)節(jié)最大、最小容量的手段。
JVM Stack 異常情況:
StackOverflowError:當(dāng)線程請求分配的棧容量超過JVM允許的最大容量時(shí)拋出
OutOfMemoryError:如果JVM Stack可以動(dòng)態(tài)擴(kuò)展,但是在嘗試擴(kuò)展時(shí)無法申請到足夠的內(nèi)存去完成擴(kuò)展,或者在建立新的線程時(shí)沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的虛擬機(jī)棧時(shí)拋出。
本地方法棧----線程私有
Java虛擬機(jī)可能會(huì)使用到傳統(tǒng)的棧來支持native方法(使用Java語言以外的其它語言編寫的方法)的執(zhí)行,這個(gè)棧就是本地方法棧(Native Method Stack)
如果JVM不支持native方法,也不依賴與傳統(tǒng)方法棧的話,可以無需支持本地方法棧。
如果支持本地方法棧,則這個(gè)棧一般會(huì)在線程創(chuàng)建的時(shí)候按線程分配。
異常情況:
StackOverflowError:如果線程請求分配的棧容量超過本地方法棧允許的最大容量時(shí)拋出
OutOfMemoryError:如果本地方法??梢詣?dòng)態(tài)擴(kuò)展,并且擴(kuò)展的動(dòng)作已經(jīng)嘗試過,但是目前無法申請到足夠的內(nèi)存去完成擴(kuò)展,或者在建立新的線程時(shí)沒有足夠的內(nèi)存去創(chuàng)建對應(yīng)的本地方法棧,那Java虛擬機(jī)將會(huì)拋出一個(gè)OutOfMemoryError異常。
Jave堆----線程公用
平時(shí)所說的java調(diào)優(yōu)就是它
在JVM中,堆(heap)是可供各條線程共享的運(yùn)行時(shí)內(nèi)存區(qū)域,也是供所有類實(shí)例和數(shù)據(jù)對象分配內(nèi)存的區(qū)域。
Java堆載虛擬機(jī)啟動(dòng)的時(shí)候就被創(chuàng)建,堆中儲(chǔ)存了各種對象,這些對象被自動(dòng)管理內(nèi)存系統(tǒng)(Automatic Storage Management System,也即是常說的“Garbage Collector(垃圾回收器)”)所管理。這些對象無需、也無法顯示地被銷毀。
Java堆的容量可以是固定大小,也可以隨著需求動(dòng)態(tài)擴(kuò)展,并在不需要過多空間時(shí)自動(dòng)收縮。
Java堆所使用的內(nèi)存不需要保證是物理連續(xù)的,只要邏輯上是連續(xù)的即可。
JVM實(shí)現(xiàn)應(yīng)當(dāng)提供給程序員調(diào)節(jié)Java 堆初始容量的手段,對于可動(dòng)態(tài)擴(kuò)展和收縮的堆來說,則應(yīng)當(dāng)提供調(diào)節(jié)其最大和最小容量的手段。
Java 堆異常:
OutOfMemoryError:如果實(shí)際所需的堆超過了自動(dòng)內(nèi)存管理系統(tǒng)能提供的最大容量時(shí)拋出。
方法區(qū)----線程公用
方法區(qū)是可供各條線程共享的運(yùn)行時(shí)內(nèi)存區(qū)域。存儲(chǔ)了每一個(gè)類的結(jié)構(gòu)信息,例如運(yùn)行時(shí)常量池(Runtime Constant Pool)、字段和方法數(shù)據(jù)、構(gòu)造函數(shù)和普通方法的字節(jié)碼內(nèi)容、還包括一些在類、實(shí)例、接口初始化時(shí)用到的特殊方法
方法區(qū)在虛擬機(jī)啟動(dòng)的時(shí)候創(chuàng)建。
方法區(qū)的容量可以是固定大小的,也可以隨著程序執(zhí)行的需求動(dòng)態(tài)擴(kuò)展,并在不需要過多空間時(shí)自動(dòng)收縮。
方法區(qū)在實(shí)際內(nèi)存空間中可以是不連續(xù)的。
Java虛擬機(jī)實(shí)現(xiàn)應(yīng)當(dāng)提供給程序員或者最終用戶調(diào)節(jié)方法區(qū)初始容量的手段,對于可以動(dòng)態(tài)擴(kuò)展和收縮方法區(qū)來說,則應(yīng)當(dāng)提供調(diào)節(jié)其最大、最小容量的手段。
Java 方法區(qū)異常:
OutOfMemoryError: 如果方法區(qū)的內(nèi)存空間不能滿足內(nèi)存分配請求,那Java虛擬機(jī)將拋出一個(gè)OutOfMemoryError異常。
JVM內(nèi)存分配
內(nèi)存分配其實(shí)真正來講是有三種的、但對于JVM來說只有兩種
- 棧內(nèi)存分配:
大家在調(diào)優(yōu)的過程中會(huì)發(fā)現(xiàn)有個(gè)參數(shù)是-Xss 默認(rèn)是1m,這個(gè)內(nèi)存是棧內(nèi)存分配, 在工作中會(huì)發(fā)現(xiàn)棧OutOfMemory Error內(nèi)存溢出、就是因?yàn)樗膬?nèi)存空間不夠了 一般情況下沒有那么大的棧、除非你的一個(gè)方法里邊有幾十萬行代碼、一直往那壓、不出,所以導(dǎo)致棧的溢出、棧的內(nèi)存分配直接決定了你的線程數(shù) 、比如說你默認(rèn)情況下是1m 、系統(tǒng)一共給你512m、那最高可以分配512個(gè)線程,再多系統(tǒng)分配不了啦、因?yàn)闆]有那么多的內(nèi)存 、像tomcat、resin、jboss等、有個(gè)最大線程數(shù)、要根據(jù)這個(gè)來調(diào)、調(diào)個(gè)100萬沒有意義、分配不了那么大、調(diào)太少整個(gè)性能發(fā)揮不出來 ,調(diào)這個(gè) 、跟你的cpu有關(guān)系、需要找一個(gè)折中位置 、根據(jù)應(yīng)用 、是IO密集型的還是CPU密集型的來調(diào)-Xss的值、它這里邊主要保存了一些參數(shù) 、還有局部變量 、就比如說寫代碼、有開始有結(jié)束、這里邊肯定定義了很多變量、比如:int x=1 y=0 只要在這方法內(nèi)的都屬于局部變量 、因?yàn)槟阋鲞\(yùn)算、要把這東西存住、只有等程序結(jié)束的時(shí)候才能銷毀,對于這種參數(shù)是不會(huì)產(chǎn)生線程安全問題、因?yàn)榫€程是私有的
- 堆內(nèi)存分配:
Java的堆是一個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū),類的(對象從中分配空間。這些對象通過new、newarray、anewarray和multianewarray等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負(fù)責(zé)的,堆的優(yōu)勢是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器,因?yàn)樗窃谶\(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存的,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢
jvm堆結(jié)構(gòu)
(圖一)
1.Young(年輕代)
年輕代分三個(gè)區(qū)。一個(gè)Eden區(qū),兩個(gè)Survivor區(qū)。大部分對象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時(shí),還存活的對象將被復(fù)制到Survivor區(qū)(兩個(gè)中的一個(gè)),當(dāng)這個(gè)Survivor區(qū)滿時(shí),此區(qū)的存活對象將被復(fù)制到另外一個(gè)Survivor區(qū),當(dāng)這個(gè)Survivor區(qū)也滿了的時(shí)候,從第一個(gè)Survivor區(qū)復(fù)制過來的并且此時(shí)還存活的對象,將被復(fù)制年老區(qū)(Old。需要注意,Survivor的兩個(gè)區(qū)是對稱的,沒先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden復(fù)制過來對象,和從前一個(gè)Survivor復(fù)制過來的對象,而復(fù)制到年老區(qū)的只有從第一個(gè)Survivor區(qū)過來的對象。而且,Survivor區(qū)總有一個(gè)是空的。
2.Old(年老代)
年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。
3.Permanent:(持久代)
也叫方法區(qū)、用于存放靜態(tài)文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如hibernate等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來存放這些運(yùn)行過程中新增的類。持久代大小通過-XX:MaxPermSize=進(jìn)行設(shè)置。
舉個(gè)例子:當(dāng)在程序中生成對象時(shí),正常對象會(huì)在年輕代中分配空間,如果是過大的對象也可能會(huì)直接在年老代生成(據(jù)觀測在運(yùn)行某程序時(shí)候每次會(huì)生成一個(gè)十兆的空間用收發(fā)消息,這部分內(nèi)存就會(huì)直接在年老代分配)。年輕代在空間被分配完的時(shí)候就會(huì)發(fā)起內(nèi)存回收,大部分內(nèi)存會(huì)被回收,一部分幸存的內(nèi)存會(huì)被拷貝至Survivor的from區(qū),經(jīng)過多次回收以后如果from區(qū)內(nèi)存也分配完畢,就會(huì)也發(fā)生內(nèi)存回收然后將剩余的對象拷貝至to區(qū)。等到to區(qū)也滿的時(shí)候,就會(huì)再次發(fā)生內(nèi)存回收然后把幸存的對象拷貝至年老區(qū)。
通常我們說的JVM內(nèi)存回收總是在指堆內(nèi)存回收,確實(shí)只有堆中的內(nèi)容是動(dòng)態(tài)申請分配的,所以以上對象的年輕代和年老代都是指的JVM的Heap空間,而持久代則是值指MethodArea,不屬于Heap。
java堆結(jié)構(gòu)和垃圾回收
圖(二)
Direct Momery 嚴(yán)格意義來說也算堆,它是一塊物理內(nèi)存、可以分為操作系統(tǒng)內(nèi)存、是比較快的、不會(huì)走JVM 在java里邊實(shí)現(xiàn)了內(nèi)存映射、這樣速度更快
CodeCache 放一些字節(jié)碼、類的信息會(huì)放在里邊
Permanent Generation space 方法區(qū)、嚴(yán)格意義來說也屬于堆
Eden Space 區(qū)
Survivor Space區(qū)
Tenured Generation Old區(qū)(年老代)
JVM GC 管理
調(diào)優(yōu)大部分調(diào)優(yōu)的是怎么回收,Minor GC 回收Eden Space和 Survivor Space , Full GC回收所有區(qū)域
不管什么GC,回收過程中會(huì)出現(xiàn)暫停、回收過程中用戶線程是不會(huì)工作的、這樣就造成程序卡了 這是無法改變不了的事實(shí)、避免不了、不過可以優(yōu)化暫停時(shí)間的長短
原則上不能出現(xiàn)Full GC 、所有區(qū)域都要跑一遍 、出現(xiàn)Full GC 應(yīng)用就不可用
Jvm 堆配置參數(shù)
1、-Xms初始堆大小
默認(rèn)物理內(nèi)存的64/1(<1GB),建議小于1G、可根據(jù)應(yīng)用業(yè)務(wù)調(diào)節(jié)
2、-Xmx最大堆大小
默認(rèn)物理內(nèi)存的4/1(<1GB)、建議小于1G、實(shí)際中建議不大于4GB(否則會(huì)出現(xiàn)很多問題)
3、一般建議設(shè)置 -Xms= -Xmx
好處是避免每次在gc后、調(diào)整堆的大小、減少系統(tǒng)內(nèi)存分配開銷
4、整個(gè)堆大小=年輕代大小+年老代大小+持久代大小(Permanent Generation space區(qū)、也會(huì)被Full GC回收)
jvm新生代(young generation
圖(三)
1、新生代=1個(gè)eden區(qū)和2個(gè)Survivor區(qū)
2、-Xmn 年輕代大小
設(shè)置年輕代大小、比如-Xmn=100m那么新生代就是100m,然后共享
3、-XX:NewRatio
年輕代(包括Eden和兩個(gè)Survivor區(qū))與年老代的比值(除去持久代)Xms=Xmx并且設(shè)置了Xmn的情況下,該參數(shù)不需要進(jìn)行設(shè)置。
4、-XX:SurvivorRatio
Eden區(qū)與Survivor區(qū)的大小比值,設(shè)置為8(默認(rèn)是8) ,則兩個(gè)Survivor區(qū)與一個(gè)Eden區(qū)的比值為2:8,一個(gè)Survivor區(qū)占整個(gè)年輕代的1/10
比如新生代=100m,設(shè)置-XX:SurvivorRatio為8,那E =80m S0 =10m S1=10m(1/10)
5、用來存放JVM剛分配的Java對象
java老年代(tenured generation)
圖(四)
1、老年代=整個(gè)堆-年輕代大小-持久代大小
年輕代就是上面講的-xmn配置的參數(shù)、持久代參數(shù)默認(rèn)是0
2、年輕代中經(jīng)過垃圾回收沒有回收掉的對象被復(fù)制到年老代。
就是這個(gè)對象收集完一次、發(fā)現(xiàn)被引用了、某個(gè)地方使用了、回收不掉才放進(jìn)去,一般是多次回收、從E區(qū)回收過程中、先進(jìn)S0或者S1、S0或者S1再回收一次、回收不掉再放到年老區(qū)
3、老年代存儲(chǔ)對象比年輕代年齡大的多,而且不乏大對象。
對互聯(lián)網(wǎng)企業(yè)來說、最常用的是"緩存"的對象比較多、緩存一般會(huì)用弱引用、但弱引用也不會(huì)輕易被回收的、除非是在整個(gè)堆的內(nèi)存不夠的情況下、防止你的內(nèi)存宕機(jī)、強(qiáng)引用是和垃圾回收機(jī)制相關(guān)的。一般的,如果一個(gè)對象可以通過一系列的強(qiáng)引用引用到,那么就 說明它是不會(huì)被垃圾回收機(jī)制(Garbage Collection)回收的,
剛才說了緩存對象一般是弱引用、有些數(shù)據(jù)丟了是沒關(guān)系的、只是提高你的系統(tǒng)性能才放到緩存里邊去、但是如果有一天內(nèi)存不夠了 、緩存占了很大一部分對象、你不回收的話、你整個(gè)系統(tǒng)都不可用了、整個(gè)服務(wù)都不能用了、如果回收掉、我可以從數(shù)據(jù)庫去取、可 能速 度慢點(diǎn)、但是我的服務(wù)可用性不會(huì)降低
比如說剛開始分配的對象 、這個(gè)對象暫定是OLD區(qū)、剛開始一部分內(nèi)存區(qū)域被緩存占據(jù)了、一般情況下對于一個(gè)緩存的設(shè)計(jì)都有初始值、對于java來說、比較通用的緩存是可以自動(dòng)伸縮的、
如圖(四)整個(gè)OLD區(qū)50M有45M是被緩存占據(jù)了、不會(huì)被回收掉、那整個(gè)OLD區(qū)只有5M可以用了 、假如E區(qū)有40M 、S0 分配10M 、S1分配也是10M 、理想情況下、經(jīng)過E區(qū)到S0、S1到老年代的大小不到1M、 那5M就夠了、不會(huì)出現(xiàn)FULL GC 、也不會(huì)出現(xiàn) 內(nèi)存溢出、一旦你的對象大于5M、比如10M的數(shù)據(jù)、 放不進(jìn)去了、就會(huì)出現(xiàn)FULL gc 、FULL gc會(huì)把整個(gè)緩存全都收掉、瞬間緩存數(shù)據(jù)就沒了、然后把10M的數(shù)據(jù)放進(jìn)去、這就是弱引用、可以理解為這是一種服務(wù)降級、如果是強(qiáng)引用那就直接掛了
4、新建的對象也有可能直接進(jìn)入老年代
4.1、大對象,可通過啟動(dòng)參數(shù)設(shè)置
-XX:PretenureSizeThreshold=1024(單位為字節(jié),默認(rèn)為0、也就是說所有的默認(rèn)都在新生代)來代表超過多大時(shí)就不再新生代分配,而是直接在老年代分配
4.2、大的數(shù)組對象,切數(shù)組中無引用外部對象。
5、老年代大小無配置參數(shù)
java持久代(perm generation)
1、持久代=整個(gè)堆-年輕代大小-老年代大小
2、-XX:PermSize 最小 -XX:MaxPermSize 最大
設(shè)置持久代的大小,一般情況推薦把-XX:PermSize設(shè)置成 -XX:MaxPermSize的值為相同的值,因?yàn)橛谰么笮〉恼{(diào)整也會(huì)導(dǎo)致堆內(nèi)存需要觸發(fā)fgc。
3、存放Class、Method元信息,其大小與項(xiàng)目的規(guī)模、類、方法的數(shù)量有關(guān)。一般設(shè)置為128M就足夠,設(shè)置原則是預(yù)留30%的空間
剛開始設(shè)置了128M、隨著程序的運(yùn)行、java有一個(gè)叫l(wèi)ib的地方放了很多類庫、這個(gè)類庫并不是所有的都加載的、只有在用的時(shí)候或者系統(tǒng)初始化的時(shí)候會(huì)加載一部分、比如已經(jīng)占了100M了、但是隨著業(yè)務(wù)的運(yùn)行會(huì)動(dòng)態(tài)去類
庫里加、把一些Class文件通過反射的方式裝進(jìn)去、這樣你的內(nèi)存不斷增大、達(dá)到128M以后就掛了、就會(huì)報(bào)方法區(qū)溢出、怎么做?調(diào)大到256M、然后監(jiān)控、超過閾值再調(diào)大、簡單方式是調(diào)大、另外JDK里邊有一個(gè)GC可以回收
如果能接受停機(jī)、就調(diào)大,簡單、快速、已解決問題為主
4、永久代的回收方式
4.1、常量池中的常量,無用的類信息,常量的回收很簡單,沒有引用了就可以被回收
比如一個(gè)常量=5 它的意義就是個(gè)值、如果回收、發(fā)現(xiàn)它沒被引用就被回收了
4.2、對于無用的類進(jìn)行回收,必須保證3點(diǎn):
類跟常量不一樣、一個(gè)類里邊可能有好多東西、比如這個(gè)類引用那個(gè)類、
類的所有實(shí)例都已經(jīng)被回收
加載類的ClassLoader已經(jīng)被回收
類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)
jvm垃圾收集算法
1、引用計(jì)數(shù)算法
每個(gè)對象有一個(gè)引用計(jì)數(shù)屬性,新增一個(gè)引用時(shí)計(jì)數(shù)加1,引用釋放時(shí)計(jì)數(shù)減1,計(jì)數(shù)為0時(shí)可以回收。此方法簡單,無法解決對象相互循環(huán)引用的問題。還有一個(gè)問題是如何解決精準(zhǔn)計(jì)數(shù)。
這種方法現(xiàn)在已經(jīng)不用了
2、根搜索算法
從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈相連時(shí),則證明此對象是不可用的。不可達(dá)對象。
在java語言中,GC Roots包括:
虛擬機(jī)棧中引用的對象。
方法區(qū)中類靜態(tài)屬性實(shí)體引用的對象。
方法區(qū)中常量引用的對象。
本地方法棧中JNI引用的對象。
jvm垃圾回收算法
1、復(fù)制算法(Copying)
- 復(fù)制算法采用從根集合掃描,并將存活對象復(fù)制到一塊新的,沒有使用過的空間中,這種算法當(dāng)控件存活的對象比較少時(shí),極為高效,但是帶來的成本是需要一塊內(nèi)存交換空間用于進(jìn)行對象的移動(dòng)。
- 此算法用于新生代內(nèi)存回收,從E區(qū)回收到S0或者S1
從根集合掃描、就是剛才說的GC-Roots 收集算法、從它開始查你的引用、如果沒有被引用、開始執(zhí)行算法、并將存活對象復(fù)制到一塊新的、(S0或者S1)
2、標(biāo)記清除算法
標(biāo)記-清除算法采用從根集合進(jìn)行掃描,對存活的對象標(biāo)記,標(biāo)記完畢后,再掃描整個(gè)空間中未被標(biāo)記的對象,進(jìn)行回收,如圖所示。
標(biāo)記-清除算法不需要進(jìn)行對象的移動(dòng),并且僅對不存活的對象進(jìn)行處理,在存活對象比較多的情況下極為高效,但由于標(biāo)記-清除算法直接回收不存活的對象,因此會(huì)造成內(nèi)存碎片!
適合老生代去回收
標(biāo)記-整理算法采用標(biāo)記-清除算法一樣的方式進(jìn)行對象的標(biāo)記,但在清除時(shí)不同,在回收不存活的對象占用的空間后,會(huì)將所有的存活對象往左端空閑空間移動(dòng),并更新對應(yīng)的指針。
標(biāo)記-整理算法是在標(biāo)記清除算法的基礎(chǔ)上,又進(jìn)行了對象的移動(dòng),因此成本更高,但是卻解決了內(nèi)存碎片的問題。
名詞解釋
1、串行回收
gc單線程內(nèi)存回收、會(huì)暫停使有用戶線程
2、并行回收
收集是指多個(gè)GC線程并行工作,但此時(shí)用戶線程是暫停的;所以,Seral是串行的,Paralle收集器是并行的,而CMS收集器是并發(fā)的。
3、并發(fā)回收
是指用戶線程與GC線程同時(shí)執(zhí)行(不一定是并行,可能交替,但總體上是在同時(shí)執(zhí)行的),不需要停頓用戶線程(其實(shí)在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個(gè)CPU上執(zhí)行)
串行回收要區(qū)分好并行回收和并發(fā)回收的區(qū)別,這地方非常關(guān)鍵、在選擇GC的過程中根據(jù)應(yīng)用場景來選擇
JVM常見垃圾回收器
上圖是HotSpot里的收集器,中間的橫線表示分代,有連線表示可以組合使用。
年輕代區(qū)域有
Serial 串行
ParNew 并發(fā)
Parallel Scavenge 并行
年老代區(qū)域有
CMS
Serial Old
Parallel Old
G1目前還不成熟 、適合年輕代和年老代
Serial 回收器(串行回收器)

是一個(gè)單線程的收集器,只能使用一個(gè)CPU或一條線程區(qū)完成垃圾收集;在進(jìn)行垃圾收集時(shí),必須暫停所有其它工作線程,直到收集完成。
缺點(diǎn):Stop-The-World
優(yōu)勢:簡單。對于單CPU的情況,由于沒有多線程交互開銷,反而可以更高效。是Client模式下默認(rèn)的新生代收集器。
新生代Serial回收器
1、通過-XX:+UseSerialGC來開啟
Serial New+Serial Old的收集器組合進(jìn)行內(nèi)存回收
2、使用復(fù)制算法。
3、獨(dú)占式的垃圾回收。
一個(gè)線程進(jìn)行GC,串行。其它工作線程暫停。
老年代Serial回收器
1、-XX:UseSerialGC來開啟
Serial New+Serial Old的收集器組合進(jìn)行內(nèi)存回收
2、使用標(biāo)記-壓縮算法
3、串行的、獨(dú)占式的垃圾回收器。
因?yàn)閮?nèi)存比較大的原因,回收比新生代慢
ParNew回收器(并行回收器)

并行回收器也是獨(dú)占式的回收器,在收集過程中,應(yīng)用程序會(huì)全部暫停。但由于并行回收器使用多線程進(jìn)行垃圾回收,因此,在并發(fā)能力比較強(qiáng)的CPU上,它產(chǎn)生的停頓時(shí)間要短
于串行回收器,而在單CPU或者并發(fā)能力較弱的系統(tǒng)中,并行回收器的效果不會(huì)比串行回收器好,由于多線程的壓力,它的實(shí)際表現(xiàn)很可能比串行回收器差。
新生代ParNew回收器
1、-XX:+UseParNewGC開啟
新生代使用并行回收收集器,老年代使用串行收集器
2、-XX:ParallelGCThreads 指定線程數(shù)
默認(rèn)最好與CPU數(shù)理相當(dāng),避免過多的線程數(shù)影響垃圾收集性能
3、使用復(fù)制算法。
4、并行的、獨(dú)占式的垃圾回收器。
新生代Parallel Scavenge回收器
1、吞吐量優(yōu)先回收器
關(guān)注CPU吞吐量,即運(yùn)行用戶代碼的時(shí)間/總時(shí)間。比如:JVM運(yùn)行100分鐘,其中運(yùn)行用戶代碼99分鐘,垃圾回收1分鐘。則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運(yùn)行后臺(tái)運(yùn)算
2、-XX:+UseParallelGC開啟
使用Parallel Scavenge+Serial Old收集器組合回收垃圾,這也是Server模式下的默認(rèn)值
3、-XX:GCTimeRation
來設(shè)置用戶執(zhí)行時(shí)間占總時(shí)間的比例,默認(rèn)99,即1%的時(shí)間用來進(jìn)行垃圾回收
4、-XX:MaxGCPauseMillis
設(shè)置GC的最大停頓時(shí)間
5、使用復(fù)制算法
老生代Parallel Old回收器
1、-XX:+UseParallelOldGC開啟
使用Parallel Scavenge +Parallel Old組合收集器進(jìn)行收集
2、使用標(biāo)記整理算法。
3、并行的、獨(dú)占式的垃圾回收器。
CMS(并發(fā)標(biāo)記清除)回收器
運(yùn)作過程分為4個(gè)階段:
初始標(biāo)記(CMS inital mark):值標(biāo)記GC Roots能直接關(guān)聯(lián)到的對象。
并發(fā)標(biāo)記(CMS concurrent mark):進(jìn)行GC RootsTracing的過程。
重新標(biāo)記(CMS remark):修正并發(fā)標(biāo)記期間用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記發(fā)生改變的那一部分對象的標(biāo)記.
并發(fā)清除(CMS concurrent sweep):
其中標(biāo)記和重新標(biāo)記兩個(gè)階段仍然需要Stop-The-World,整個(gè)過程中耗時(shí)最長的并發(fā)標(biāo)記和并發(fā)清除過程中收集器都可以和用戶線程一起工作
CMS(并發(fā)標(biāo)記清除)回收器
1、標(biāo)記-清除算法
同時(shí)它又是一個(gè)使用多線程并發(fā)回收的垃圾收集器
2、-XX:ParalleCMSThreads
手工設(shè)定CMS的線程數(shù)量,CMS默認(rèn)啟動(dòng)的線程數(shù)是(ParallelGCTherads+3)+3/4)
這是它的公式,一般情況下、對于IO密集型的 cpu的核數(shù)乘以2+1 ,CPU密集型的一般CPU的核數(shù)+1
3、-XX+UseConcMarkSweepGC開啟
使用ParNew+CMS+Serial Old的收集器組合進(jìn)行內(nèi)存回收,Serial Old作為CMS出現(xiàn)“Concurrent Mode Failure” 失敗后的后備收集器使用.
失敗以后就會(huì)觸發(fā)Full GC 、位了避免這種情況發(fā)生、就要對它進(jìn)行配置、觸發(fā)Full GC有兩種情況、promotion failed和concurrent mode failure
對于采用CMS進(jìn)行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure兩種狀況,當(dāng)這兩種狀況出現(xiàn)時(shí)可能
會(huì)觸發(fā)Full GC。
promotion failed是在進(jìn)行Minor GC時(shí),survivor space放不下、對象只能放入老年代,而此時(shí)老年代也放不下造成的;concurrent mode failure是在
執(zhí)行CMS GC的過程中同時(shí)有對象要放入老年代,而此時(shí)老年代空間不足造成的(有時(shí)候“空間不足”是CMS GC時(shí)當(dāng)前的浮動(dòng)垃圾過多導(dǎo)致暫時(shí)性的空間不足觸發(fā)Full GC)。
對應(yīng)措施為:增大survivor space、老年代空間或調(diào)低觸發(fā)并發(fā)GC的比率。
4、
-XX:CMSInitiatingOccupancyFraction
設(shè)置CMS收集器在老年代空間被使用多少后觸發(fā)垃圾回收器,默認(rèn)值為68%,僅在CMS收集器時(shí)有效,
-XX:CMSInitiatingOccupancyFraction=70
(一般情況為70%,設(shè)太高了可能會(huì)出現(xiàn)失敗,設(shè)太低了、頻繁, 只能去找一個(gè)比值、可以分析GC log、看是否符合你的要求 )
5、-XX:+
UseCMSCompactAtFullCollection
由于CMS收集器會(huì)產(chǎn)生碎片,此參數(shù)設(shè)置在垃圾收集器后是否需要一次內(nèi)存碎片整理過程,僅在CMS收集器時(shí)有效
6、-XX:+CMSFullGCBeforeCompaction
設(shè)置CMS收集器在進(jìn)行若干次垃圾收集后再進(jìn)行一次內(nèi)存碎片整理過程,通常與
UseCMSCompactAtFullCollection參數(shù)一起使用
7、
-XX:CMSInitiatingPermOccupancyFraction
設(shè)置Perm Gen使用到達(dá)多少比率時(shí)觸發(fā),默認(rèn)92%