一、Java虛擬機(1)

一、Java虛擬機

1、Java內存區域

簡單說下Javad內存區域劃分,如圖所示:

1.1、運行時數據區域(五大區域)

Java虛擬機在執行Java程序的過程中會把它管理的內存劃分成若干個不同的數據區域。

1.1.1、 程序計數器(Program Counter Register)

程序計數器是一塊較小的內存空間,可以看作是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完。另外,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

1.1.2、Java虛擬機棧(Java Virtual Machine Stacks)

與程序計數器一樣,Java虛擬機棧也是線程私有的,它的生命週期和線程相同,描述的是Java方法執行的內存模型。每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用直至執行完成的過程就對應着一個棧幀在虛擬機中的入棧到出棧的過程。Java內存可以粗糙的區分爲堆內存(Heap)和棧內存(Stack),其中棧就是現在說的虛擬機棧,或者說是虛擬機棧中局部變量表部分,比如常用的int,char基礎類型的變量,都是存儲在該區域內。

局部變量表主要存放了編譯器可知的各種數據類型、對象引用。

1.1.3、本地方法棧(Native Method Stack)

    和虛擬機棧所發揮的作用非常相似,區別是: 虛擬機棧爲虛擬機執行Java方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。

1.1.4、JAVA堆(Java Heap)

    Java虛擬機所管理的內存中最大的一塊,Java堆是所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都在這裏分配內存。Java堆是垃圾收集器管理的主要區域,因此也被稱作GC堆(Garbage Collected Heap)。從垃圾回收的角度,由於現在收集器基本都採用分代垃圾收集算法,所以Java堆還可以細分爲:新生代和老年代。新生代再細緻一點有:Eden空間、From Survivor、To Survivor空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。堆區也是Java GC機制所管理的主要內存區域,如果在執行垃圾回收之後,仍沒有足夠的內存分配,也不能再擴展,將會拋出OutOfMemoryError:Java heap space異常。

1.1.4.1、新生代

    是用來存放新生的對象。一般佔據堆的 1/3空間。由於頻繁創建對象,所以新生代會頻繁觸發

MinorGC 進行垃圾回收。新生代又分爲 Eden 區、 ServivorFrom、 ServivorTo 三個區。

  • Eden 區

    Java新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老年代)。當 Eden區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。

  • ServivorFrom

    上一次 GC 的倖存者,作爲這一次 GC 的被掃描者。

  • ServivorTo

    保留了一次 MinorGC 過程中的倖存者。

MinorGC 的過程(複製->清空->互換)

    MinorGC 採用複製算法。

  1. eden、 servicorFrom 複製到 ServicorTo,年齡+1:首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServicorTo 區域(如果有對象的年齡以及達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果 ServicorTo 不夠位置了就放到老年區);
  2. 清空 eden、 servicorFrom:然後,清空 Eden 和 ServicorFrom 中的對象;
  3. ServicorTo 和 ServicorFrom 互換:最後, ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成爲下一次 GC 時的 ServicorFrom區。

1.1.4.2、 老年代

    主要存放應用程序中生命週期長的內存對象。

    老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。

    MajorGC 採用標記整理算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒有標記的對象。 MajorGC 的耗時比較長,因爲要掃描再回收。 MajorGC 會產生內存碎片,爲了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM( Out of Memory)異常。

1.1.4.3、永久代

    指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被

放入永久區域, 它和和存放實例的區域不同,GC 不會在主程序運行期對永久區域進行清理。所以這

也導致了永久代的區域會隨着加載的 Class 的增多而脹滿,最終拋出 OOM 異常。

    在 Java8 中, 永久代已經被移除,被一個稱爲“元數據區”(元空間)的區域所取代。元空間

的本質和永久代類似,元空間與永久代之間最大的區別在於: 元空間並不在虛擬機中,而是使用

本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。 類的元數據放入 native

memory, 字符串池和類的靜態變量放入 java 堆中, 這樣可以加載多少類的元數據就不再由

MaxPermSize 控制, 而由系統的實際可用空間來控制。

1.1.5、方法區(Method Area)

    方法區與Java堆一樣,是各個線程共享的內存區域,它用於存儲已經被虛擬機加載的類信息(即加載類時需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態變量、編譯器即時編譯的代碼等。

    HotSpot虛擬機中方法區也常被稱爲 “永久代”,本質上兩者並不等價,除HotSpot之外的多數虛擬機,並不將方法區當做永久代,HotSpot本身,也計劃取消永久代。僅僅是因爲HotSpot虛擬機設計團隊用永久代來實現方法區而已,這樣HotSpot虛擬機的垃圾收集器就可以像管理Java堆一樣管理這部分內存了。但是這並不是一個好主意,因爲這樣更容易遇到內存溢出問題。 相對而言,垃圾收集行爲在這個區域是比較出現的,但並非數據進入方法區後就“永久存在”了。

    在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮。在方法區上定義了OutOfMemoryError:PermGen space異常,在內存不足時拋出。

1.1.6、運行時常量池(Runtime Constant Pool)

    運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池信息(用於存儲編譯期就生成的字面常量、符號引用、翻譯出來的直接引用(符號引用就是編碼是用字符串表示某個變量、接口的位置,直接引用就是根據符號引用翻譯出來的地址,將在類鏈接階段完成翻譯)。

    運行時常量池除了存儲編譯期常量外,也可以存儲在運行時間產生的常量(比如String類的intern()方法,作用是String維護了一個常量池,如果調用的字符“abc”已經在常量池中,則返回池中的字符串地址,否則,新建一個常量加入池中,並返回地址,比如:“abc”.intern()==new String("abc")))。

1.1.7、直接內存(Direct Memory)

    直接內存並不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用。可以這樣理解,直接內存,就是JVM以外的機器內存,比如,你有4G的內存,JVM佔用了1G,則其餘的3G就是直接內存,JDK1.4中有一種基於通道(Channel)和緩衝區(Buffer)的I/O內存分配方式,將由C語言實現的native函數庫分配在直接內存中,然後通過一個存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣就能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆之間來回複製數據。由於直接內存收到本機器內存的限制,所以也可能出現OutOfMemoryError的異常。本機直接內存的分配不會收到Java堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

1.2、 HotSpot虛擬機對象訪問

    通過上面的介紹我們大概知道了虛擬機的內存情況,下面我們來詳細的瞭解一下HotSpot虛擬機在Java堆中對象分配、佈局和訪問的全過程。

1.2.1、對象的創建

    java是面向對象的語言,因此對象的創建無時無刻都存在。虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已被加載過、準備、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

    在類加載檢查通過後,接下來虛擬機將爲新生對象分配內存當然是在java堆中分配。對象所需的內存大小在類加載過程中就已經確定了,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。分配方式有 “指針碰撞(Bump the Pointer)” 和 “空閒列表(Free List)” 兩種,選擇那種分配方式由Java堆是否規整決定。

    指針碰撞:如果java堆是規整的,即所有用過的內存放在一邊,沒有用過的內存放在另外一邊,並且有一個指針指向分界點,在需要爲新生對象分配內存的時候,只需要移動指針畫出一塊內存分配和新生對象即可。

    空閒列表:當java堆不是規整的,意思就是使用的內存和空閒內存交錯在一起,這時候需要一張列表來記錄哪些內存可使用,在需要爲新生對象分配內存的時候,在這個列表中尋找一塊大小合適的內存分配給它即可。而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

    在爲新生對象分配內存的時候,同時還需要考慮線程安全問題。因爲在併發的情況下內存分配並不是線程安全的。有兩種方案解決這個線程安全問題:

  1. 爲分配內存空間的動作進行同步處理;
  2. 爲每個線程預先分配一小塊內存,稱爲本地線程分配緩存(Thread Local Allocation Buffer, TLAB),哪個線程需要分配內存,就在哪個線程的TLAB上分配。

    虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性。內存分配後,虛擬機需要將每個對象分配到的內存初始化爲0值(不包括對象頭),這也就是爲什麼實例字段可以不用初始化,直接爲0的原因。接下來,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的GC分代年齡等信息。這些信息存放在對象頭中,根據虛擬機當前運行狀態的不同,如是否啓用偏向鎖等,對象頭會與不同的設置方式。 new指令執行完後,所有的字段都爲0,再按照程序員的意願執行init方法後一個真正可用的對象才誕生。

1.2.2、對象的內存佈局

    在Hotspot虛擬機中,對象在內存中的佈局可以分爲3快區域:對象頭、實例數據、對齊填充。

對象頭(Header)包括2部分信息:

1:存儲對象自身的運行時數據(哈希嗎、GC分代年齡、鎖狀態標誌等等);

2:類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例。

實例數據(Instance Data):這部分是對象真正存儲的有效信息,也是在程序中所定義的各種類型的字段內容。

對齊填充(Padding):這部分不是必然存在的,也沒有什麼特別的含義,僅僅起佔位作用。 因爲Hotspot虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

1.2.3、對象的訪問定位

    建立對象就是爲了使用對象,我們的Java程序通過棧上的reference數據來操作堆上的具體對象。對象的訪問方式有虛擬機實現而定,目前主流的訪問方式有①使用句柄和②直接指針兩種:

1、使用句柄方式:會在java堆中創建一個句柄池,reference指向的這塊句柄池,句柄池中包括兩個指針,其中一個指針指向對象實例數據,另外一個指針指向對象的類型數據。

2、 直接指針訪問,那麼Java堆對像的佈局中就必須考慮如何防止訪問類型數據的相關信息,reference中存儲的直接就是對象的地址。

    這兩種對象訪問方式各有優勢。使用句柄來訪問的最大好處是reference中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而reference本身不需要修改。使用指針的方式優勢則是速度快,並且省去了一次指針定位的開銷。

1.3、 Java內存模型

    在Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽各個硬件平臺和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。那麼Java內存模型規定了哪些東西呢,它定義了程序中變量的訪問規則,往大一點說是定義了程序執行的次序。注意,爲了獲得較好的執行性能,Java內存模型並沒有限制執行引擎使用處理器的寄存器或者高速緩存來提升指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內存模型中,也會存在緩存一致性問題和指令重排序的問題。

  Java內存模型規定所有的變量都是存在主存當中(類似於前面說的物理內存),每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。

  舉個簡單的例子:在java中,執行下面這個語句:

i  = 10;

執行線程必須先在自己的工作線程中對變量i所在的緩存行進行賦值操作,然後再寫入主存當中。而不是直接將數值10寫入主存當中。

  那麼Java語言 本身對 原子性、可見性以及有序性提供了哪些保證呢?

1、原子性

  在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。

  上面一句話雖然看起來簡單,但是理解起來並不是那麼容易。看下面一個例子i:

  請分析以下哪些操作是原子性操作:

x = 10;         //語句1 y = x;         //語句2 x++;           //語句3 x = x + 1;     //語句4

咋一看,有些朋友可能會說上面的4個語句中的操作都是原子性操作。其實只有語句1是原子性操作,其他三個語句都不是原子性操作。

  語句1是直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工作內存中。

  語句2實際上包含2個操作,它先要去讀取x的值,再將x的值寫入工作內存,雖然讀取x的值以及 將x的值寫入工作內存 這2個操作都是原子性操作,但是合起來就不是原子性操作了。

  同樣的,x++和 x = x+1包括3個操作:讀取x的值,進行加1操作,寫入新的值。

   所以上面4個語句只有語句1的操作具備原子性。

  也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)纔是原子操作。

  不過這裏有一點需要注意:在32位平臺下,對64位數據的讀取和賦值是需要通過兩個操作來完成的,不能保證其原子性。但是好像在最新的JDK中,JVM已經保證對64位數據的讀取和賦值也是原子性操作了。

  從上面可以看出,Java內存模型只保證了基本讀取和賦值是原子性操作,如果要實現更大範圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。

2、可見性

  對於可見性,Java提供了volatile關鍵字來保證可見性。

  當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。

  而普通的共享變量不能保證可見性,因爲普通共享變量被修改之後,什麼時候被寫入主存是不確定的,當其他線程去讀取時,此時內存中可能還是原來的舊值,因此無法保證可見性。

  另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。

3、有序性

  在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。

  在Java裏面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。

  另外,Java內存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱爲 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。

  下面就來具體介紹下happens-before原則(先行發生原則):

  • 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
  • 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
  • volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作
  • 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
  • 線程啓動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
  • 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  • 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
  • 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始

這8條原則摘自《深入理解Java虛擬機》。這8條規則中,前4條規則是比較重要的,後4條規則都是顯而易見的。

  下面我們來解釋一下前4條規則:

  對於程序次序規則來說,我的理解就是一段程序代碼的執行在單個線程中看起來是有序的。注意,雖然這條規則中提到“書寫在前面的操作先行發生於書寫在後面的操作”,這個應該是程序看起來執行的順序是按照代碼順序執行的,因爲虛擬機可能會對程序代碼進行指令重排序。雖然進行重排序,但是最終執行的結果是與程序順序執行的結果一致的,它只會對不存在數據依賴性的指令進行重排序。因此,在單個線程中,程序執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,但無法保證程序在多線程中執行的正確性。

  第二條規則也比較容易理解,也就是說無論在單線程中還是多線程中,同一個鎖如果出於被鎖定的狀態,那麼必須先對鎖進行了釋放操作,後面才能繼續進行lock操作。

  第三條規則是一條比較重要的規則,也是後文將要重點講述的內容。直觀地解釋就是,如果一個線程先去寫一個變量,然後一個線程去進行讀取,那麼寫入操作肯定會先行發生於讀操作。

  第四條規則實際上就是體現happens-before原則具備傳遞性。

1.4、內存溢出或泄露

內存溢出的方式,大致有以下幾種:

  1. 棧溢出(StackOverflowError)
  2. 堆溢出(OutOfMemoryError:Java heap space)
  3. 永久代溢出(OutOfMemoryError: PermGen space)
  4. 直接內存溢出

1、棧溢出

    -Xoss參數設置本地方法棧大小 -Xss 參數設置棧容量。

    -Xoss參數是否有效,取決於jvm採用了哪種虛擬機,譬如如果採用HotSpot虛擬機,-Xoss參數(無效),這樣虛擬機棧和本地方法棧通過棧容量控制。附:當前大部分的虛擬機棧都是可動態擴展的。

關於虛擬機棧和本地方法棧,在java虛擬機規範中描述了兩種異常: 

  • 線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。
  •  虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

    在單個線程下,無論是由於棧幀太大還是虛擬機容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError異常。 如果不限於單線程,在這種情況下,爲每個線程的棧分配的內存越大,反而越容易產生內存溢出異常。

    如果建立過多線程導致內存溢出,在不能減少線程數或者更換64位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。

1)、StackOverflowError實例

/** * 循環調用對象引用的方式實現棧溢出 **/ public class StackSOFTest {     int depth = 0;     public void sofMethod(){         depth ++ ;         sofMethod();     }     public static void main(String[] args) {         StackSOFTest test = null;         try {             test = new StackSOFTest();             test.sofMethod();         } finally {             System.out.println("遞歸次數:"+test.depth);         }     } } 執行結果: 遞歸次數:982 Exception in thread "main" java.lang.StackOverflowError     at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:8)     at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9)     at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9) ……後續堆棧信息省略

2)、棧空間不足——OutOfMemberError實例 

    單線程情況下,不論是棧幀太大還是虛擬機棧容量太小,都會拋出StackOverflowError,導致單線程情境下模擬棧內存溢出不是很容易,循環調用new A()實現可以產生內存溢出異常。

如何讓虛擬機棧快速內存溢出呢?比如ArrayList,當擴容量(newCapacity)大於ArrayList數組定義的最大值後會調用hugeCapacity來進行判斷。如果minCapacity已經大於Integer的最大值(溢出爲負數)那麼拋出OutOfMemoryError(內存溢出)否則的話根據與MAX_ARRAY_SIZE的比較情況確定是返回Integer最大值還是MAX_ARRAY_SIZE。這邊也可以看到ArrayList允許的最大容量就是Integer的最大值(-2的31次方~2的31次方減1)。

2、堆內存溢出

java堆用於存儲對象實例,只要不斷地創建對象,並且保證gc roots到對象之間有可達路徑來避免垃圾回收機制來清楚這些對象,那麼在 對象到達最大堆的容量限制後就會產生內存溢出溢出。

異常:java.lang.OutOfMemoryError: java heap space

要解決這個區域的異常,首先要區分是出現了內存泄露(Memory Leak)還是內存溢出(Memory OverFlow)。 解決方式:如果是內存泄露,通過工具(eclipse memory analyzer)查看泄露對象到gc roots的引用鏈。於是就能找到泄露對象是通過怎樣的路徑與gc roots相關聯 並導致垃圾回收器無法自動回收它們的。掌握了泄露對象的類型信息及gc roots引用鏈的信息,就可以準確的找出泄露代碼的位置。 如果不存在泄露,換句話說,就是內存中的對象確實都還必須存活着,那就應當檢查虛擬機的堆參數(-Xmx與-Xms)與機器物理內存是否還可以調大,從代碼上檢查 是否存在某些對象生命週期過長,持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。

/** * 堆溢出 VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public static void main(String[] args) { List<byte[]> list = new ArrayList<>(); int i=0; while(true){ list.add(new byte[5*1024*1024]); System.out.println("分配次數:"+(++i)); } }

3、永久代溢出(OutOfMemoryError: PermGen space)

運行時常量池是方法區的一部分。 從JDK1.7開始逐步“去永久代”,我們這裏討論1.6版本,在1.6版本中,由於常量池分配在永久代內,我們可以 通過-XX:PermSeize和-XX:MaxPermSeize限制方法區大小,從而間接限制其中常量池的容量。

異常:java.lang.OutOfMemoryError: PermGen space

方法區用於存放Class的相關信息,如類名,訪問修飾符,常量池,字段描述,方法描述等。 方法區異常是一種常見的內存溢出異常,一個類要被垃圾收集器回收掉,判定條件是比較苛刻的。在經常動態生成大量Class的應用中,需要特別注意類的回收情況。

永久代溢出可以分爲兩種情況,第一種是常量池溢出,第二種是方法區溢出。

1)、永久代溢出——常量池溢出 

要模擬常量池溢出,可以使用String對象的intern()方法。如果常量池包含一個此String對象的字符串,就返回代表這個字符串的String對象,否則將String對象包含的字符串添加到常量池中。

public class ConstantPoolOOMTest { /** * VM Args:-XX:PermSize=10m -XX:MaxPermSize=10m * @param args */ public static void main(String[] args) { List<String> list = new ArrayList<>(); int i=1; try { while(true){ list.add(UUID.randomUUID().toString().intern()); i++; } } finally { System.out.println("運行次數:"+i); } } }

因爲在JDK1.7中,當常量池中沒有該字符串時,JDK7的intern()方法的實現不再是在常量池中創建與此String內容相同的字符串,而改爲在常量池中記錄Java Heap中首次出現的該字符串的引用,並返回該引用。 

簡單來說,就是對象實際存儲在堆上面,所以,讓上面的代碼一直執行下去,最終會產生堆內存溢出。 下面我將堆內存設置爲:-Xms5m -Xmx5m,執行上面的代碼,運行結果如下:

運行次數:58162 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.lang.Long.toUnsignedString(Unknown Source) at java.lang.Long.toHexString(Unknown Source) at java.util.UUID.digits(Unknown Source) at java.util.UUID.toString(Unknown Source) at com.ghs.test.ConstantPoolOOMTest.main(ConstantPoolOOMTest.java:18)

2)、永久代溢出——方法區溢出 

方法區存放Class的相關信息,下面藉助CGLib直接操作字節碼,生成大量的動態類。

public class MethodAreaOOMTest { public static void main(String[] args) { int i=0; try { while(true){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); i++; } } finally{ System.out.println("運行次數:"+i); } } static class OOMObject{ } } 運行結果: 運行次數:56 Exception in thread "main" Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

4、直接內存溢出

異常:java.lang.OutOfMemoryError

DirectMemory容量可通過-XX:MaxDirectMemorySize,如果不指定,默認與java堆最大值(-Xmx指定)一樣。 由DirectMemory導致的內存溢出,一個明顯的特徵是在Heap Dump文件中不會看見明顯的異常,如果發現OOM之後Dump文件很小,而程序中又直接或間接使用了NIO,那就可以 考慮檢查一下是不是這方面的原因。

NIO會使用到直接內存,你可以通過NIO來模擬,在下面的例子中,跳過NIO,直接使用UnSafe來分配直接內存。

public class DirectMemoryOOMTest { /** * VM Args:-Xms20m -Xmx20m -XX:MaxDirectMemorySize=10m * @param args */ public static void main(String[] args) { int i=0; try { Field field = Unsafe.class.getDeclaredFields()[0]; field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); while(true){ unsafe.allocateMemory(1024*1024); i++; } } catch (Exception e) { e.printStackTrace(); }finally { System.out.println("分配次數:"+i); } } } 運行結果: Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at com.ghs.test.DirectMemoryOOMTest.main(DirectMemoryOOMTest.java:20) 分配次數:27953

總結: 

  • 棧內存溢出:程序所要求的棧深度過大。 
  • 堆內存溢出: 分清內存泄露還是 內存容量不足。泄露則看對象如何被 GC Root 引用,不足則通過調大-Xms,-Xmx參數。 
  • 永久代溢出:Class對象未被釋放,Class對象佔用信息過多,有過多的Class對象。 
  • 直接內存溢出:系統哪些地方會使用直接內存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章