Java學習總結 自動內存管理機制
在學習Java的時候,我們通常會將其與c++進行對比,Java在c++的基礎上作了許多改進,摒棄了c++中很多很少使用、難以理解的且容易混淆的特性,例如頭文件、指針、運算符重載、多繼承等等。Java與c++相比有很多不同之處,其中就包括自動內存管理機制。在c++中,對于new分配的內存最終都需要使用對應的delete進行釋放。而對于Java來說,Java虛擬機的自動內存管理機制在內存管理方面幫我們作了很多工作,避免了很多內存方面的問題。本文主要簡單總結一下自動內存管理方面的內容,如有錯誤之處,還請指出,共同學習。
一、內存分配 1.JVM體系結構 2.運行時數據區域 3.內存分配 二、內存回收 1.垃圾收集算法 2.垃圾收集器 三、相關參考
一、內存分配
1.JVM體系結構
在了解自動內存管理的內存分配之前,我們先看下JVM的體系結構。代碼編譯的結果是從本地機器碼轉變為字節碼,經過類加載器加載到虛擬機后才能執行程序。JVM的體系結構主要如下圖所示:

JVM體系結構
2.運行時數據區域
在上圖中我們可以清楚地看到,JVM在執行Java程序的過程中會把它管理的內存劃分為若干個不同的數據區域,分別是程序計數器、Java虛擬機棧、本地方法棧、方法區(包括運行時常量池)、堆。下面逐一介紹。
- 程序計數器
線程私有,不會出現OOM,是一塊較小的內存空間,作用可以看作是當前線程所執行的字節碼的行號指示器,因為JVM的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,為了線程切換后能恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器,各條線程間的計數器互不影響,獨立存儲。 - 虛擬機棧
線程私有,每創建一個線程,虛擬機就會為這個線程創建一個虛擬機棧,虛擬機棧表示Java方法執行的內存模型,每調用一個方法,就會生成一個棧幀(Stack Frame)用于存儲方法的本地變量表、操作棧、方法出口等信息,當這個方法執行完后,就會彈出相應的棧幀。這部分區域,如果請求的棧的深度過大,虛擬機可能會拋出StackOverflowError異常,如果虛擬機的實現中允許虛擬機棧動態擴展,當內存不足以擴展棧的時候,會拋OutOfMemoryError異常。 - 本地方法棧 ,
線程私有,本地方法棧與虛擬機棧類似,只是在執行本地方法時使用。 - 堆
線程共享,幾乎所有的對象實例以及數組都是在這個區域進行分配,這里也是垃圾回收的主要區域就是這里(還可能有方法區)。Java堆可以處于物理上不連續的內存空間,只要邏輯上連續即可。從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。從內存回收的角度看,Java堆中可細分為新生代和老年代,再細致點有Eden空間、From Survivor空間、To Survivor空間等。如果在堆中沒有內存完成實例分配,并且堆也無法再擴展時,就會拋出OOM異常。
堆內存
- 方法區
線程共享,用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等信息。當方法區無法滿足內存分配需求時,會拋出OOM異常。 運行時常量池 是方法區的一部分,主要用來存儲編譯時生成的字面量和符號引用。
下面這張運行時數據區域圖可能更形象一點。

3.內存分配
大多數情況下,對象在新生代Eden區中分配,當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次新生代GC(Minor GC)。
在設置虛擬機參數的時候,-Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=8的含義是-Xmx與-Xms相等限制了堆大小為20MB,-Xmn表示新生代大小為10MB,剩下的10MB分配給老年代,-XX:SurvivorRatio=8表示新生代中Eden區與一個Survivor區空間比例大小為8:1,所以Eden區大小為8192K,一個Survivor區大小為1024K,新生代總可用空間為9216K。
大對象直接進入老年代,大對象是指需要連續內存空間的Java對象,比如很長的字符串及數組。長期存活的對象將進入老年代。
有道題是這樣的:

問題
這道題是自己在之前遇到過的,題目本身表達有點歧義,答案是堆和字符串常量池中,當new String("abc")時,其實會先在字符串常量區生成一個abc的對象,然后new String()時會在堆中分配空間,然后此時會把字符串常量區中abc復制一個給堆中的String,故abc應該在堆中和字符串常量區。
二、內存回收
垃圾收集器在對堆進行回收前,首先需要判斷對象是否存活,即是否可能再被使用。這里就要提到一個引用計數算法,每當一個對象被引用時,計數器就加1,引用失效時就減1,當計數器為0時就不可能被使用了,這是一個簡單的判斷對象是否存活的算法,但是Java中并沒有選用,主要是因為它很難解決對象間相互循環引用的問題。
Java中判斷對象是否存活,使用的是根搜索算法,基本思路就是通過一系列的名為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連,這個對象就是不可用的。
1.垃圾收集算法
在確定哪些內存需要回收后,接下來就是怎么回收的問題了,也就是垃圾收集算法,常用的幾種如下:標記-清除算法、復制算法、標記-整理算法以及分代回收算法。
- 標記-清除算法
這是最基礎的收集算法,首先標記出所有需要回收的對象,標記完成后統一回收掉所有被標記的對象。缺點有兩個,一個是效率問題,標記和清除過程的效率不高,另一個是空間問題,標記清除后會產生大量的不連續的內存碎片。 - 復制算法
這個算法主要是為了解決效率問題,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當一塊的內存用完時,將活著的對象復制到另外一塊上面,然后把已經使用過的內存空間清理掉。優點是每次都是對其中一塊內存進行回收,不用考慮內存碎片等情況,實現簡單,運行高效,缺點是內存縮小為原來的一半。 - 標記-整理算法
復制收集算法在對象存活率較高時就要執行較多的復制操作,效率會變低,標記整理算法首先標記出所有需要回收的對象,之后讓所有存活的對象都向一端移動,然后清理掉端邊界以外的內存。 - 分代收集算法
根據對象的存活周期的不同將內存劃分為幾塊。一般是把Java堆分為新生代和老年代,根據各個年代的特點采用適當地收集算法。在新生代中,每次垃圾收集時都有大量對象死去,只有少量存活,就選用復制算法,老年代中因為對象存活率高、沒有額外空間進行 分配擔保,就采用標記-清理或者標記-整理回收。
2.垃圾收集器
垃圾收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現,下面介紹幾種常見的垃圾收集器。
- Serial收集器
最基本、歷史最悠久的收集器,在JDK 1.3.1之前是新生代收集的唯一選擇,它是一個單線程收集器,在工作時必須暫停其他所有的工作線程。 - ParNew收集器
Serial收集器的多線程版本,其它方面基本與Serial一致。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。 - Parallel Scavenge 收集器
吞吐量優先的垃圾回收器,作用在新生代,使用復制算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間。其它收集器主要盡可能地縮短垃圾收集時用戶線程的停頓時間,停頓時間短適合需要與用戶交互的程序,而高吞吐量則可以最高效率地利用CPU時間,盡快地完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。 - Serial Old收集器
Serial收集器的老年代版本,單線程收集器,使用標記-整理算法。 - Parallel Old收集器
Parallel Scavenge 收集器的老年代版本,使用標記-整理算法,多線程,這個收集器是JDK 1.6才開始提供的,在此之前新生代的Parallel Scavenge收集器比較尷尬,只能與Serial Old搭配,性能有待提高,Parallel Old出現后與Parallel Scavenge搭配很不錯。 - CMS(Concurrent Mark Sweep)收集器
致力于獲取最短回收停頓時間(即縮短垃圾回收的時間),使用標記清除算法,多線程,優點是并發收集(用戶線程可以和GC線程同時工作),停頓小。
三、相關參考
1、《深入理解Java虛擬機》 周志明
2、JVM內幕:Java虛擬機詳解 http://www.importnew.com/17770.html
來自:http://www.jianshu.com/p/2f2f03d29de5