国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

JVM詳解1.Java內存模型

TANKING / 3475人閱讀

摘要:編譯參見深入理解虛擬機節走進之一自己編譯源碼內存模型運行時數據區域根據虛擬機規范的規定,的內存包括以下幾個運運行時數據區域程序計數器程序計數器是一塊較小的內存空間,他可以看作是當前線程所執行的字節碼的行號指示器。

點擊進入我的博客 1.1 基礎知識 1.1.1 一些基本概念

JDK(Java Development Kit):Java語言、Java虛擬機、Java API類庫
JRE(Java Runtime Environment):Java虛擬機、Java API類庫
JIT(Just In Time):Java虛擬機內置JIT編譯器,將字節碼編譯成本機機器代碼。
OpenJDK:OpenJDK是基于Oracle JDK基礎上的JDK的開源版本,但由于歷史原因缺少了部分(不太重要)的代碼。Sun JDK > SCSL > JRL > OpenJDK
JCP組織(Java Community Process):由Java開發者以及被授權者組成,負責維護和發展技術規范、參考實現(RI)、技術兼容包。

1.1.2 編譯JDK

參見《深入理解Java虛擬機》1.6節
走進JVM之一 自己編譯openjdk源碼

1.2 Java內存模型 1.2.1 運行時數據區域


根據Java虛擬機規范(Java SE7)的規定,JVM的內存包括以下幾個運運行時數據區域:

程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,他可以看作是當前線程所執行的字節碼的行號指示器

在虛擬機的概念模型里(僅是概念模型,各種虛擬機可能會通過一些更高效的方式去實現),字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

程序計數器是線程私有的,每條線程都有一個獨立的獨立的程序計數器,各條線程之間計數器互不影響。

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的Native方法,這個計數器值則為空(Undefined)。此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。

Java虛擬機棧

Java虛擬機棧是線程私有的,他的生命周期與線程相同。

虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame),用于包含局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用到執行完成這個過程,就對應這一個棧幀在虛擬機棧中的入棧到出棧的過程。

局部變量表存放了編譯期可知的各種基本數據類型和對象引用(reference類型,他不等同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此相關的位置)和returnAddress類型(指向了一條字節碼指令的地址) 。

其中64位長度的long和double類型會占用2個局部變量空間,其余的數據類型只會占用1個局部變量空間。局部變量表所需的內存空間在編譯期間完成內存分配。當進入一個方法時,這個方法需要在幀中分配多大的內存空間是完全確定的,在方法運行期間不會改變局部變量表的大小

在Java虛擬機規范中,對這個區域規定了兩種異常狀態:如果線程請求的棧的深度大于虛擬機允許的深度,將拋出StackOverFlowError異常(棧溢出);如果虛擬機棧可以動態擴展(現在大部分Java虛擬機都可以動態擴展,只不過Java虛擬機規范中也允許固定長度的java虛擬機棧),如果擴展時無法申請到足夠的內存空間,就會拋出OutOfMemoryError異常(沒有足夠的內存)。

本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,他們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的本地Native方法服務。

在虛擬機規范中對本地方法棧中的使用方法、語言、數據結構并沒有強制規定,因此具體的虛擬機可以自由實現它。甚至有的虛擬機(例如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一。

本地方法棧也會拋出StackOverFlowError和OutOfmMemoryError異常。

Java堆

Java堆(Java Heap)是Java虛擬機管理內存中的最大一塊。

Java堆是所有線程共享的一塊內存管理區域。

此內存區域唯一目的就是存放對象的實例,幾乎所有對象實例都在堆中分配內存。這一點在Java虛擬機規范中的描述是:所有對象實例以及數組都要在堆上分配,但是隨著JIT編譯器的發展與逃逸技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也不是變的那么“絕對”了。

Java堆是垃圾回收器管理的主要區域,因此很多時候也被稱為GC堆(Garbage Collected Heap)。

內存回收的角度來看,由于現在收集器基本都采用分代收集算法,所以Java堆中還可以細分為:新生代和年老代。再在細致一點的劃分可以分為:Eden空間、From Survivor空間、To Survivor空間等。

內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區。

不過無論如何劃分,都與存放內容無關,無論哪個區域存放的都是對象實例。進一步劃分的目的是為了更好的回收內存,或者更快的分配內存。

Java堆可以處在物理上不連續的內存空間,只要邏輯上是連續的即可。在實現上既可以實現成固定大小,也可以是可擴展的大小,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。

如果在堆中沒有內存實例完成分配,并且堆也無法在擴展時將會拋出OutOfMemoryError異常。

方法區

方法區是各個線程共享的內存區域

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

雖然Java虛擬機規范把方法區描述為堆的一部分,但是他還有個別名叫做Non-heap(非堆),目的應該是與Java堆區分開來。

根據Java虛擬機規范的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError 異常。

Java虛擬機規范對方法區的限制非常寬松,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。相對而言,垃圾收集在這個區域是比較少出現的,但并非數據進入了方法區就如永久代的名字一樣永久存在了。這區域的內存回收目標重要是針對常量池的回收和類型的卸載,一般來說這個內存區域的回收‘成績’比較難以令人滿意。尤其是類型的卸載條件非常苛刻,但是這部分的回收確實是必要的。在Sun公司的bug列表中,曾出現過的若干個嚴重的bug就是由于低版本的HotSpot虛擬機對此區域未完成回收導致的內存溢出。

注意:方法區與永久代

對于習慣在HotSpot虛擬機上開發、部署程序的開發者來說,很多人都更愿意把方法區稱為“永久代”(Permanent Generation),本質上兩者并不等價

僅僅是因為HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內存,能夠省去專門為方法區編寫內存管理代碼的工作。

對于其他虛擬機(如 BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。

對于HotSpot虛擬機,根據官方發布的路線圖信息,現在也有放棄永久代并逐步改為采用Native Memory來實現方法區的規劃了,在目前已經發布的JDK1.7的HotSpot中,已經把原本放在永久代的字符串常量池移出。

運行時常量池

(見1.2.2)

直接內存

直接內存(Direct Memory)并不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規范中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致 OutOfMemoryError 異常出現。

在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的 I/O 方式,它可以使用Native函數庫直接分配堆外內存,然后通過一個存儲在Java堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回復制數據。

顯然,本機直接內存的分配不會受到 Java 堆大小的限制。但是,既然是內存,肯定還是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置 -Xms 等參數信息,但經常忽略直接內存,使得各個內存區域總和大于物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現 OutOfMemoryError 異常。

1.2.2 常量池
Class常量池

Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用。

字面量(Literal):文本字符串(如String str = "SpiderLucas"SpiderLucas就是字面量)、八種基本類型的值(如int i = 00就是字面量)、被聲明為final的常量等;

符號引用(Symbolic References):類和方法的全限定名、字段的名稱和描述符、方法的名稱和描述符。

每個class文件都有一個class常量池。

字符串常量池

參考資料來源:Java中的常量池、徹底弄懂字符串常量池等相關問題、Java中String字符串常量池

字符串常量池中的字符串只存在一份。

字符串常量池(String Constant Pool)是存儲Java String的一塊內存區域,在JDK 6之前是放置于方法區的,在JDK 7之后放置于堆中。

在HotSpot中實現的字符串常量池功能的是一個StringTable類,它是一個Hash表,默認值大小長度是1009;這個StringTable在每個HotSpot VM的實例只有一份,被所有的類共享。字符串常量由一個一個字符組成,放在了StringTable上。

StringTable的長度:在JDK 6中,StringTable的長度是固定的,因此如果放入String Pool中的String非常多,就會造成hash沖突,導致鏈表過長,當調用String#intern()時會需要到鏈表上一個一個找,從而導致性能大幅度下降;在JDK 7中,StringTable的長度可以通過參數指定:-XX:StringTableSize=1024

字符串常量池中存放的內容:在JDK 6及之前版本中,String Pool里放的都是字符串常量;JDK 7中,由于String#intern()發生了改變,因此String Pool中也可以存放放于堆內的字符串對象的引用。

intern() 函數

在JDK 6中,intern()的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該常量;如果沒有找到,則將該字符串常量加入到字符串常量區,也就是在字符串常量區建立該常量。

在JDK 7中,intern()的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該常量,如果沒有找到,說明該字符串常量在堆中,則處理是把堆區該對象的引用加入到字符串常量池中,以后別人拿到的是該字符串常量的引用,實際存在堆中

字符串常量池案例
        String s1 = new String("Spider"); // s1 -> 堆
        // 該行代碼創建了幾個對象
        // 兩個對象(不考慮對象內部的對象):首先創建了一個字符串常量池的對象,然后創建了堆里的對象
        s1.intern(); // 字符串常量池中存在"Spider",直接返回該常量
        String s2 = "Spider"; // s2 -> 字符串字符串常量池
        System.out.println(s1 == s2); // false

        String s3 = new String("Str") + new String("ing"); // s3 -> 堆
        // 該行代碼創建了幾個對象?
        // 反編譯后的代碼:String s3 = (new StringBuilder()).append(new String("Str")).append(new String("ing")).toString();
        // 六個對象(不考慮對象內部的對象):兩個字符串常量池的對象"Str"和"ing",兩個堆的對象"Str"和"ing",一個StringBuilder,一個toString方法創建的new String對象
        s3.intern(); // 字符串常量池中沒有,在JDK 7中以后會把堆中該對象的引用放在字符串常量池中(JDK 6中創建一個jdk1.6中會在字符串常量池中建立該常量)
        String s4 = "String"; // s4 -> 堆(JDK 6:s4 -> 字符串字符串常量池)
        System.out.println(s3 == s4); // true(JDK6 false)

        String s5 = "AAA";
        String s6 = "BBB";
        String s7 = "AAABBB"; // s7 -> 字符串常量池
        String s8 = s5 + s6; // s8 -> 堆(原因就是如上字符串+的重載)
        String s9 = "AAA" + "BBB"; // JVM會對此代碼進行優化,直接創建字符串常量池
        System.out.println(s7 == s8); // false
        System.out.println(s7 == s9); // true(都指向字符串常量池)
方法區與運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。

Class常量池的內容將在類加載后進入方法區的運行時常量池中存放

Java虛擬機對Class文件每一部分(自然也包括常量池)的格式都有嚴格規定,每一個字節用于存儲哪種數據都必須符合規范的要求才會被虛擬機認可、裝載和執行,但對于運行時常量池,Java 虛擬機規范沒有做任何細節的要求,不同的提供商實現的虛擬機可以按照自己的需要來實現這個內存區域。

運行時常量池相對于Class文件常量池的另外一個重要特征是具備動態性,Java 語言并不要求常量一定只有編譯期才能產生,也就是并非預置如Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String類的intern()方法。

既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。

1.3 HotSpot中的對象 1.3.1 對象的創建
new一個對象的全部流程

從常量池中查找該類的符號引用,并且檢查該符號引用代表的類是否被加載、解析、初始化。如果類已經被加載,則跳轉至3;否則跳轉至2。

執行類的加載過程。

為新對象分配內存空間:由于對象所需要內存大小在類加載完成時可以確定,所以可以直接從Java堆中劃分一塊確定大小的內存。

把分配的內存空間都初始化為零值(不包括對象頭),如果使用TLAB則該操作可以提前至TLAB中,這是為了保證對象的字段都被初始為默認值。

執行init方法,按照程序員的意愿進行初始化。

對象分配內存空間詳解

指針碰撞:如果堆內存是規整,已經分配和為分配的內存有一個指針作為分界點,那么只需要將指針向空閑內存移動即可。

空閑列表:如果內存是不規整的,虛擬機需要維護一個列表,記錄那些內存塊是可用的。在分配的時候從足夠大的空間劃分給對象,并更新該列表。

Java堆是否規整取決于GC是否會壓縮整理,Serial、ParNew等帶Compact過程的收集器,分配算法是指針碰撞;是用CMS這種基于Mark-Sweep算法的收集器時,分配算法是空閑列表。

分配內存的并發問題

無論是指針碰撞還是空閑列表,都有可能因為并發而產生問題,解決方法有兩種:

對分配內存空間的動作進行同步處理——實際上JVM采用CAS(Compare And Swap)配上失敗重試的方式保證更新操作的原子性。

把內存分配的動作按照線程劃分在不同的空間,每個線程在Java堆中預先分配一小塊內存,成為本地緩沖內存(Tread Local Allocation Buffer,TLAB)。哪個線程需要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完了,才需要同步鎖定。可以通過-XX:+/-UseTLAB參數來設定。

CAS原理
一個CAS方法包含三個參數CAS(V,E,N)。V表示要更新的變量,E表示預期的值,N表示新值。只有當V的值等于E時,才會將V的值修改為N。如果V的值不等于E,說明已經被其他線程修改了,當前線程可以放棄此操作,也可以再次嘗試次操作直至修改成功。基于這樣的算法,CAS操作即使沒有鎖,也可以發現其他線程對當前線程的干擾(臨界區值的修改),并進行恰當的處理。
1.3.2 對象的內存布局

在HotSpot虛擬機中,對象在內存中的存儲布局可以分為3部分:對象頭(Object Header)、實例數據(Instance Data)、對齊填充(Padding)。

對象頭第一部分

對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等等,這部分數據的長度在32位和64位的虛擬機(暫不考慮開啟壓縮指針的場景)中分別為32個和64個Bits,官方稱它為“Mark Word”。

對象需要存儲的運行時數據很多,其實已經超出了32、64位所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息,原理是它會根據對象的狀態復用自己的存儲空間

例如在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用于存儲對象哈希碼(HashCode),4Bits用于存儲對象分代年齡,2Bits用于存儲鎖標志位,1Bit固定為0,在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如下表所示。??

對象頭第二部分

對象頭的第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

并不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據信息不一定要經過對象本身。

如果對象是一個數組:對象頭中還需要一塊用于記錄數組長度的數據。

實例數據

接下來實例數據部分是對象真正存儲的有效信息,也既是我們在程序代碼里面所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的都需要記錄下來。

這部分的存儲順序會受到虛擬機分配策略參數(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機默認的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。

如果 CompactFields參數值為true(默認為true),那子類之中較窄的變量也可能會插入到父類變量的空隙之中。

對齊填充

第三部分對齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說就是對象的大小必須是8字節的整數倍。對象頭正好是8字節的倍數(1倍或者2倍),因此當對象實例數據部分沒有對齊的話,就需要通過對齊填充來補全。

1.3.3 對象的訪問定位

建立對象是為了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。由于reference類型在Java虛擬機規范里面只規定了是一個指向對象的引用,并沒有定義這個引用應該通過什么種方式去定位、訪問到堆中的對象的具體位置,對象訪問方式也是取決于虛擬機實現而定的。

對象的兩種訪問定位方式

主流的訪問方式有使用句柄和直接指針兩種。

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

直接指針:Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,reference中存儲的直接就是對象地址,如下圖所示。


兩種方式比較

句柄訪問的優勢:reference中存儲的是穩定句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要被修改。

直接指針的優勢:最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象訪問的在Java中非常頻繁,因此這類開銷積小成多也是一項非常可觀的執行成本。

HotSpot虛擬機:它是使用第二種方式進行對象訪問。

但在整個軟件開發的范圍來看,各種語言、框架中使用句柄來訪問的情況也十分常見。

1.4 OOM異常分類 1.4.1 堆溢出

Java堆用于存儲對象實例,只要不斷創建對象,并且保證GC Roots到對象之間有可達路徑來避免GC,那么在對象數量到達最大堆容量限制之后便會產生堆溢出。

/**
 * VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * 1.將堆的最小值-Xms與最大值-Xmx參數設置為一樣可以避免堆自動擴展
 * 2.通過參數-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機出現內存異常時Dump當前堆內存堆轉儲快照
 * 3.快照位置默認為user.dir
 */
public class HeapOOM {
    static class OOMObject {}
    public static void main(String[] args) {
        // 保留引用,防止GC
        List list = new ArrayList<>();
        for (;;) {
            list.add(new OOMObject());
        }
    }
}
// 運行結果
// java.lang.OutOfMemoryError: Java heap space
// Dumping heap to java_pid72861.hprof ...
// Heap dump file created [27888072 bytes in 0.086 secs]
堆轉儲快照

以下是JProfiler對轉儲快照的分析


內存泄漏與內存溢出

重點:確認內存中的對象是否是必要的,也就是分清楚到底是出現了內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)

內存泄漏:是指程序在申請內存后,無法釋放已申請的內存空間,一次內存泄漏似乎不會有大的影響,但內存泄漏堆積后的后果就是內存溢出。

內存溢出:是指程序在申請內存時,沒有足夠的內存空間供其使用。內存泄漏的堆積最終會導致內存溢出。

內存泄漏的分類(按發生方式來分類)

常發性內存泄漏:發生內存泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊內存泄漏。

偶發性內存泄漏:發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對于特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測內存泄漏至關重要。

一次性內存泄漏:發生內存泄漏的代碼只會被執行一次,或者由于算法上的缺陷,導致總會有一塊僅且一塊內存發生泄漏。比如,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,所以內存泄漏只會發生一次。

隱式內存泄漏:程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這里并沒有發生內存泄漏,因為最終程序釋放了所有申請的內存。但是對于一個服務器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。

處理方式

如果是內存泄漏:需要找到泄漏對象的類型信息,和對象與GC Roots的引用鏈的信息,分析GC無法自動回收它們的原因。

如果不存在內存泄漏,即內存中的對象的確必須存活:那就應當檢查JVM的參數能否調大;從代碼上檢查是否某些對象生命周期過長、持有狀態時間過長,嘗試減少程序運行期的內存消耗。

1.4.2 棧溢出

在HotSpot虛擬機中并不區分虛擬機棧和本地方法棧,對于HotSpot來說,雖然-Xoss參數(設置本地方法棧大小)存在,但實際上是無效的。棧容量只由-Xss參數設定。關于虛擬機棧和本地方法棧,在Java虛擬機規范中描述了兩種異常:

如果線程請求的棧深度大于虛擬機所允許的最大深度,將拋出StackOverflowError異常。

如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。

這里把異常分成兩種情況,看似更加嚴謹,但卻存在著一些互相重疊的地方:當棧空間無法繼續分配時,到底是內存太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。

StackOverflowError
/**
 * VM args: -Xss256k
 * 1. 設置-Xss參數減小棧內存
 * 2. 死遞歸增大此方法棧中本地變量表的長度
 */
public class SOF {
    int stackLength = 1;

    void stackLeak() {
        stackLength++;
        stackLeak();
    }

    public static void main(String[] args) {
        SOF sof = new SOF();
        try {
            sof.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack Length:" + sof.stackLength);
            throw e;
        }
    }
}
// Stack Length:2006
// Exception in thread "main" java.lang.StackOverflowError
//      at s1.SOF.stackLeak(SOF.java:13)
//      at s1.SOF.stackLeak(SOF.java:13)
多線程導致棧OOM異常
/**
 * VM Args: -Xss20M
 * 通過不斷創建線程的方式產生OOM
 */
public class StackOOM {
    private void dontStop() {
        for (;;) {

        }
    }

    private void stackLeakByThread() {
        for (;;) {
            Thread thread = new Thread(this::dontStop);
            thread.start();
        }
    }

    public static void main(String[] args) {
        new StackOOM().stackLeakByThread();
    }
}

通過不斷創建線程的方式產生OOM異常,但是這樣產生的內存溢出異常與棧空間是否足夠大并不存在任何聯系。或者準確地說,在這種情況下,為每個線程的棧分配的內存越大,反而越容易產生內存溢出異常
原因:操作系統分配給每個進程的內存是有限制的,假設操作系統的內存為2GB,剩余的內存為2GB(操作系統限制)減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區容量),程序計數器消耗內存很小,可以忽略掉。如果虛擬機進程本身耗費的內存不計算在內,剩下的內存就由虛擬機棧和本地方法棧“瓜分”了。所以每個線程分配到的棧容量越大,可以建立的線程數量自然就越少,建立線程時就越容易把剩下的內存耗盡。
解決方法:如果是建立過多線程導致的內存溢出,在不能減少線程數或者更換64位虛擬機的情況下,就只能通過“減少內存”的手段來解決內存溢出——減少最大堆和減少棧容量來換取更多的線程

1.4.3 方法區和運行時常量池溢出

由于運行時常量池是方法區的一部分,因此這兩個區域的溢出測試就放在一起進行。方法區用于存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等,所以對于動態生成類的情況比較容易出現永久代的內存溢出。對于這些區域的測試,基本的思路是運行時產生大量的類去填滿方法區,直到溢出

/**
 * (JDK 8)VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10m
 * (JDK 7之前)VM Args: -XX:PermSize=10M -XX:MaxPermSize=10m
 */
public class MethodAreaOOM {
    static class OOMClass {}

    public static void main(final String[] args) {
        for (;;) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMClass.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invokeSuper(o, objects);
                }
            });
            enhancer.create();
        }
    }
}
//    Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
//        at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)
//        at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
//        at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
//        at com.ankeetc.commons.Main.main(Main.java:28)
方法區溢出場景

方法區溢出也是一種常見的內存溢出異常,一個類要被垃圾收集器回收掉,判定條件是比較苛刻的。在經常動態生成大量Class的應用中,需要特別注意類的回收狀況。這類場景主要有:

使用了CGLib字節碼增強,當前的很多主流框架,如Spring、Hibernate,在對類進行增強時,都會使用到CGLib這類字節碼技術,增強的類越多,就需要越大的方法區來保證動態生成的Class可以加載入內存。

大量JSP或動態產生JSP文件的應用(JSP第一次運行時需要編譯為Java類)

基于OSGi的應用(即使是同一個類文件,被不同的加載器加載也會視為不同的類)等

JVM上的動態語言(例如Groovy等)通常都會持續創建類來實現語言的動態性

1.4.4 本機直接內存溢出

下面代碼越過了DirectByteBuffer類,直接通過反射獲取Unsafe實例進行內存分配(Unsafe類的getUnsafe()方法限制了只有引導類加載器才會返回實例,也就是設計者希望只有rt.jar中的類才能使用Unsafe的功能)。因為,雖然使用DirectByteBuffer分配內存也會拋出內存溢出異常,但它拋出異常時并沒有真正向操作系統申請分配內存,而是通過計算得知內存無法分配,于是手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory()

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 * DirectMemory容量可通過-XX:MaxDirectMemorySize指定
 * 如果不指定,則默認與Java堆最大值(-Xmx指定)一樣。
 */
public class Main {

    private static final long _1024MB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws Exception {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1024MB);
        }
    }
}
//    Exception in thread "main" java.lang.OutOfMemoryError
//        at sun.misc.Unsafe.allocateMemory(Native Method)
//        at com.ankeetc.commons.Main.main(Main.java:25)
DirectMemory特征

DirectMemory導致的內存溢出,一個明顯的特征是在Heap Dump文件中不會看見明顯的異常。

如果發現OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。

1.5 不同版本的JDK
參考資料

Java8內存模型—永久代(PermGen)和元空間(Metaspace)

JDK8-廢棄永久代(PermGen)迎來元空間(Metaspace)

關于永久代和方法區

在 HotSpot VM 中 “PermGen Space” 其實指的就是方法區

“PermGen Space” 和方法區有本質的區別。前者是 JVM 規范的一種實現(HotSpot),后者是 JVM 的規范。

只有 HotSpot 才有 “PermGen Space”,而對于其他類型的虛擬機,如 JRockit、J9并沒有 “PermGen Space”。

不同版本JDK總結

JDK 7之后將字符串常量池由永久代轉移到堆中

JDK 8中, HotSpot 已經沒有 “PermGen space”這個區間了,取而代之是一個叫做 Metaspace(元空間) 的東西。

元空間的本質和永久代類似,都是對JVM規范中方法區的實現。不過元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。

-XX:MetaspaceSize:初始空間大小,達到該值就會觸發垃圾收集進行類型卸載。同時GC會對該值進行調整——如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時,適當提高該值。

-XX:MaxMetaspaceSize:最大空間,默認是沒有限制的。

-XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空間容量的百分比,減少為分配空間所導致的垃圾收集

-XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空間容量的百分比,減少為釋放空間所導致的垃圾收集

PermSizeMaxPermSize參數已移除

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72764.html

相關文章

  • JVM虛擬機詳解

    摘要:虛擬機包括一套字節碼指令集一組寄存器一個棧一個垃圾回收堆和一個存儲方法域。而使用虛擬機是實現這一特點的關鍵。虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。此內存區域是唯一一個在虛擬機規范中沒有規定任何情況的區域。 1、 什么是JVM?   JVM是Java Virtual Machine(Java虛擬機)的縮寫,JVM是一種用于計算設備的規范,它是一個虛構出來的計算機,...

    rottengeek 評論0 收藏0
  • Java面試 32個核心必考點完全解析

    摘要:如問到是否使用某框架,實際是是問該框架的使用場景,有什么特點,和同類可框架對比一系列的問題。這兩個方向的區分點在于工作方向的側重點不同。 [TOC] 這是一份來自嗶哩嗶哩的Java面試Java面試 32個核心必考點完全解析(完) 課程預習 1.1 課程內容分為三個模塊 基礎模塊: 技術崗位與面試 計算機基礎 JVM原理 多線程 設計模式 數據結構與算法 應用模塊: 常用工具集 ...

    JiaXinYi 評論0 收藏0
  • 我的阿里之路+Java面經考點

    摘要:我的是忙碌的一年,從年初備戰實習春招,年三十都在死磕源碼,三月份經歷了阿里五次面試,四月順利收到實習。因為我心理很清楚,我的目標是阿里。所以在收到阿里之后的那晚,我重新規劃了接下來的學習計劃,將我的短期目標更新成拿下阿里轉正。 我的2017是忙碌的一年,從年初備戰實習春招,年三十都在死磕JDK源碼,三月份經歷了阿里五次面試,四月順利收到實習offer。然后五月懷著忐忑的心情開始了螞蟻金...

    姘擱『 評論0 收藏0
  • JVM 完整深入解析

    摘要:堆內存的劃分在里面的示意圖垃圾回收一判斷對象是否要回收的方法可達性分析法可達性分析法通過一系列對象作為起點進行搜索,如果在和一個對象之間沒有可達路徑,則稱該對象是不可達的。 工作之余,想總結一下JVM相關知識。 Java運行時數據區: Java虛擬機在執行Java程序的過程中會將其管理的內存劃分為若干個不同的數據區域,這些區域有各自的用途、創建和銷毀的時間,有些區域隨虛擬機進程的啟動而...

    shenhualong 評論0 收藏0
  • JVM系列(一):深入詳解JVM 內存區域總結!

    摘要:一內存區域虛擬機在運行時,會把內存空間分為若干個區域,根據虛擬機規范版的規定,虛擬機所管理的內存區域分為如下部分方法區堆內存虛擬機棧本地方法棧程序計數器。前言 在JVM的管控下,Java程序員不再需要管理內存的分配與釋放,這和在C和C++的世界是完全不一樣的。所以,在JVM的幫助下,Java程序員很少會關注內存泄露和內存溢出的問題。但是,一旦JVM發生這些情況的時候,如果你不清楚JVM內存的...

    Aldous 評論0 收藏0

發表評論

0條評論

TANKING

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<