jvm內(nèi)存結(jié)構(gòu)組成
Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的框杜,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機制及規(guī)范塑陵。
簡要言之,jmm是jvm的一種規(guī)范,定義了jvm的內(nèi)存模型师幕。它屏蔽了各種硬件和操作系統(tǒng)的訪問差異,不像c那樣直接訪問硬件內(nèi)存诬滩,相對安全很多霹粥,它的主要目的是解決由于多線程通過共享內(nèi)存進行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致疼鸟、編譯器會對代碼指令重排序后控、處理器會對代碼亂序執(zhí)行等帶來的問題】站担可以保證并發(fā)編程場景中的原子性浩淘、可見性和有序性。
從下圖可以看出吴攒,java內(nèi)存模型分為五大數(shù)據(jù)區(qū)域馋袜,這些區(qū)域都有各自的用途以及他們的創(chuàng)建時間和銷毀時間
其中方法區(qū)和堆是所有線程共享的,棧舶斧,本地方法棧和程序虛擬機則為線程私有的欣鳖。
程序計數(shù)器(PC寄存器)
程序計數(shù)器是一塊很小的區(qū)域,它是線程獨占區(qū)域,可以認為它是線程行號的指示器茴厉。
椩筇ǎ空間
同程序計數(shù)器一樣是線程獨占區(qū)域
每個方法被執(zhí)行的時候都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧矾缓、動態(tài)連接怀酷、方法返回地址等信息,每一個方法被調(diào)用的過程就是對應(yīng)一個棧幀從虛擬機入棧到出棧過程(棧是先進后出)
看如下圖,可以知道代碼執(zhí)行是先從main方法執(zhí)行,入棧開始,然后調(diào)用add方法,add方法在進入入棧,執(zhí)行完畢之后add出棧,最后才main方法出棧,所以著對應(yīng)著一個棧是先進后出的結(jié)果(一個方法對應(yīng)一個棧幀)
且對于棧幀的詳細解釋參考 Java虛擬機運行時棧幀結(jié)構(gòu)
棧幀:是存儲數(shù)據(jù)結(jié)構(gòu),以及部分過程結(jié)果
棧幀的位置: 內(nèi)存 -> 運行時數(shù)據(jù)區(qū) -> 某個線程對應(yīng)的虛擬機棧 -> here[在這里]
棧幀大小確認時間: 是在編譯的時候就確認好了,不會在運行的時候受到數(shù)據(jù)變化的影響
需要注意的是,局部變量表所需要的空間大小是在編譯的過程中就已經(jīng)分配好了,當(dāng)進入一個方法的時候,在棧中所需要的局部變量表的空間是完成確認的,在方法運行期間是不會受到數(shù)據(jù)的影響
Java虛擬機棧可能出現(xiàn)兩種類型的異常:
1.線程請求的棧深度大于虛擬機允許的棧深度嗜闻,將拋出StackOverflowError蜕依。
2.虛擬機棧空間可以動態(tài)擴展琉雳,當(dāng)動態(tài)擴展是無法申請到足夠的空間時样眠,拋出OutOfMemory異常。
本地方法棧
本地方法棧與虛擬機棧的作用其實相差不大,最大一個區(qū)別就是虛擬機棧執(zhí)行的是java方法(也就是字節(jié)碼)翠肘,而本地方法棧則為虛擬機使用的native方法,native方法主要是調(diào)用的是c或者c++
可以用過unsafe類查看這些方法
堆空間
對于大多數(shù)應(yīng)用來說,堆是java虛擬機中最大的一塊區(qū)域,因為他存儲的的對象線程是共享的,所以在多線程情況下也需要進行同步機制檐束。
且主要存儲就是對象本身以及數(shù)組
方法區(qū)
方法區(qū)跟堆一樣,是所有線程共享的區(qū)域,在jdk8中叫元數(shù)據(jù)區(qū)域
用于存儲每個類的信息(類的名稱,方法信息,字段信息)、靜態(tài)變量束倍、常量被丧、以及編譯器編譯之后的代碼等
(注:在方法區(qū)中有一個非常重要的部分就是運行時常量池盟戏,它是每一個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM后甥桂,對應(yīng)的運行時常量池就被創(chuàng)建出來柿究。當(dāng)然并非Class文件常量池中的內(nèi)容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中黄选,比如String的intern方法笛求。)
注意:在老版jdk,方法區(qū)也被稱為永久代【因為沒有強制要求方法區(qū)必須實現(xiàn)垃圾回收糕簿,HotSpot虛擬機以永久代來實現(xiàn)方法區(qū),從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域狡孔,從而不需要專門為這部分設(shè)計垃圾回收機制懂诗。不過自從JDK7之后,Hotspot虛擬機便將運行時常量池從永久代移除了苗膝⊙旰悖】
jdk1.7開始逐步去永久代。從String.interns()方法可以看出來
String.interns()
native方法:作用是如果字符串常量池已經(jīng)包含一個等于這個String對象的字符串辱揭,則返回代表池中的這個字符串的String對象离唐,在jdk1.6及以前常量池分配在永久代中∥是裕可通過 -XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小亥鬓。
static String base = "test";
/**
* 方式一:需要全局變量,不需要設(shè)定堆空間大小
* @param args
*/
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
String str = base + base;
base = str;
list.add(str.intern());
}
}
/**
* 方式二:需要設(shè)定堆空間大小,不需要全局變量
*/
public static void main(String[] args) {
//用list保持著引用 防止full gc回收常量池
List<String> list = new ArrayList<String>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
//以上其中一個即可測試
//如果在jdk1.6環(huán)境下運行 同時限制方法區(qū)大小 將報OOM后面跟著PermGen space說明方法區(qū)OOM域庇,即常量池在永久代
//如果是jdk1.7或1.8環(huán)境下運行 同時限制堆的大小 將報heap space 即常量池在堆中
idea設(shè)置相關(guān)內(nèi)存大小設(shè)置
這邊暫時不用全局的方式,設(shè)置main方法的vm參數(shù)即可
做相關(guān)的設(shè)置,比如說設(shè)定堆大星陡辍(-Xmx5m -Xms5m -XX:-UseGCOverheadLimit)
這邊如果不設(shè)置UseGCOverheadLimit將報java.lang.OutOfMemoryError: GC overhead limit exceeded,
這個錯是因為GC占用了多余98%(默認值)的CPU時間卻只回收了少于2%(默認值)的堆空間听皿。目的是為了讓應(yīng)用終止熟呛,給開發(fā)者機會去診斷問題。一般是應(yīng)用程序在有限的內(nèi)存上創(chuàng)建了大量的臨時對象或者弱引用對象尉姨,從而導(dǎo)致該異常庵朝。雖然加大內(nèi)存可以暫時解決這個問題,但是還是強烈建議去優(yōu)化代碼又厉,后者更加有效九府,也可通過UseGCOverheadLimit避免[不推薦,這里是因為測試用覆致,并不能解決根本問題]
jdk8真正開始廢棄永久代,轉(zhuǎn)而使用元空間(Metaspace)
java虛擬機對方法區(qū)比較寬松昔逗,除了跟堆一樣可以不存在連續(xù)的內(nèi)存空間,定義空間和可擴展空間篷朵,還可以選擇不實現(xiàn)垃圾收集勾怒。