JVM內(nèi)存模型及相關(guān)概念

虛擬機整體結(jié)構(gòu)

我們先來張圖看看虛擬機的整體結(jié)構(gòu)


556px-JvmSpec7.png

我們可以從上圖看出敢辩,JVM大概可以分為以下幾部分內(nèi)容:類加載器、內(nèi)存空間逛万、執(zhí)行引擎泳猬、本地方法接口、本地方法庫宇植。接下來我們在本篇文章中著重分析內(nèi)存空間得封。

JVM內(nèi)存空間

JVM內(nèi)存空間大致分為程序計數(shù)器、堆指郁、虛擬機棧忙上、本地方法棧、方法區(qū)闲坎、直接內(nèi)存幾部分疫粥。

程序計數(shù)器

程序計數(shù)器是一塊較小的內(nèi)存空間,可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器腰懂。分支手形、循環(huán)、跳轉(zhuǎn)悯恍、異常處理库糠、線程恢復等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成
在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)只會執(zhí)行一條線程中的指令涮毫。因此瞬欧,為了線程切換后能恢復到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器罢防,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存艘虎。
此內(nèi)存區(qū)域是唯一一個在Java 虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域

虛擬機棧

線程私有,它的生命周期與線程相同咒吐。虛擬機棧描述的是Java 方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀野建、用于存儲局部變量表属划、操作棧、動態(tài)鏈接候生、方法出口等信息同眯。
每個在虛擬機中運行的程序也是由許多的幀的切換產(chǎn)生的結(jié)果,只是這些幀里面存放的是方法的局部變量唯鸭,操作數(shù)棧须蜗,動態(tài)鏈接,方法返回地址和一些額外的附加信息組成目溉。每一個方法被調(diào)用直至執(zhí)行完成的過程明肮,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
我們來簡單看下局部變量表和動態(tài)鏈接:

局部變量表

一組變量值存儲空間缭付,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量

動態(tài)連接

虛擬機運行的時候,運行時常量池會保存大量的符號引用柿估,這些符號引用可以看成是每個方法的間接引用。如果代表棧幀A的方法想調(diào)用代表棧幀B的方法陷猫,那么這個虛擬機的方法調(diào)用指令就會以B方法的符號引用作為參數(shù)秫舌,但是因為符號引用并不是直接指向代表B方法的內(nèi)存位置,所以在調(diào)用之前還必須要將符號引用轉(zhuǎn)換為直接引用烙丛,然后通過直接引用才可以訪問到真正的方法。
如果符號引用是在類加載階段或者第一次使用的時候轉(zhuǎn)化為直接應(yīng)用羔味,那么這種轉(zhuǎn)換成為靜態(tài)解析河咽,如果是在運行期間轉(zhuǎn)換為直接引用,那么這種轉(zhuǎn)換就成為動態(tài)連接赋元。

本地方法棧

與虛擬機棧所發(fā)揮的作用是非常相似的忘蟹,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java 方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機使用到的Native 方法服務(wù)搁凸。
與虛擬機棧一樣媚值,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常。

方法區(qū)

方法區(qū)在一個jvm實例的內(nèi)部护糖,類型信息被存儲在一個稱為方法區(qū)的內(nèi)存邏輯區(qū)中褥芒。類型信息是由類加載器在類加載時從類文件中提取出來的。類(靜態(tài))變量也存儲在方法區(qū)中嫡良。
  存儲已被虛擬機加載的類信息锰扶、常量、靜態(tài)變量寝受、即時編譯器編譯后的代碼等數(shù)據(jù)坷牛,如字面量及符號引用,類的全限定名很澄,繼承的父類全限定名京闰,實現(xiàn)的接口全限定名颜及,訪問修飾符,方法訪問修飾符蹂楣、參數(shù)列表俏站、返回值類型 、方法名捐迫,字段訪問修飾符乾翔、字段名稱、字段類型施戴,除了常量以外的所有類(靜態(tài))變量反浓,還有一個指向ClassLoader的指針,一個指向Class對象的指針赞哗, 常量池(常量數(shù)據(jù)以及對其他類型的符號引用)等

直接內(nèi)存

JDk1.4中新加入了NIO類雷则,引入了一種基于通道與緩沖區(qū)的I/O方式,它可以使用Native函數(shù)直接分配堆外內(nèi)存肪笋,這樣能夠在一些場景中顯著提高性能月劈,因為避免了在Java堆和Native堆中來回復制數(shù)據(jù)。

堆是Java 虛擬機所管理的內(nèi)存中最大的一塊藤乙。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域猜揪,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例坛梁,幾乎所有的對象實例都在這里分配內(nèi)存而姐。
堆分為Eden,Survivor to,Survivor from,Old等區(qū)域

內(nèi)存分配過程

1划咐、JVM 會試圖為相關(guān)Java對象在Eden Space中初始化一塊內(nèi)存區(qū)域拴念。
2、當Eden空間足夠時褐缠,內(nèi)存申請結(jié)束政鼠;否則到下一步。
3队魏、JVM 試圖釋放在Eden中所有不活躍的對象(這屬于1或更高級的垃圾回收)公般。釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區(qū)胡桨。
4俐载、Survivor區(qū)被用來作為Eden及Old的中間交換區(qū)域,當Old區(qū)空間足夠時登失,Survivor區(qū)的對象會被移到Old區(qū)遏佣,否則會被保留在Survivor區(qū)。
5揽浙、當Old區(qū)空間不夠時状婶,JVM 會在Old區(qū)進行完全的垃圾收集(0級)意敛。
6、完全垃圾收集后膛虫,若Survivor及Old區(qū)仍然無法存放從Eden復制過來的部分對象草姻,導致JVM無法在Eden區(qū)為新對象創(chuàng)建內(nèi)存區(qū)域,則出現(xiàn)“outofmemory”錯誤稍刀。

對象內(nèi)存布局

對象內(nèi)存中布局可以分為3塊:對象頭撩独,實例數(shù)據(jù)和對齊填充

對象頭

對象頭分為兩部分:
1.運行時數(shù)據(jù),如哈希碼账月、GC分代年齡综膀、鎖狀態(tài)標志、線程持有的鎖局齿、偏向線程ID剧劝、偏向時間戳等
2.類型指針,即對象指向它的類元數(shù)據(jù)指針抓歼,虛擬機通過這個指針來確定屬于哪個類的實例讥此。

對象訪問

對象訪問在Java 語言中無處不在,是最普通的程序行為谣妻,但即使是最簡單的訪問萄喳,也會卻涉及Java 棧、Java 堆蹋半、方法區(qū)這三個最重要內(nèi)存區(qū)域之間的關(guān)聯(lián)關(guān)系他巨,如下面的這句代碼:

Object obj = newObject();

假設(shè)這句代碼出現(xiàn)在方法體中,那“Object obj”這部分的語義將會反映到Java 棧的局部變量表中湃窍,作為一個reference 類型數(shù)據(jù)出現(xiàn)闻蛀。而“new Object()”這部分的語義將會反映到Java 堆中匪傍,形成一塊存儲了Object 類型所有實例數(shù)據(jù)值(Instance Data您市,對象中各個實例字段的數(shù)據(jù))的結(jié)構(gòu)化內(nèi)存,根據(jù)具體類型以及虛擬機實現(xiàn)的對象內(nèi)存布局(Object Memory Layout)的不同役衡,這塊內(nèi)存的長度是不固定的茵休。另外,在Java 堆中還必須包含能查找到此對象類型數(shù)據(jù)(如對象類型手蝎、父類榕莺、實現(xiàn)的接口、方法等)的地址信息棵介,這些類型數(shù)據(jù)則存儲在方法區(qū)中钉鸯。

由于reference 類型在Java 虛擬機規(guī)范里面只規(guī)定了一個指向?qū)ο蟮囊茫]有定義這個引用應(yīng)該通過哪種方式去定位邮辽,以及訪問到Java 堆中的對象的具體位置唠雕,因此不同虛擬機實現(xiàn)的對象訪問方式會有所不同贸营,主流的訪問方式有兩種:使用句柄和直接指針。

句柄訪問方式

Java 堆中將會劃分出一塊內(nèi)存來作為句柄池岩睁,reference中存儲的就是對象的句柄地址钞脂,而句柄中包含了對象實例數(shù)據(jù)和類型數(shù)據(jù)各自的具體地址信息。

直接指針訪問方式

reference變量中直接存儲的就是對象的地址捕儒,而java堆對象一部分存儲了對象實例數(shù)據(jù)冰啃,另外一部分存儲了對象類型數(shù)據(jù)。

4種情況必須立即對類進行初始化

1刘莹、遇到new(使用new關(guān)鍵字實例化對象)阎毅、getstatic(獲取一個類的靜態(tài)字段,final修飾符修飾的靜態(tài)字段除外)栋猖、putstatic(設(shè)置一個類的靜態(tài)字段净薛,final修飾符修飾的靜態(tài)字段除外)和invokestatic(調(diào)用一個類的靜態(tài)方法)這4條字節(jié)碼指令時,如果類還沒有初始化蒲拉,則必須首先對其初始化
2肃拜、使用java.lang.reflect包中的方法對類進行反射調(diào)用時,如果類還沒有初始化雌团,則必須首先對其初始化
3燃领、當初始化一個類時,如果其父類還沒有初始化锦援,則必須首先初始化其父類
4猛蔽、當虛擬機啟動時,需要指定一個主類(main方法所在的類)灵寺,虛擬機會首選初始化這個主類
除了上面這4種方式曼库,所有引用類的方式都不會觸發(fā)初始化,稱為被動引用略板。如:通過子類引用父類的靜態(tài)字段毁枯,不會導致子類初始化;通過數(shù)組定義來引用類叮称,不會觸發(fā)此類的初始化种玛;引用類的靜態(tài)常量不會觸發(fā)定義常量的類的初始化,因為常量在編譯階段已經(jīng)被放到常量池中了瓤檐。

常見異常及實例代碼

內(nèi)存溢出和內(nèi)存泄漏

內(nèi)存溢出 out of memory赂韵,是指程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用挠蛉,出現(xiàn)out of memory祭示;比如申請了一個integer,但給它存了long才能存下的數(shù)谴古,那就是內(nèi)存溢出质涛。
內(nèi)存泄露 memory leak悄窃,是指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間蹂窖,一次內(nèi)存泄露危害可以忽略轧抗,但內(nèi)存泄露堆積后果很嚴重,無論多少內(nèi)存,遲早會被占光瞬测。
memory leak會最終會導致out ofmemory横媚。

Java 堆內(nèi)存的OutOfMemoryError異常是實際應(yīng)用中最常見的內(nèi)存溢出異常情況。出現(xiàn)Java 堆內(nèi)存溢出時月趟,異常堆棧信息“java.lang.OutOfMemoryError”會跟著進一步提示“Java heapspace”灯蝴。
要解決這個區(qū)域的異常,一般的手段是首先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對dump 出來的堆轉(zhuǎn)儲快照進行分析孝宗,重點是確認內(nèi)存中的對象是否是必要的穷躁,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
如果是內(nèi)存泄漏因妇,可進一步通過工具查看泄漏對象到GC Roots 的引用鏈问潭。于是就能找到泄漏對象是通過怎樣的路徑與GC Roots 相關(guān)聯(lián)并導致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息婚被,以及GC Roots 引用鏈的信息狡忙,就可以比較準確地定位出泄漏代碼的位置。
如果不存在泄漏址芯,換句話說就是內(nèi)存中的對象確實都還必須存活著灾茁,那就應(yīng)當檢查虛擬機的堆參數(shù)(-Xmx 與-Xms),與機器物理內(nèi)存對比看是否還可以調(diào)大谷炸,從代碼上檢查是否存在某些對象生命周期過長北专、持有狀態(tài)時間過長的情況,嘗試減少程序運行期的內(nèi)存消耗旬陡。
棧溢出

導致各個內(nèi)存區(qū)域溢出異常實例代碼:

STACKOVERFLOW異常
package exceptiontest;

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;
        }
    }
}

運行結(jié)果:
stack length: 19792
Exception in thread “main” java.lang.StackOverflowError
at exceptiontest.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:9)

堆內(nèi)存溢出代碼
package exceptiontest;

import java.util.ArrayList;
import java.util.List;

public class HeapOOM {
    static class OOMObject {}

    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        long i = 1;
        while (true) {
            list.add(new OOMObject());
        }
    }
}

方法區(qū)溢出
package exceptiontest;

import java.lang.reflect.Method;

public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
                        throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }

    static class OOMObject {
    }
}

常量池溢出
package exceptiontest;

import java.util.ArrayList;
import java.util.List;

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

直接內(nèi)存溢出
package exceptiontest;

import java.util.ArrayList;
import java.util.List;

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
/*public String intern()

返回字符串對象的規(guī)范化表示形式拓颓。

一個初始時為空的字符串池,它由類 String 私有地維護季惩。

當調(diào)用 intern 方法時录粱,如果池已經(jīng)包含一個等于此 String 對象的字符串(該對象由 equals(Object) 方法確定)腻格,則返回池中的字符串画拾。否則,將此 String 對象添加到池中菜职,并且返回此 String 對象的引用青抛。

它遵循對于任何兩個字符串 s 和 t,當且僅當 s.equals(t) 為 true 時酬核,s.intern() == t.intern() 才為 true蜜另。

所有字面值字符串和字符串賦值常量表達式都是內(nèi)部的适室。*/

jdk常用命令

對于一些常見的問題,我們可以使用jdk提供的一些工具來分析我們的jvm內(nèi)存狀況举瑰。從而跟蹤和分析問題科盛,如下是一些常用的命令娱俺。
jps 查看虛擬機進程
jstat 用于監(jiān)控虛擬機各種運行狀態(tài)信息的命令行工具
jinfo 實時查看和調(diào)整虛擬機各項參數(shù)
jmap 用于生成堆轉(zhuǎn)儲快照
jhat 虛擬機堆轉(zhuǎn)儲快照分析工具
jstack java堆棧跟蹤工具
具體用法大家可以根據(jù)需求去查,后續(xù)我會慢慢填充這一塊。

判斷對象是否可回收翩剪?

  1. 引用計數(shù)法
    類似C++智能指針,但是當兩個對象互相引用的時候喳资,可能造成無法回收的情況檀头。在c++當中是根據(jù)實際業(yè)務(wù)情況用弱引用來強制回收分配內(nèi)存。
  2. 可達性分析算法
    算法的基本思想就是通過一系列成為”GC Roots”的對象作為起始點坎怪,從這些節(jié)點開始向下搜索罢坝,搜索走過的路徑成為引用鏈,當一個對象到GC Roots沒有任何引用鏈的時候搅窿,則證明此對象是不可用的嘁酿。
    在Java中,可作為引用鏈的GC Roots對象包括下面幾種
    1. 虛擬機棧(本地變量表)中引用的對象男应。
    2. 方法區(qū)中靜態(tài)變量引用的對象痹仙。
    3. 方法區(qū)中常量引用的對象。
    4. 本地方法棧中JNI引用的對象殉了。

垃圾收集算法:

標記-清除法 優(yōu)缺點:速度慢开仰,碎片多
復制法 優(yōu)缺點:速度快,但有一半內(nèi)存無法利用 主要用于在新生代垃圾回收
標記-整理法 優(yōu)缺點:速度慢薪铜,內(nèi)存利用率高 主要用于老年代垃圾回收
關(guān)于垃圾回收更多內(nèi)容可參考這篇文章

引用概念:

強引用: 類似”O(jiān)bject obj = new Object()”众弓,只要引用還在,垃圾收集器就無法回收
軟引用:內(nèi)存溢出之前隔箍,才會對這些對象進行回收谓娃。
弱引用:實例化到下次GC就會被回收
虛引用:虛引用主要用來跟蹤對象被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列 (ReferenceQueue)聯(lián)合使用蜒滩。當垃圾回收器準備回收一個對象時滨达,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內(nèi)存之前俯艰,把這個虛引用加入到與之 關(guān)聯(lián)的引用隊列中捡遍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市竹握,隨后出現(xiàn)的幾起案子画株,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谓传,死亡現(xiàn)場離奇詭異蜈项,居然都是意外死亡,警方通過查閱死者的電腦和手機续挟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門紧卒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诗祸,你說我怎么就攤上這事常侦。” “怎么了贬媒?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵聋亡,是天一觀的道長。 經(jīng)常有香客問我际乘,道長坡倔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任脖含,我火速辦了婚禮罪塔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘养葵。我一直安慰自己征堪,他們只是感情好,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布关拒。 她就那樣靜靜地躺著佃蚜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪着绊。 梳的紋絲不亂的頭發(fā)上谐算,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機與錄音归露,去河邊找鬼洲脂。 笑死,一個胖子當著我的面吹牛剧包,可吹牛的內(nèi)容都是我干的恐锦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼疆液,長吁一口氣:“原來是場噩夢啊……” “哼一铅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起枚粘,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤馅闽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后馍迄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體福也,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年攀圈,在試婚紗的時候發(fā)現(xiàn)自己被綠了暴凑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡赘来,死狀恐怖现喳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情犬辰,我是刑警寧澤嗦篱,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站幌缝,受9級特大地震影響灸促,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涵卵,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一浴栽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧轿偎,春花似錦典鸡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至昆婿,卻和暖如春间护,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背挖诸。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工汁尺, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人多律。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓痴突,卻偏偏與公主長得像,于是被迫代替她去往敵國和親狼荞。 傳聞我的和親對象是個殘疾皇子辽装,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354