JVM調優之:內存模型

1. JVM內存模型

2.程序計數器

程序計數器是一個很小的內存空間。因爲Java是支持多線程的語音,當線程數量超過cpu數量,線程之間根據時間片輪詢搶奪CPU資源。對於單核CPU而言,每一時刻只能有一個線程在執行,而其餘線程必須被切換出去。爲此每個線程必須有一個獨立的程序計數器,用於記錄下一條要執行的指令。各個線程之間的計數器互不影響,獨立工做。是一塊線程私有的內存空間。java

若是當前線程正在執行一個Java方法,程序計數器中存放的就是正在執行的Java字節碼地址,若是正在執行的是native方法,則程序計數器爲空。數組

3.虛擬機棧

Java虛擬機棧也是線程的私有的內存空間,它和Java線程在同一時刻建立,它保存方法的局部變量、部分結果,並參與方法的調用和返回。數據結構

JAVA規範中容許Java棧的大小是動態的或者是固定不變的;Java虛擬機中定義了兩種異常與棧空間有關多線程

StackOverflowError:若是線程在計算過程當中,請求的棧深度大於最大可用的棧深度,則拋出該錯誤。jvm

OutOfMemoryError:若是Java棧能夠動態擴展,在擴展過程當中沒有足夠的內存空間來支持棧的擴展,則拋出該錯誤。函數

JVM中但是使用-Xss參數來設置棧大小,棧的大小直接決定了函數調用的可達深度。如下代碼經過遞歸調用查看方法調用可達深度。在JDK5.0及其之前棧默認大小爲256k,JDK5.0以後jvm棧默認大小爲1m。工具

public class StackTest {
    private int count = 0;//記錄棧可達深度
    public void recursion(){
        count ++;
        recursion();
    }

    public static void main(String[] args) {
        StackTest stackTest = new StackTest();
        try {
            stackTest.recursion();
        }catch (Throwable e){
            System.out.println("deep of stack is "+stackTest.count);
            e.printStackTrace();
        }
    }
}

默認執行結果:性能

deep of stack is 17127
java.lang.StackOverflowErrorthis

若是系統須要更深的棧調用,設置-Xss2mspa

執行結果:

deep of stack is 73391
java.lang.StackOverflowError

能夠看到增長棧空間後,函數調用的棧深度明顯增長。

虛擬機棧在運行時使用一種叫作棧幀的數據結構保存上下文數據,在棧幀中存放了方法局部變量表、操做數棧、動態鏈接方法和返回地址等信息。每一個方法的調用都伴隨着入棧,方法的返回伴隨着出棧。若是方法調用時方法的參數和局部變量越多那麼棧幀中局部變量表就越大,棧幀佔用的空間就越大,那麼方法調用嵌套次數就越小。

以下方法和上面例子比較

public class StackTest {
    private int count = 0;//記錄棧可達深度
    public void recursion(long a,long b,long c){
        long d = 0,f = 0,e = 0;
        count ++;
        recursion(d,f,e);
    }

    public static void main(String[] args) {
        StackTest stackTest = new StackTest();
        try {
            stackTest.recursion(1L,2L,3L);
        }catch (Throwable e){
            System.out.println("deep of stack is "+stackTest.count);
            e.printStackTrace();
        }
    }
}

默認棧大小-Xss1m時執行結果,能夠看到當方法參數和局部變量增長時,調用深度明顯減少。

deep of stack is 5445
java.lang.StackOverflowError

在棧幀中,與性能調優關係最爲密切的的部分就是局部變量表。局部變量表存放方法參數和內部變量,非static方法虛擬機還會把當前對象(this)做爲參數經過局部變量表傳遞給當前方法。

經過jclasslib工具能夠查看class文件中每一個方法所分配的最大局部變量表的容量。打開StackTest.calss文件找到方法recursion(),將其展開後查看Code屬性,選擇Misc頁面,能夠查看該方法最大局部變量。局部變量表以「字」爲單位進行劃份內存空間。long/double類型佔兩個「字」,其餘類型佔一個「字」。

//共13個「字」
public void recursion(long a,long b,long c){
    long d = 0,e = 0, f = 0;
    count ++;
    recursion(d,e,f);
}

局部變量表中字空間是能夠重用的,由於在一個方法體內,局部變量的做用範圍並非必定是整個方法體。

public void test1(){
    {
        long a = 0;
    }
    long b = 0;
}

比較

public void test2(){
    long a = 0;
    long b = 0;
}

局部變量表的字對系統GC也有必定影響,若是與幾個變量被保存在局部變量表中,那麼GC根就能引用到這個局部變量所指向的內存空間,從而GC時沒法回收這部分空間。

1. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
    }
    System.gc();
    System.out.println("first explict gc over");
}

雖然系統GC時已經超出了b變量做用範圍,可是不會被回收

[GC (System.gc())  26542K->25256K(62976K), 0.0043875 secs]
[Full GC (System.gc())  25256K->25191K(62976K), 0.0326676 secs]
first explict gc over

2. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
        b = null;
    }
    System.gc();
    System.out.println("first explict gc over");
}

手動把b變量設置爲null,可使GC時回收b所佔用的內存空間。

[GC (System.gc())  26542K->25328K(62976K), 0.0020175 secs]
[Full GC (System.gc())  25328K->627K(62976K), 0.0074723 secs]
first explict gc over

3. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
    }
    int a = 0;
    System.gc();
    System.out.println("first explict gc over");
}

更多的方法是新聲明的變量,會複用變量b的字,使b所佔的內存空間被GC回收。

[GC (System.gc())  26542K->25384K(62976K), 0.0015061 secs]
[Full GC (System.gc())  25384K->627K(62976K), 0.0077682 secs]
first explict gc over

4. 本地方法棧

本地方法棧和Java虛擬機棧的功能很類似,Java虛擬機棧用於管理Java函數的調用,而本地方法棧用於管理本地方法的調用。所以和Java虛擬機棧同樣本地方法棧也會拋出StackOverflowError和OutOfMemoryError。

本地方法棧是使用C語言實現的,在SUN的Hot Spot虛擬機中不區分本地方法棧和Java虛擬中棧。

5. Java堆

Java堆能夠說是Java運行時內存中最爲重要的部分,幾乎全部的對象和數組都在堆中分配空間。Java堆分爲新生代和老年代兩部分,新生代用來存放新產生的對象和年輕的對象,若是一個對象通過屢次GC都沒有被回收,則該對象會被存放到老年代。

新生代

Eden:剛剛產生的對象存放該空間。

s0(from space)/s1(to space):至少通過一次GC沒有被回收的對象存放在該空間。

老年代:屢次GC都沒有被回收的對象最終會被存放在該空間。

6. 方法區(JDK6)

方法區也是JVM內存中很是重要的的一塊內存區域。與堆空間相似,它也是被JVM中因此的線程共享。方法區中主要保存的信息是類的元數據。

方法區存放

類的類型信息:包括類的完整名稱,父類的完整名稱,類型修飾符,類型的直接接口類表。

常量池:包括這個類方法、域等信息所引用的常量信息。

域信息:包括域名稱,域類型和修飾符。

方法信息:包括方法名稱,返回類型,方法參數,方法修飾符,方法字節碼,操做數棧和方法幀棧的局部變量區大小以及異常表。

在Hot Spot虛擬機中,方法區也成爲永久區,是一塊獨立的內存空間。雖然叫作永久區,可是在永久區中的對象也是能夠被回收的。對永久區的回收主要從兩個方面分析:一是GC對永久區常量池的回收;二是永久區對類元數據的回收。Hot Spot虛擬機對常量池的回收,只要常量池中的常量沒有被任何地方引用就能夠被回收。

常量池演示:

String.intern():若是常量池存在當前String,返回常量池中的String;若是常量池中不存在當前String,將當前String添加到常量池,並返回池中對象。

public static void main(String[] args) {
    for (int i = 0;i< Integer.MAX_VALUE;i++){
        String t = String.valueOf(i).intern();
    }
}

使用JVM參數-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails運行,每當常量池滿時就進行回收,確保程序正常運行。

結果顯示,當常量池空間不足時,沒有被引用的常量會被回收。

元數據演示:

與常量池的回收相比,類的元數據回收,稍微複雜一些,使用javassist類庫,產生大量類佔用元數據。觀察元數據的回收狀況。

動態類父類,生成的子類都要繼承給父類

//定義演示動態類的父類,後面使用Javassist產生的動態類都是該類的子類
public class JavaBeanObject{
    private String name="java";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

動態類生成的

public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    for (int i=0;i<Integer.MAX_VALUE;i++){//循環動態生成大量類
        CtClass c = ClassPool.getDefault().makeClass("Geym"+i);//定義類名
        c.setSuperclass(ClassPool.getDefault().get("com.eaju.jvm.JavaBeanObject"));//設置父類
        Class clz = c.toClass();//新建類
        JavaBeanObject v = (JavaBeanObject)clz.newInstance();
    }
}

運行參數:-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails

以上代碼運行會產生大量的JavaBeanObject類的子類,佔用元數據,致使永久區空間不足,運行一段時間以後會拋出「java.lang.OutOfMemoryError:PermGen space」顯示持久帶溢出。

事實上類元數據也是能夠被回收的,須要知足如下兩個條件:1.該類的全部實例均已經被回收;2.該類的加載器ClassLoader也已經被回收。

7.  方法區(JDK8)

在Java7以前,HotSpot虛擬機中將GC分代收集擴展到了方法區,使用永久代來實現了方法區。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。可是在以後的HotSpot虛擬機實現中,逐漸開始將方法區從永久代移除。

Java7中已經將運行時常量池從永久代移除,在Java 堆(Heap)中開闢了一塊區域存放運行時常量池。而在Java8中,已經完全沒有了永久代,將方法區直接放在一個與堆不相連的本地內存區域,這個區域被叫作元空間。 

總之:jdk1,6常量池放在方法區,jdk1.7常量池放在堆內存,jdk1.8放在元空間裏面,和堆相獨立。

驗證常量池

一樣的方法驗證元數據

//定義演示動態類的父類,後面使用Javassist產生的動態類都是該類的子類
public class JavaBeanObject{
    private String name="java";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

動態建立類方法

public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    for (int i=0;i<Integer.MAX_VALUE;i++){//循環動態生成大量類
        CtClass c = ClassPool.getDefault().makeClass("Geym"+i);//定義類名
        c.setSuperclass(ClassPool.getDefault().get("com.eaju.jvm.JavaBeanObject"));//設置父類
        Class clz = c.toClass();//新建類
        JavaBeanObject v = (JavaBeanObject)clz.newInstance();
    }
}

運行參數:-XX:MaxMetaspaceSize=8m  -XX:+PrintGCDetails

限制元空間大小,在不斷建立元數據時元空間很快就會被佔用完就會拋出異常

[GC (Last ditch collection) [PSYoungGen: 0K->0K(44544K)] 8631K->8631K(132608K), 0.0075906 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Last ditch collection) [PSYoungGen: 0K->0K(44544K)] [ParOldGen: 8631K->8631K(127488K)] 8631K->8631K(172032K), [Metaspace: 7758K->7758K(1056768K)], 0.0336065 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
Heap
 PSYoungGen      total 44544K, used 1662K [0x00000000eb180000, 0x00000000eee80000, 0x0000000100000000)
  eden space 41984K, 3% used [0x00000000eb180000,0x00000000eb31fab0,0x00000000eda80000)
  from space 2560K, 0% used [0x00000000edd00000,0x00000000edd00000,0x00000000edf80000)
  to   space 10240K, 0% used [0x00000000ee480000,0x00000000ee480000,0x00000000eee80000)
 ParOldGen       total 127488K, used 8631K [0x00000000c1400000, 0x00000000c9080000, 0x00000000eb180000)
  object space 127488K, 6% used [0x00000000c1400000,0x00000000c1c6de78,0x00000000c9080000)
 Metaspace       used 7790K, capacity 8098K, committed 8192K, reserved 1056768K
  class space    used 1936K, capacity 1997K, committed 2048K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at javassist.ClassPool.toClass(ClassPool.java:1099)
    at javassist.ClassPool.toClass(ClassPool.java:1042)
    at javassist.ClassPool.toClass(ClassPool.java:1000)
    at javassist.CtClass.toClass(CtClass.java:1224)

JDK8以後內存模型圖

相關文章
相關標籤/搜索