日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

java 内存调优_JVM内存模型以及性能调优

發布時間:2025/3/15 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java 内存调优_JVM内存模型以及性能调优 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

JVM 內存模型

JVM.png

程序計數器

程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

由于Java 虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內核)只會執行一條線程中的指令。因此,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間的計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。

如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined)。

此內存區域是唯一一個在Java 虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。

虛擬機棧

虛擬機棧(Java Virtual Machine Stacks)是線程隔離的,每創建一個線程時就會對應創建一個Java棧,即每個線程都有自己獨立的虛擬機棧。這個棧中又會對應包含多個棧幀,每調用一個方法時就會往棧中創建并壓入一個棧幀,棧幀存儲局部變量表、操作棧、動態鏈接、方法出口等信息,每一個方法從調用到最終返回結果的過程,就對應一個棧幀從入棧到出棧的過程。

虛擬機棧是一個后入先出的數據結構,線程運行過程中,只有處于棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法,當前活動幀棧始終是虛擬機棧的棧頂元素。

局部變量表存放了編譯期可知的各種基本數據類型和對象引用類型。通常我們所說的“棧內存”指的就是局部變量表這一部分。

局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀分配多少內存是固定的,運行期間不會改變局部變量表的大小。

64位的long和double類型的數據會占用2個局部變量空間,其余的數據類型只占用1個。

棧的大小可以固定也可以動態擴展。

在固定大小的情況下,JVM會為每個線程的虛擬機棧分配一定的內存大小(-Xss參數),因此虛擬機棧能夠容納的棧幀數量是有限的,若棧幀不斷進棧而不出棧,最終會導致當前線程虛擬機棧的內存空間耗盡,會拋出StackOverflowError異常。

在動態擴展的情況下,當整個虛擬機棧內存耗盡,并且無法再申請到新的內存時,就會拋出OutOfMemoryError異常。

在概念模型上,典型的棧幀結構如圖所示:

JVM Stack.jpg

棧特點

是一種運算受限的線性表。其限制是僅允許在表的一端進行插入和刪除運算。這一端被稱為棧頂,相對地,把 另一端稱為棧底。其特性是先進后出;

棧是線程私有的,生命周期跟線程相同,當創建一個線程時,同時會創建一個棧,棧的大小和深度都是固定的;

方法參數列表中的變量,方法體中的基本數據類型的變量和引用數據類型的引用都存放在棧中,成員變量和對象本身不存放在棧中。運行時,成員函數的局部變量引用也存放在棧中;

棧的變量隨著變量作用域的結束而釋放,不需要jvm垃圾回收機制回收;

棧不是全局共享的,每個線程創建一個棧,該線程只能訪問其對應的棧數據;

棧內存的大小是在編譯期就確定了的;

棧幀

一個棧中可以有多個棧幀,棧幀隨著方法的調用而創建,隨著方法的結束而消亡。該棧幀中存儲該方法中的變量,原則上各個棧幀之間的數據是不能共享的,但是在方法間調用時,jvm會將一方法的返回值賦值給調用它的棧幀中。每一個方法調用,就是一個壓棧的過程,每個方法的結束就是一個彈棧的過程。壓棧都將會將該棧幀置于棧頂,每個棧不會同時操作多個棧幀,只會操作棧頂,當棧頂操作結束時,會將該棧幀彈出,同時會釋放該棧幀內存,其下一個棧幀將變為棧頂。棧內存歸屬于單個線程,每個線程都會有一個棧內存,其存儲的變量只能在其所屬線程中可見,即棧內存可以理解成線程的私有內存。

棧中的優化,其一是當局部變量賦值時,會在棧空間中找其對應的值,當有該值時,將該值指向變量,當沒有該值時,創建一個該值,然后再指向該變量,例如:int a = 1, int b = 1, b = 2; 其二是棧中的變量隨著方法的調用而創建,當方法執行結束后,jvm會自動釋放內存。

棧幀的組成部分

局部變量表:

是一組變量值的存儲空間,用呀存放方法參數和局部變量,虛擬機通過索引定位的方式使用局部變量表。

操作樹棧:

常稱為操作數棧,是一個后入先出棧。方法執行中進行算術運算或者是調用其他的方法進行參數傳遞的時候是通過操作數棧進行的。在概念模型中,兩個棧幀是相互獨立的。但是大多數虛擬機的實現都會進行優化,令兩個棧幀出現一部分重疊。令下面的部分操作數棧與上面的局部變量表重疊在一塊,這樣在方法調用的時候可以共用一部分數據,無需進行額外的參數復制傳遞。

動態連接:

在說明什么是動態連接之前先看看方法的大概調用過程,首先在虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用可以看成是每個方法的間接引用,如果代表棧幀A的方法想調用代表棧幀B的方法,那么這個虛擬機的方法調用指令就會以B方法的符號引用作為參數,但是因為符號引用并不是直接指向代表B方法的內存位置,所以在調用之前還必須要將符號引用轉換為直接引用,然后通過直接引用才可以訪問到真正的方法,這時候就有一點需要注意,如果符號引用是在類加載階段或者第一次使用的時候轉化為直接應用,那么這種轉換成為靜態解析,如果是在運行期間轉換為直接引用,那么這種轉換就成為動態連接。

方法返回地址:

方法的返回分為兩種情況,一種是正常退出,退出后會根據方法的定義來決定是否要傳返回值給上層的調用者,一種是異常導致的方法結束,這種情況是不會傳返回值給上層的調用方法.不過無論是那種方式的方法結束,在退出當前方法時都會跳轉到當前方法被調用的位置,如果方法是正常退出的,則調用者的PC計數器的值就可以作為返回地址,如果是因為異常退出的,則是需要通過異常處理表來確定.在方法的的一次調用就對應著棧幀在虛擬機棧中的一次入棧出棧操作,因此方法退出時可能做的事情包括,恢復上層方法的局部變量表以及操作數棧,如果有返回值的話,就把返回值壓入到調用者棧幀的操作數棧中,還會把PC計數器的值調整為方法調用入口的下一條指令。

棧優點

棧幀內存數據共享:

棧幀之間數據不能共享,但是同一個棧幀內的數據是可以共享的,這樣設計是為了減小內存消耗,例如:int a = 1, int b= 1時,前面定義了a=1,a和1都在棧內存內,如果再定義一個b=1,此時將b放入棧內存,然后查找棧內存中是否有1,如果有則b指向1。如果再給b賦值2,則在棧內存中查找是否有2,如果沒有就在棧內存中放一個2,然后b指向2。也就是如果常量在棧內存中,就將變量指向該常量,如果沒有就在該棧內存增加一個該常量,并將變量指向該常量。

存取速度比堆要快,僅次于寄存器:

速度快之一是棧在編譯器就申請好了內存空間,所以在運行時不需要申請內存大小,節約了時間,其二是棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。其三是訪問時間,訪問堆的一個具體單元,需要兩次訪問內存,第一次得取得指針,第二次才是真正得數據,而棧只需訪問一次。

棧的缺點

存在棧的數據大小和生存期必須是確定的,缺乏靈活性。當棧在運行執行程序時,發現棧內存不夠,不會動態的去申請內存,以至于導致程序報錯,所以靈活性較差。

本地方法棧

本地方法棧(Native MethodStacks)與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java 方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native 方法服務。虛擬機規范中對本地方法棧中的方法使用的語言、使用方式與數據結構并沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(譬如Sun HotSpot 虛擬機)直接就把本地方法棧和虛擬機棧合二為一。

與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。

堆內存

堆內存

堆是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。但是隨著JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那么“絕對”了。

堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”。

堆的大小可以通過-Xms(最小值)和-Xmx(最大值)參數設置,-Xms為JVM啟動時申請的最小內存,默認為操作系統物理內存的1/64但小于1G,-Xmx為JVM可申請的最大內存,默認為物理內存的1/4但小于1G,默認當空余堆內存小于40%時,JVM會增大Heap到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列;當空余堆內存大于70%時,JVM會減小heap的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,對于運行系統,為避免在運行時頻繁調整Heap的大小,通常-Xms與-Xmx的值設成一樣。

如果從內存回收的角度看,由于現在收集器基本都是采用的分代收集算法,所以Java 堆中還可以細分為:新生代和老年代;

新生代:程序新創建的對象都是從新生代分配內存,新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱S0和S1或From和To)構成,可通過-Xmn參數來指定新生代的大小,也可以通過-XX:SurvivorRation來調整Eden Space及SurvivorSpace的大小。

老年代:用于存放經過多次新生代GC仍然存活的對象,例如緩存對象,新建的對象也有可能直接進入老年代,主要有兩種情況:1、大對象,可通過啟動參數設置-XX:PretenureSizeThreshold=1024(單位為字節,默認為0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。2、大的數組對象,且數組中無引用外部對象。

老年代所占的內存大小為-Xmx對應的值減去-Xmn對應的值。

方法區

方法區在一個jvm實例的內部,類型信息被存儲在一個稱為方法區的內存邏輯區中。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態)變量也存儲在方法區中。

簡單說方法區用來存儲類型的元數據信息,一個.class文件是類被java虛擬機使用之前的表現形式,一旦這個類要被使用,java虛擬機就會對其進行裝載、連接(驗證、準備、解析)和初始化。而裝載(后的結果就是由.class文件轉變為方法區中的一段特定的數據結構,這個數據結構會存儲如下信息:

類型信息

這個類型的全限定名

這個類型的直接超類的全限定名

這個類型是類類型還是接口類型

這個類型的訪問修飾符

任何直接超接口的全限定名的有序列表

字段信息

字段名

字段類型

字段的修飾符

方法信息

方法名

方法返回類型

方法參數的數量和類型(按照順序)

方法的修飾符

其他信息

除了常量以外的所有類(靜態)變量

一個指向ClassLoader的指針

一個指向Class對象的指針

常量池(常量數據以及對其他類型的符號引用)

JVM為每個已加載的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer,和floating point常量)和對類型,域和方法的符號引用。池中的數據項象數組項一樣,是通過索引訪問的。

每個類的這些元數據,無論是在構建這個類的實例還是調用這個類某個對象的方法,都會訪問方法區的這些元數據。

構建一個對象時,JVM會在堆中給對象分配空間,這些空間用來存儲當前對象實例屬性以及其父類的實例屬性(而這些屬性信息都是從方法區獲得),注意,這里并不是僅僅為當前對象的實例屬性分配空間,還需要給父類的實例屬性分配,到此其實我們就可以回答第一個問題了,即實例化父類的某個子類時,JVM也會同時構建父類的一個對象。從另外一個角度也可以印證這個問題:調用當前類的構造方法時,首先會調用其父類的構造方法直到Object,而構造方法的調用意味著實例的創建,所以子類實例化時,父類肯定也會被實例化。

類變量被類的所有實例共享,即使沒有類實例時你也可以訪問它。這些變量只與類相關,所以在方法區中,它們成為類數據在邏輯上的一部分。在JVM使用一個類之前,它必須在方法區中為每個non-final類變量分配空間。

方法區主要有以下幾個特點:

方法區是線程安全的。由于所有的線程都共享方法區,所以,方法區里的數據訪問必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那么只允許一個線程去裝載它,而其它線程必須等待

方法區的大小不必是固定的,JVM可根據應用需要動態調整。同時,方法區也不一定是連續的,方法區可以在一個堆(甚至是JVM自己的堆)中自由分配。

方法區也可被垃圾收集,當某個類不在被使用(不可觸及)時,JVM將卸載這個類,進行垃圾收集

可以通過-XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。

對于習慣在HotSpot 虛擬機上開發和部署程序的開發者來說,很多人愿意把方法區稱為“永久代”(PermanentGeneration),本質上兩者并不等價,僅僅是因為HotSpot 虛擬機的設計團隊選擇把GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已。對于其他虛擬機(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。

相對而言,垃圾收集行為在這個區域是比較少出現的,但并非數據進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。

堆內存和棧內存的對比

經常有人把Java 內存區分為堆內存(Heap)和棧內存(Stack),這種分法比較粗糙,Java內存區域的劃分實際上遠比這復雜。這種劃分方式的流行只能說明大多數程序員最關注的、與對象內存分配關系最密切的內存區域是這兩塊。

堆很靈活,但是不安全。對于對象,我們要動態地創建、銷毀,不能說后創建的對象沒有銷毀,先前創建的對象就不能銷毀,那樣的話我們的程序就寸步難行,所以Java中用堆來存儲對象。而一旦堆中的對象被銷毀,我們繼續引用這個對象的話,就會出現著名的 NullPointerException,這就是堆的缺點——錯誤的引用邏輯只有在運行時才會被發現。

棧不靈活,但是很嚴格,是安全的,易于管理。因為只要上面的引用沒有銷毀,下面引用就一定還在,在大部分程序中,都是先定義的變量、引用先進棧,后定義的后進棧,同時,區塊內部的變量、引用在進入區塊時壓棧,區塊結束時出棧,理解了這種機制,我們就可以很方便地理解各種編程語言的作用域的概念了,同時這也是棧的優點——錯誤的引用邏輯在編譯時就可以被發現。

棧--主要存放引用和基本數據類型。

堆--用來存放 new 出來的對象實例。

內存分配過程

JVM 會試圖為相關Java對象在Eden Space中初始化一塊內存區域。

當Eden空間足夠時,內存申請結束;否則到下一步。

JVM 試圖釋放在Eden中所有不活躍的對象(這屬于1或更高級的垃圾回收)。釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區。

Survivor區被用來作為Eden及Old的中間交換區域,當Old區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區。

當Old區空間不夠時,JVM 會在Old區進行完全的垃圾收集(0級)。

完全垃圾收集后,若Survivor及Old區仍然無法存放從Eden復制過來的部分對象,導致JVM無法在Eden區為新對象創建內存區域,則出現“outofmemory”錯誤。

對象訪問

對象訪問在Java 語言中無處不在,是最普通的程序行為,但即使是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區這三個最重要內存區域之間的關聯關系。

如下面的這句代碼:

Object obj = newObject();

假設這句代碼出現在方法體中,那“Object obj”這部分的語義將會反映到Java 棧的本地變量表中,作為一個reference 類型數據出現。而“new Object()”這部分的語義將會反映到Java 堆中,形成一塊存儲了Object 類型所有實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現的對象內存布局(Object Memory Layout)的不同,這塊內存的長度是不固定的。另外,在Java 堆中還必須包含能查找到此對象類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存儲在方法區中。

由于reference 類型在Java 虛擬機規范里面只規定了一個指向對象的引用,并沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java 堆中的對象的具體位置,因此不同虛擬機實現的對象訪問方式會有所不同,主流的訪問方式有兩種:使用句柄和直接指針。

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

如圖:

JVM1.jpg

如果使用直接指針訪問方式,reference 中直接存儲的就是對象地址.

如圖:

JVM2.jpg

總結

名稱

特征

作用

配置參數

異常

程序計數器

占用內存小,線程私有,生命周期與線程相同

大致為字節碼行號指示器

虛擬機棧

線程私有,生命周期與線程相同,使用連續的內存空間

Java 方法執行的內存模型,存儲局部變量表、操作棧、動態鏈接、方法出口等信息

-Xss

StackOverflowError、OutOfMemoryError

java堆

線程共享,生命周期與虛擬機相同,可以不使用連續的內存地址

保存對象實例,所有對象實例(包括數組)都要在堆上分配

-Xms、-Xmx、-Xmn

OutOfMemoryError

方法區

線程共享,生命周期與虛擬機相同,可以不使用連續的內存地址

存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據

-XX:PermSize:16M、-XX:MaxPermSize:64M

OutOfMemoryError

運行時常量池

方法區的一部分,具有動態性

存放字面量及符號引用

JVM 垃圾回收機制

哪些內存需要回收

JVM的內存結構包括五大區域:程序計數器、虛擬機棧、本地方法棧、堆區、方法區。其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生、隨線程而滅,因此這幾個區域的內存分配和回收都具備確定性,就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨著回收了。而Java堆區和方法區則不一樣,這部分內存的分配和回收是動態的,正是垃圾收集器所需關注的部分。

垃圾收集器在對堆區和方法區進行回收前,首先要確定這些區域的對象哪些可以被回收,哪些暫時還不能回收,這就要用到判斷對象是否存活的算法,常用的算法有“引用計數算法”、“可達性分析算法”!

引用計數算法

引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數。當一個對象被創建時,就將該對象實例分配給一個變量,該變量計數設置為1。當任何其它變量被賦值為這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命周期或者被設置為一個新值時,對象實例的引用計數器減1。任何引用計數器為0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。

優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序需要不被長時間打斷的實時環境比較有利。

缺點:無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能為0。

public class ReferenceFindTest {

public static void main(String[] args) {

MyObject object1 = new MyObject();

MyObject object2 = new MyObject();

object1.object = object2;

object2.object = object1;

object1 = null;

object2 = null;

}

}

這段代碼是用來驗證引用計數算法不能檢測出循環引用。最后面兩句將object1和object2賦值為null,也就是說object1和object2指向的對象已經不可能再被訪問,但是由于它們互相引用對方,導致它們的引用計數器都不為0,那么垃圾收集器就永遠不會回收它們。

可達性分析算法

可達性分析算法是從離散數學中的圖論引入的,就是通過一系列名為 “ GC Roots ”的對象為起點,然后開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈(在圖里面稱為路徑)時,則證明此對象是不可達的。

GC5.jpeg

在Java語言中,可作為GC Roots的對象包括下面幾種:

虛擬機棧中引用的對象(棧幀中的本地變量表);

方法區中類靜態屬性引用的對象;

方法區中常量引用的對象;

本地方法棧中JNI(Native方法)引用的對象。

常用的垃圾回收算法

標記-清除(mark-and-sweep、Tracing)

這是最基礎的垃圾回收算法,之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。

標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致后續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。

具體過程如下圖所示:

GC1.jpg

標記-復制(Copying)

為了解決Mark-And-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那么Copying算法的效率將會大大降低。

在CMS垃圾收集器中,新生代里面分為一個Eden區和兩個survivor區,默認Eden與survivor區的占比是8:1:1,也就是說新生代中,內存利用的有效率為80%+10%=90%,僅有10%是浪費掉的。當然并不是每次存活的對象會低于10%,如果大于10%,那么這些對象就會通過分配擔保機制進入老年代。在經歷一次新生代GC后,后入新到來的對象如果eden區能夠容納,仍然會放在新生代中。

具體過程如下圖所示:

GC2.jpg

標記-整理(Compacting)

為了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動,然后清理掉端邊界以外的內存。

具體過程如下圖所示:

GC3.jpg

分代收集(Generation)

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據不同代的特點采取最適合的收集算法。

目前大部分垃圾收集器對于新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,但是實際中并不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象復制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過的Survivor空間。

而由于老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

我們從一個object1來說明其在分代垃圾回收算法中的回收軌跡

object1新建,出生于新生代的Eden區域。

image

minor GC,object1 還存活,移動到Fromsuvivor空間,此時還在新生代。

image

minor GC,object1 仍然存活,此時會通過復制算法,將object1移動到ToSuv區域,此時object1的年齡age+1。

image

minor GC,object1 仍然存活,此時survivor中和object1同齡的對象并沒有達到survivor的一半,所以此時通過復制算法,將fromSuv和Tosuv 區域進行互換,存活的對象被移動到了Tosuv。

image

minor GC,object1 仍然存活,此時survivor中和object1同齡的對象已經達到survivor的一半以上(toSuv的區域已經滿了),object1被移動到了老年代區域。

image

object1存活一段時間后,發現此時object1不可達GcRoots,而且此時老年代空間比率已經超過了閾值,觸發了majorGC(也可以認為是fullGC,但具體需要垃圾收集器來聯系),此時object1被回收了。fullGC會觸發 stop the world。

image

在以上的新生代中,我們有提到對象的age,對象存活于survivor狀態下,不會立即晉升為老生代對象,以避免給老生代造成過大的影響,它們必須要滿足以下條件才可以晉升:

1, minor gc 之后,存活于survivor 區域的對象的age會+1,當超過(默認)15的時候,轉移到老年代。

2, 動態對象,如果survivor空間中相同年齡所有的對象大小的綜合和大于survivor空間的一半,年級大于或等于該年級的對象就可以直接進入老年代。

JVM 性能調優

調優目標

GC優化的基本方法是:將不同的JVM配置,應用到多個相同的機器環境中,對比找到可以提高性能、減少GC時間和次數的配置。

降低進入老年代的對象數量

除了可以在 JDK7 及更高版本中使用的 G1 收集器以外,其他分代 GC 都是由 Oracle JVM 提供的。關于分代 GC,就是對象在 Eden 區被創建,經過多次Minor GC、Survivor交換后,任然存活的對象會被轉入老年代。也有一些對象由于占用內存過大,在 Eden 區被創建后會直接被傳入老年代。老年代 GC 相對來說會比新生代 GC 更耗時,因此,減少進入老年代的對象數量可以顯著降低 Full GC 的頻率。

減少FULL GC執行的時間和次數

Full GC 的執行時間比 Minor GC 要長很多,因此,如果 Full GC 時間過長(超過 1s),將可能出現超時錯誤。如果通過減小老年代內存來減少 Full GC 時間,可能會引起 OutOfMemoryError 或者導致 Full GC 的頻率升高。如果通過增加老年代內存來降低 Full GC 的頻率,Full GC 的時間可能會增長。所以你需要通過不斷的實驗對比,找到一個“合適”的值。

下面情況無需進行GC優化:

Minor GC執行時間不到50ms;

Minor GC執行不頻繁,約10秒一次;

Full GC執行時間不到1s;

Full GC執行頻率不算頻繁,不低于10分鐘1次;

JVM啟動參數

參數

說明

實例

-Xms

初始堆大小,默認物理內存的1/64

-Xms512M

-Xmx

最大堆大小,默認物理內存的1/4

-Xms2G

-Xmn

新生代內存大小,官方推薦為整個堆的3/8

-Xmn512M

-Xss

線程堆棧大小,jdk1.5及之后默認1M,之前默認256k

-Xss512k

-XX:NewRatio=n

設置新生代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4

-XX:NewRatio=3

-XX:SurvivorRatio=n

年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:8,表示Eden:Survivor=8:1:1,一個Survivor區占整個年輕代的1/8

-XX:SurvivorRatio=8

-XX:PermSize=n

永久代初始值,默認為物理內存的1/64

-XX:PermSize=128M

-XX:MaxPermSize=n

永久代最大值,默認為物理內存的1/4

-XX:MaxPermSize=256M

-verbose:class

在控制臺打印類加載信息

-

-verbose:gc

在控制臺打印垃圾回收日志

-

-XX:+PrintGC

打印GC日志,內容簡單

-

-XX:+PrintGCDetails

打印GC日志,內容詳細

-

-XX:+PrintGCDateStamps

在GC日志中添加時間戳

-

-Xloggc:filename

指定gc日志路徑

-Xloggc:/data/jvm/gc.log

-XX:+UseSerialGC

年輕代設置串行收集器Serial

-

-XX:+UseParallelGC

年輕代設置并行收集器Parallel Scavenge

-

-XX:ParallelGCThreads=n

設置Parallel Scavenge收集時使用的CPU數。并行收集線程數。

-XX:ParallelGCThreads=4

-XX:MaxGCPauseMillis=n

設置Parallel Scavenge回收的最大時間(毫秒)

-XX:MaxGCPauseMillis=100

-XX:GCTimeRatio=n

設置Parallel Scavenge垃圾回收時間占程序運行時間的百分比。公式為1/(1+n)

-XX:GCTimeRatio=19

-XX:+UseParallelOldGC

設置老年代為并行收集器ParallelOld收集器

-

-XX:+UseConcMarkSweepGC

設置老年代并發收集器CMS

-

-XX:+CMSIncrementalMode

設置CMS收集器為增量模式,適用于單CPU情況。

-

配置建議

JVM配置方面,一般情況可以先用默認配置(基本的一些初始參數可以保證一般的應用跑的比較穩定了),在測試中根據系統運行狀況(會話并發情況、會話時間等),結合GC日志、內存監控、使用的垃圾收集器等進行合理的調整。當老年代內存過小時可能引起頻繁Full GC,過大時Full GC時間會特別長。

那么JVM的配置比如新生代、老年代應該配置多大最合適呢?答案是不一定,調優就是找答案的過程。物理內存一定的情況下,新生代設置越大,老年代就越小,Full GC頻率就越高,但Full GC時間越短;相反新生代設置越小,老年代就越大,Full GC頻率就越低,但每次Full GC消耗的時間越大

發現FullGC頻繁的時候優先調查內存泄漏問題

-Xms和-Xmx的值設置成相等。堆大小默認為-Xms指定的大小,默認空閑堆內存小于40%時,JVM會擴大堆到-Xmx指定的大小;空閑堆內存大于70%時,JVM會減小堆到-Xms指定的大小。如果在Full GC后滿足不了內存需求會動態調整,這個階段比較耗費資源,所以設置成想通知,以避免每次垃圾回收完成后JVM重新分配內存。

年輕代的設置,整個JVM內存大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代后,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。

避免創建過大的對象及數組:過大的對象或數組在新生代沒有足夠空間容納時會直接進入老年代,如果是短命的大對象,會提前出發Full GC。

避免同時加載大量數據,如一次從數據庫中取出大量數據,或者一次從Excel中讀取大量記錄,可以分批讀取,用完盡快清空引用。

當集合中有對象的引用,這些對象使用完之后要盡快把集合中的引用清空,這些無用對象盡快回收避免進入老年代。

盡量避免長時間等待外部資源(數據庫、網絡、設備資源等)的情況,縮小對象的生命周期,避免進入老年代,如果不能及時返回結果可以適當采用異步處理的方式等。

調優工具

jmap (Memory Map for Java):

可以生成 java 程序的 dump 文件, 也可以查看堆內對象示例的統計信息、查看 ClassLoader 的信息以及 finalizer 隊列

命令:jmap pid

描述:查看進程的內存映像信息,類似 Solaris pmap 命令。

命令:jmap -heap pid

描述:顯示Java堆詳細信息

命令:jmap -histo:live pid

描述:顯示堆中對象的統計信息

命令:jmap -clstats pid

描述:打印類加載器信息

命令:jmap -finalizerinfo pid

描述:打印等待終結的對象信息

命令:jmap -dump:format=b,file=heapdump.phrof pid

描述:生成堆轉儲快照dump文件。

jstack:

線程跟蹤工具,用于打印指定Java進程的線程堆棧信息。

jstack -l 5524 > /opt/jstack.txt

jps (JVM process Status):

可以查看虛擬機啟動的所有進程、執行主類的全名、JVM啟動參數。

jstat (JVM Statistics Monitoring Tool):

可以查看堆內存各部分的使用量,以及加載類的數量。

命令:jstat -gc pid

描述:垃圾回收統計

命令:jstat -gccapacity pid

描述:堆內存統計

命令:jstat -gcnew pid

描述:新生代垃圾回收統計

命令:jstat -gcnewcapacity pid

描述:新生代內存統計

命令:jstat -gcold pid

描述:老年代垃圾回收統計

命令:jstat -gcoldcapacity pid

描述:老年代內存統計

命令:jstat -gcmetacapacity pid

描述:元數據空間統計

命令:jstat -gcutil pid

描述:總結垃圾回收統計

jstat -gc pid 500 10 :每500毫秒打印一次Java堆狀況(各個區的容量、使用容量、gc時間等信息),打印10次

S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT

12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838

12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838

12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838

12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838

12288.0 12800.0 0.0 0.0 642048.0 244137.2 102400.0 33034.0 60080.0 57964.0 7936.0 7472.9 11 0.898 3 1.940 2.838

名稱

描述

S0C

第一個幸存區的大小

S1C

第二個幸存區的大小

S0U

第一個幸存區的使用大小

S1U

第二個幸存區的使用大小

EC

伊甸園區的大小

EU

伊甸園區的使用大小

OC

老年代大小

OU

老年代使用大小

MC

方法區大小

MU

方法區使用大小

CCSC

壓縮類空間大小

CCSU

壓縮類空間使用大小

YGC

年輕代垃圾回收次數

YGCT

年輕代垃圾回收消耗時間

FGC

老年代垃圾回收次數

FGCT

老年代垃圾回收消耗時間

GCT

垃圾回收消耗總時間

jhat:

用來分析java堆的命令,可以將堆中的對象以html的形式顯示出來,包括對象的數量,大小等等,并支持對象查詢語言。

jinfo:

可以查看運行中jvm的全部參數,還可以設置部分參數。

jinfo pid

總結

以上是生活随笔為你收集整理的java 内存调优_JVM内存模型以及性能调优的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。