jvm高級特性(1)(內存泄漏實例)


jvm內存結構回顧:

1.8同1.7比,最大的差別就是:元數據區取代了永久代。
元空間的本質和永久代類似,都是對JVM規范中方法區的實現

不過元空間與永久代之間最大的區別在於:元數據空間不在虛擬機中,而是使用本地內存

jdk1.4引入了NIO,它可以使用Native函數庫直接分配堆外內存。(這也是為什么nio效率高)

 

1 . 程序計數器

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

JVM的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的

每條線程擁有一個獨立的程序計數器,各線程之間計數器互不影響
此內存區域是唯一一個在Java虛擬機規范中沒有規定任何 OutOfMemoryError情況的區域

2 . Java虛擬機棧

1,同程序計數器相同,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的
2,虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
3,每個方法對應一個棧幀在虛擬機棧中入棧到出棧的過程。

局部變量表中存放了編譯期可知的類型。

八大數據類型(boolean、bytecharshortintfloatlongdouble)。
對象引用(reference類型,它不等於對象本身,可能是一個指向對象起始地址的指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)
returnAddress類型(指向了一條字節碼指令的地址)(ps:應該就是一個方法)

因為類型可知,幀中分配多大的局部變量空間是完全確定的,在方法運行期間局部變量表的大小也不變。

出現的異常:

1,方法嵌套棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常。
2,動態擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。

3 . 本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用類似,
區別是:虛擬機棧執行Java方法(也就是字節碼),而本地方法棧使用Native方法。

甚至有的虛擬機(Sun HotSpot虛擬機)直接把本地方法棧和虛擬機棧合二為一。

同樣拋出:

StackOverflowError和OutOfMemoryError異常。

4 . Java堆

被所有線程共享的一塊內存區域,在虛擬機啟動時創建。幾乎所有的對象實例都在這里分配內存。

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

從內存回收角度看,由於目前收集器基本采用分代收集算法,所以Java堆可細分為:新生代和老年代。
從內存分配角度看,線程共享的Java堆中可能划分出多個線程私有的分配緩沖區(TLAB:Thread Local Allocation Buffer)。

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

5 . 方法區

方法區(Method Area)與堆一樣,是各個線程共享的,用於存儲類信息、常量、靜態變量、
內存回收目標主要是針對常量池的回收和對類型的卸載

方法無法滿足內存需求時,將會拋出OutOfMemoryError異常。

6 . 運行時常量池

運行時常量池(Runtime Constant Pool)是方法區的一部分。
Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有常量池(Constant Pool Table,各種字面量和符號引用)類加載后存放在方法區的運行時常量池。
除了保存Class文件中的描述符號引用外,還會把翻譯出的直接引用也存儲在運行時常量池中。(也就是動態解析時添加)

運行時常量池相對於Class文件常量池的另外一個重要特征是具備動態性:

運行期間也可能將新的常量放入池中,比如:String類的intern() 方法。

由於運行時常量池是方法區的一部分,所以會受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError: PermGen space異常

(Java 8以后沒有方法區,由本地元空間代替,溢出會拋出OutOfMemoryError: Metaspace異常)。

7 . 直接內存

這部分的數據是在JVM之外的,因此它不會占用應用的內存。

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

 

簡要說明一下內存泄露和內存溢出的區別:

內存泄露是指:因為內存被無用的資源占用,導致內存不夠。

分配出去的內存沒有被回收回來,由於失去了對該內存區域的控制,因而造成了資源的浪費。
Java中一般不會產生內存泄露,因為有垃圾回收器自動回收垃圾,但這也不絕對,
當我們new了對象,並保存了其引用,但是后面一直沒用它,而垃圾回收器又不會去回收它,這邊會造成內存泄露,

內存溢出是指:程序所需要的內存超出了系統所能分配的內存(包括動態擴展)的上限。

 

補充:JVM內存參數( -Xms -Xmx -Xmn -Xss 直接內存)

以下基於《深入理解Java虛擬機》

重現OutOfMemoryError異常

除了程序計數器外,虛擬機內存的其它區域都有發生OOM異常的可能

目的:根據異常信息判斷出是那個區域,定位到錯誤原因

1 . Java堆溢出

Java堆用於存儲對象實例,只要不斷地創建對象,並且避免垃圾回收機制清除這些對象,那么在對象數量到達最大堆的容量限制后就會產生內存溢出異常。

/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOM {

    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();

        while (true) {
            list.add(new OOMObject());
        }}}
java.lang.OutOfMemoryError: Java heap space 
    Dumping heap to java_pid1820.hprof ... 
    Heap dump file created [24787111 bytes in 0.346 secs] 
    Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 

當出現異常時,堆棧消息“java.lang.OutOfMemoryError”會進一步提示“Java heap space”,而問題原因很明顯,對象數量過多,到達最大堆的容量限制。

2 . 虛擬機棧和本地方法棧溢出

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

異常實質上存在着重疊:當棧空間無法繼續分配時,是內存太小還是已使用的棧空間過大。

【虛擬機棧和本地方法棧OOM測試(僅作第一點測試)】
/** 
 * 如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError 
 * 如果虛擬機在擴展棧時無法申請到足夠的內存空間,將拋出OutOfMemoryError 
 * VM Args:-Xss128k 
 */  
public class JavaVMStackSOF {  

    private int stackLength = 1;  

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

    public static void main(String[] args) throws Throwable {  
        JavaVMStackSOF oom = new JavaVMStackSOF();  
        try {  
            oom.stackLeak();  
        } catch (Throwable e) {  
            System.out.println("stack length:" + oom.stackLength);  
            throw e;  
        }      }  }
stack length:2403 
     Exception in thread "main" java.lang.StackOverflowError 
         at baby.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11) 
         at baby.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12) 
         at baby.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12) 

         默認情況下,即不加Xss限制,輸出的length為8956,加了Xss128k length位2403 

在單線程下,無論是由於棧幀太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StackOverflowError。

 

通過不斷地建立線程的方式可以產生內存溢出異常,

但是這樣產生的內存溢出異常與棧空間是否足夠大並不存在任何聯系,為每個線程的棧分配內存越大,越容易產生內存溢出的異常。

簡述操作系統內存分配

由於操作系統分配給每個進程的內存是有限的,
虛擬機提供了參數來控制Java堆和方法區域(Xmx(最大堆容量),MaxPermSize(最大方法區容量))。 其余內存主要由虛擬機棧和本地方法瓜分。

每個線程分配到的棧容量越大,可以建立的線程數量自然越少,建立線程越容易耗盡剩下內存。

【創建線程導致內存溢出異常】

/**
 * VM Args:-Xss2M (這時候不妨設大些)
 */
public class JavaVMStackOOM {

       private void dontStop() {
              while (true) {
              }
       }

       public void stackLeakByThread() {
              while (true) {
                     Thread thread = new Thread(new Runnable() {
                            @Override
                            public void run() {
                                   dontStop();
                            }
                     });
                     thread.start();
              }
       }

       public static void main(String[] args) throws Throwable {
              JavaVMStackOOM oom = new JavaVMStackOOM();
              oom.stackLeakByThread();
       }
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread  

以上測試只是說明線程太多可能導致棧內存不夠。

3 . 方法區和運行常量池溢出

由於運行時常量池是方法區的一部分,因此兩者測試放在一起。

測試中涉及到String.intern()方法,

它是一個Native方法,作用為:如果字符串常量池中已經包含一個等於此String對象的字符串,則返回String對象;
否則將此String對象包含的字符串添加到常量池中,並返回String對象的引用。
【運行時常量池導致的內存溢出異常】

/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 */
public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        // 使用List保持着常量池引用,避免Full GC回收常量池行為
        List<String> list = new ArrayList<String>();
        // 10MB的PermSize在integer范圍內足夠產生OOM了
        int i = 0; 
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space 
    at java.lang.String.intern(Native Method) 
    at baby.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18) 

在OutOfMemoryError后面跟隨的提示信息時“PermGen space”(Permanent Generation space)是指內存的永久保存區域,也就方法區。

4. 本機直接內存溢出

DirectMemory容量可通過 -XX: MaxDirectMemorySize指定,若不指定則默認與Java堆最大值(-Xmx指定)相同。

以下代碼越過了DerictByteBuffer類,直接通過反射獲取Unsafe 實例進行內存分配。

雖然使用DerictByteBuffer分配內存也會拋出內存溢出異常,但它拋出異常時並沒有真正向操作系統申請分配,

而是通過計算得知內存無法分配,於是手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory()

使用unsafe 分配本機內存】

/**
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectMemoryOOM {

    private static final int _1MB = 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(_1MB);
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError 
    at sun.misc.Unsafe.allocateMemory(Native Method) 
    at baby.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20) 

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

如果開發人員發現OOM異常后的Dump文件很小,而程序中又直接或間接使用了NIO,可以考慮是否是這方面的原因。

補充:java程序性能分析之thread dump和heap dump

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM