JVM優化系列-Stop-The-World實戰

導語
  垃圾收集器的主要任務是識別和回收垃圾對象進行內存清理,爲了讓垃圾回收器可正常高效執行,在大部分的情況下會請求系統進入到一個停頓階段,在這個停頓階段對所欲應用進程進行終止,然後執行垃圾清理操作,只有所有應用線程都停止了纔不會有新的垃圾產生。同時保證在停頓瞬間所有應用的一致性,更有利於垃圾清理的標記每個對象。所以在垃圾清理的過程中這樣的停頓被稱爲是Stop-The-World。

示例

public class StopWorldTest {
    public static class MyThread extends Thread{
        HashMap map = new HashMap();

        @Override
        public void run() {
            try{
                while (true){
                    if (map.size()*512/1024/1024>=900){
                        map.clear();
                        System.out.println("clean map");
                    }
                    byte[] b1 ;
                    for (int i =0;i<100;i++){
                        b1 = new byte[512];
                        map.put(System.nanoTime(),b1);
                    }
                    Thread.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class PrintThread extends Thread{
        public static final long starttime = System.currentTimeMillis();

        @Override
        public void run() {
            try {
                while (true){
                    long t = System.currentTimeMillis()-starttime;
                    System.out.println(t/1000+"."+t%1000);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
        MyThread t = new MyThread();
        PrintThread p = new PrintThread();
        t.start();
        p.start();
    }
}

  上面代碼,開啓了兩個線程,PrintThread負責在0.1秒的瞬間內進行一次時間戳輸出。MyThread則是負責消耗內存資源,引起GC操作。當內存消耗大於900MB的時候,清空內存,防止內存溢出。

添加運行參數

-Xmx1g -Xms1g -Xmn512k -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails

在這裏插入圖片描述
運行程序

Exception in thread "Thread-0" java.lang.OutOfMemoryError 
 - klass: 'java/lang/OutOfMemoryError'
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (exceptions.cpp:427), pid=1230, tid=19715
#  fatal error: ExceptionMark destructor expects no pending exceptions
#
# JRE version: Java(TM) SE Runtime Environment (8.0_74-b02) (build 1.8.0_74-b02)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25.74-b02 mixed mode bsd-amd64 compressed oops)
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/nihui/Documents/IDEAProject/JVM/jvm-perm/hs_err_pid1230.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.java.com/bugreport/crash.jsp
#

在這裏插入圖片描述

45.676Exception in thread "MyThread-" 
47.115
47.822
48.553
49.281
50.18
50.748*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message can't create name string at JPLISAgent.c line: 807

java.lang.OutOfMemoryError: Java heap space
	at com.nihui.stoptheworld.StopWorldTest$MyThread.run(StopWorldTest.java:25)
55.750
58.860

  其實在之前的分享中有看到過實際分配的內存跟代碼中的內容其實是不相符的,在實際內存使用過程中還有考慮到其他類對象創建以及對象索引創建,所以如果沒有不能合理的調整內存大小分配是無法出現測試結果的。

分析問題

  對於這個問題的分析也是對之前學過的內容的總結。
首先來看看垃圾回收日誌中其實在26秒左右的時候就已經Heap內存達到了900M,但是實際上並沒有執行垃圾回收。而知繼續擴展內存。但是最大的堆內存爲1G如上面圖中所展示的一樣,繼續分配就會導致Heap內存溢出,但是這個時候程序並沒有停止。因爲其中有一個PrintThread線程沒有停止。
在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

  如上圖所示,在內存溢出之後線程MyThread已經死了,而不是進入sleep。這個從另一個角度上可以印證了JVM的堆內存是線程共享的內存區域,當一個線程因爲內存溢出操作而導致線程關閉,不會影響其他線程使用對堆內存。對堆內存進行垃圾回收之後,其他線程又可以繼續使用,但是對於已經停止的線程就提供不了其他的幫助了。

  上面說到當堆內存達到900MB的時候回自動對Map 進行clear但是實際的運行時調用對應代碼之後並沒有執行clear操作,就是因爲沒有考慮到整個的JVM使用中還有其他內容也在使用內存空間,當等到整個的線程運行完分配完內存之後,就已經是內存溢出了。

下面將代碼調整到如下的內容

public class StopWorldTest {
    public static class MyThread extends Thread{
        HashMap map = new HashMap();

        @Override
        public void run() {
            try{
                while (true){
                    if (map.size()*512/1024/1024>=500){
                        map.clear();
                        System.out.println("clean map");
                    }
                    byte[] b1 ;
                    for (int i =0;i<100;i++){
                        b1 = new byte[512];
                        map.put(System.nanoTime(),b1);
                    }
                    Thread.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class PrintThread extends Thread{
        public static final long starttime = System.currentTimeMillis();

        @Override
        public void run() {
            try {
                while (true){
                    long t = System.currentTimeMillis()-starttime;
                    System.out.println(t/1000+"."+t%1000);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
        MyThread t = new MyThread();
        PrintThread p = new PrintThread();
        t.setName("MyThread");
        p.setName("PrintThread");
        t.start();
        p.start();
    }
}

運行結果

在這裏插入圖片描述

在這裏插入圖片描述

30.282: [GC (Allocation Failure) 30.282: [DefNew: 447K->447K(448K), 0.0000344 secs]30.282: [Tenured: 1047932K->491318K(1048064K), 0.4429925 secs] 1048380K->491318K(1048512K), [Metaspace: 8690K->8690K(1056768K)], 0.4431948 secs] [Times: user=0.44 sys=0.00, real=0.44 secs] 

46.834: [GC (Allocation Failure) 46.834: [DefNew: 447K->447K(448K), 0.0000142 secs]46.834: [Tenured: 1047967K->477397K(1048064K), 0.3491591 secs] 1048415K->477397K(1048512K), [Metaspace: 8792K->8792K(1056768K)], 0.3492485 secs] [Times: user=0.35 sys=0.00, real=0.35 secs] 

63.685: [GC (Allocation Failure) 63.685: [DefNew: 447K->447K(448K), 0.0000318 secs]63.685: [Tenured: 1047893K->464358K(1048064K), 0.3490292 secs] 1048341K->464358K(1048512K), [Metaspace: 8820K->8820K(1056768K)], 0.3491777 secs] [Times: user=0.35 sys=0.00, real=0.35 secs]  

  從GC日誌中可以看到,在整個的階段中新生代的GC操作是比較頻繁的,但是每一次執行GC操作耗時都比較少,老年代GC發生的次數比較少並且發生之後會直接影響到heap的使用結構,對於未使用的與已使用的影響較大。每一次GC操作消耗的時間要比新生代的GC操作要長。這種現象和虛擬機的參數設置有關。下面就來看看其他參數設置的效果。

-Xmx1g -Xms1g -Xmn900m -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails

在這裏插入圖片描述

在這裏插入圖片描述

  整個的內存GC都有所下降,並且全部是耗時比較長的GC操作,與之前的無法相比。

總結

  在使用JVM內存調優的時候一定要考慮到內存的合理分配,給整個程序運行過程中的每個使用的對象都要分配合理的內存結構,不然就會出現上面的內存溢出的異常,導致程序處於假死不能提供服務的狀態。對於其他的測試只需要調整上面的對應的參數來看看不同內存區域對於應用程序的影響。

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