深入理解運(yùn)行時(shí)數(shù)據(jù)區(qū)
代碼示例:
1. JVM 向操作系統(tǒng)申請(qǐng)內(nèi)存:
JVM 第一步就是通過(guò)配置參數(shù)或者默認(rèn)配置參數(shù)向操作系統(tǒng)申請(qǐng)內(nèi)存空間,根據(jù)內(nèi)存大小找到具體的內(nèi)存分配表嗜价,然后把內(nèi)存段的起始地址和終止地址分配給 JVM艇抠,接下來(lái) JVM 就進(jìn)行內(nèi)部分配。
2. JVM 獲得內(nèi)存空間后久锥,會(huì)根據(jù)配置參數(shù)分配堆家淤、棧以及方法區(qū)的內(nèi)存大小。
-Xms30m -Xmx30m -Xss1m -XX:MaxMetaspaceSize=30m
3. 類(lèi)加載:
這里主要是把 class 放入方法區(qū)瑟由、還有 class 中的靜態(tài)變量和常量也要放入方法區(qū)絮重。
4. 執(zhí)行方法及創(chuàng)建對(duì)象:
啟動(dòng) main 線(xiàn)程,執(zhí)行 main 方法歹苦,開(kāi)始執(zhí)行第一行代碼青伤。此時(shí)堆內(nèi)存中會(huì)創(chuàng)建一個(gè) student 對(duì)象,對(duì)象引用 student 就存放在棧中殴瘦。
后續(xù)代碼中遇到 new 關(guān)鍵字狠角,會(huì)再創(chuàng)建一個(gè) student 對(duì)象,對(duì)象引用 student 就存放在棧中蚪腋。
總結(jié)一下 JVM 運(yùn)行內(nèi)存的整體流程:
JVM 在操作系統(tǒng)上啟動(dòng)丰歌,申請(qǐng)內(nèi)存,先進(jìn)行運(yùn)行時(shí)數(shù)據(jù)區(qū)的初始化屉凯,然后把類(lèi)加載到方法區(qū)动遭,最后執(zhí)行方法。
方法的執(zhí)行和退出過(guò)程在內(nèi)存上的體現(xiàn)上就是虛擬機(jī)棧中棧幀的入棧和出棧神得。
同時(shí)在方法的執(zhí)行過(guò)程中創(chuàng)建的對(duì)象一般情況下都是放在堆中厘惦,最后堆中的對(duì)象也是需要進(jìn)行垃圾回收清理的。
從底層深入理解運(yùn)行時(shí)數(shù)據(jù)區(qū)
堆空間分代劃分
堆被劃分為新生代和老年代(Tenured)哩簿,新生代又被進(jìn)一步劃分為 Eden 和 Survivor 區(qū)宵蕉,最后 Survivor 由 From Survivor 和 To Survivor 組成。
GC 概念
GC- Garbage Collection 垃圾回收节榜,在 JVM 中是自動(dòng)化的垃圾回收機(jī)制羡玛,我們一般不用去關(guān)注,在 JVM 中 GC 的重要區(qū)域是堆空間宗苍。
我們也可以通過(guò)一些額外方式主動(dòng)發(fā)起它稼稿,比如 System.gc(),主動(dòng)發(fā)起薄榛。(項(xiàng)目中切記不要使用)
JHSDB 工具
JHSDB 是一款基于服務(wù)性代理實(shí)現(xiàn)的進(jìn)程外調(diào)試工具。服務(wù)性代理是 HotSpot 虛擬機(jī)中一組用于映射 Java 虛擬機(jī)運(yùn)行信息的让歼,主要基于 Java 語(yǔ)言實(shí)現(xiàn)的API 集合敞恋。
JDK1.8 的開(kāi)啟方式
開(kāi)啟 HSDB 工具
Jdk1.8 啟動(dòng) JHSDB 的時(shí)候必須將 sawindbg.dll(一般會(huì)在 JDK 的目錄下)復(fù)制到對(duì)應(yīng)目錄的 jre 下(注意在 win 上安裝了 JDK1.8 后往往同級(jí)目錄下有一個(gè)jre 的目錄)。
然后到目錄:C:\Program Files\Java\jdk1.8.0_101\lib 進(jìn)入命令行谋右,執(zhí)行 java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
代碼改造
VM 參數(shù)加入:
-XX:+UseConcMarkSweepGC
JHSDB 中查看對(duì)象
實(shí)例代碼啟動(dòng)
因?yàn)?JVM 啟動(dòng)有一個(gè)進(jìn)程硬猫,需要借助一個(gè)命令 jps 查找到對(duì)應(yīng)程序的進(jìn)程
從底層深入理解運(yùn)行時(shí)數(shù)據(jù)區(qū)(總結(jié))
深入辨析堆和棧
?功能:
以棧幀的方式存儲(chǔ)方法調(diào)用的過(guò)程,并存儲(chǔ)方法調(diào)用過(guò)程中基本數(shù)據(jù)類(lèi)型的變量(int改执、short啸蜜、long、byte辈挂、float衬横、double、boolean终蒂、char 等)以及對(duì)象的引用變量冕香,其內(nèi)存分配在棧上,變量出了作用域就會(huì)自動(dòng)釋放后豫;
而堆內(nèi)存用來(lái)存儲(chǔ) Java 中的對(duì)象悉尾。無(wú)論是成員變量,局部變量挫酿,還是類(lèi)變量构眯,它們指向的對(duì)象都存儲(chǔ)在堆內(nèi)存中。
線(xiàn)程獨(dú)享還是共享:
棧內(nèi)存歸屬于單個(gè)線(xiàn)程早龟,每個(gè)線(xiàn)程都會(huì)有一個(gè)棧內(nèi)存惫霸,其存儲(chǔ)的變量只能在其所屬線(xiàn)程中可見(jiàn),即棧內(nèi)存可以理解成線(xiàn)程的私有內(nèi)存葱弟;
堆內(nèi)存中的對(duì)象對(duì)所有線(xiàn)程可見(jiàn)壹店。堆內(nèi)存中的對(duì)象可以被所有線(xiàn)程訪(fǎng)問(wèn)。?
空間大兄ゼ印:
棧的內(nèi)存要遠(yuǎn)遠(yuǎn)小于堆內(nèi)存硅卢。
虛擬機(jī)內(nèi)存優(yōu)化技術(shù)
棧的優(yōu)化技術(shù)——棧幀之間數(shù)據(jù)的共享
在一般的模型中,兩個(gè)不同的棧幀的內(nèi)存區(qū)域是獨(dú)立的藏杖,但是大部分的 JVM 在實(shí)現(xiàn)中會(huì)進(jìn)行一些優(yōu)化将塑,使得兩個(gè)棧幀出現(xiàn)一部分重疊。(主要體現(xiàn)在方法中有參數(shù)傳遞的情況)蝌麸,讓下面棧幀的操作數(shù)棧和上面棧幀的部分局部變量重疊在一起点寥,這樣做不但節(jié)約了一部分空間,更加重要的是在進(jìn)行方法調(diào)用時(shí)就可以直接公用一部分?jǐn)?shù)據(jù)来吩,無(wú)需進(jìn)行額外的參數(shù)復(fù)制傳遞了敢辩。
內(nèi)存溢出
棧溢出
參數(shù):-Xss1m蔽莱, 具體默認(rèn)值需要查看官網(wǎng):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BABHDABI
HotSpot 版本中棧的大小是固定的,是不支持拓展的戚长。
java.lang.StackOverflowError 一般的方法調(diào)用是很難出現(xiàn)的盗冷,如果出現(xiàn)了可能會(huì)是無(wú)限遞歸。
虛擬機(jī)棧帶給我們的啟示:方法的執(zhí)行因?yàn)橐虬蓷E历葛,所以天生要比實(shí)現(xiàn)同樣功能的循環(huán)慢,所以樹(shù)的遍歷算法中:遞歸和非遞歸(循環(huán)來(lái)實(shí)現(xiàn))都有存在的意義嘀略。遞歸代碼簡(jiǎn)潔恤溶,非遞歸代碼復(fù)雜但是速度較快。
OutOfMemoryError:不斷建立線(xiàn)程帜羊,JVM 申請(qǐng)棧內(nèi)存咒程,機(jī)器沒(méi)有足夠的內(nèi)存。(一般演示不出讼育,演示出來(lái)機(jī)器也死了)
同時(shí)要注意帐姻,棧區(qū)的空間 JVM 沒(méi)有辦法去限制的,因?yàn)?JVM 在運(yùn)行過(guò)程中會(huì)有線(xiàn)程不斷的運(yùn)行奶段,沒(méi)辦法限制饥瓷,所以只限制單個(gè)虛擬機(jī)棧的大小。
堆溢出
內(nèi)存溢出:申請(qǐng)內(nèi)存空間,超出最大堆內(nèi)存空間痹籍。
如果是內(nèi)存溢出呢铆,則通過(guò) 調(diào)大 -Xms,-Xmx 參數(shù)蹲缠。
如果不是內(nèi)存泄漏棺克,就是說(shuō)內(nèi)存中的對(duì)象卻是都是必須存活的,那么就應(yīng)該檢查 JVM 的堆參數(shù)設(shè)置线定,與機(jī)器的內(nèi)存對(duì)比娜谊,看是否還有可以調(diào)整的空間,再?gòu)拇a上檢查是否存在某些對(duì)象生命周期過(guò)長(zhǎng)斤讥、持有狀態(tài)時(shí)間過(guò)長(zhǎng)纱皆、存儲(chǔ)結(jié)構(gòu)設(shè)計(jì)不合理等情況,盡量減少程序運(yùn)行時(shí)的內(nèi)存消耗芭商。?
方法區(qū)溢出
(1) 運(yùn)行時(shí)常量池溢出
(2)方法區(qū)中保存的 Class 對(duì)象沒(méi)有被及時(shí)回收掉或者 Class 信息占用的內(nèi)存超過(guò)了我們配置抹剩。
注意 Class 要被回收,條件比較苛刻(僅僅是可以蓉坎,不代表必然澳眷,因?yàn)檫€有一些參數(shù)可以進(jìn)行控制):
1、該類(lèi)所有的實(shí)例都已經(jīng)被回收蛉艾,也就是堆中不存在該類(lèi)的任何實(shí)例钳踊。
2衷敌、 加載該類(lèi)的 ClassLoader 已經(jīng)被回收。
3拓瞪、 該類(lèi)對(duì)應(yīng)的 java.lang.Class 對(duì)象沒(méi)有在任何地方被引用缴罗,無(wú)法在任何地方通過(guò)反射訪(fǎng)問(wèn)該類(lèi)的方法。
cglib 是一個(gè)強(qiáng)大的祭埂,高性能面氓,高質(zhì)量的 Code 生成類(lèi)庫(kù),它可以在運(yùn)行期擴(kuò)展 Java 類(lèi)與實(shí)現(xiàn) Java 接口蛆橡。
CGLIB 包的底層是通過(guò)使用一個(gè)小而快的字節(jié)碼處理框架 ASM舌界,來(lái)轉(zhuǎn)換字節(jié)碼并生成新的類(lèi)。除了 CGLIB 包泰演,腳本語(yǔ)言例如 Groovy 和 BeanShell呻拌,也是使用 ASM 來(lái)生成 java 的字節(jié)碼。當(dāng)然不鼓勵(lì)直接使用 ASM睦焕,因?yàn)樗竽惚仨殞?duì) JVM 內(nèi)部結(jié)構(gòu)包括 class 文件的格式和指令集都很熟悉藐握。
本機(jī)直接內(nèi)存溢出
直接內(nèi)存的容量可以通過(guò) MaxDirectMemorySize 來(lái)設(shè)置(默認(rèn)與堆內(nèi)存最大值一樣),所以也會(huì)出現(xiàn) OOM 異常垃喊;
由直接內(nèi)存導(dǎo)致的內(nèi)存溢出猾普,一個(gè)比較明顯的特征是在 HeapDump 文件中不會(huì)看見(jiàn)有什么明顯的異常情況,如果發(fā)生了 OOM本谜,同時(shí) Dump 文件很小抬闷,可以考慮重點(diǎn)排查下直接內(nèi)存方面的原因。
常量池
Class 常量池(靜態(tài)常量池)
在 class 文件中除了有類(lèi)的版本耕突、字段笤成、方法和接口等描述信息外,還有一項(xiàng)信息是常量池 (Constant Pool Table)眷茁,用于存放編譯期間生成的各種字面量和符號(hào)引用炕泳。
字面量:給基本類(lèi)型變量賦值的方式就叫做字面量或者字面值。
比如:String a=“b” 上祈,這里“b”就是字符串字面量培遵,同樣類(lèi)推還有整數(shù)字面值、浮點(diǎn)類(lèi)型字面量登刺、字符字面量籽腕。
符號(hào)引用 :符號(hào)引用以一組符號(hào)來(lái)描述所引用的目標(biāo)。符號(hào)引用可以是任何形式的字面量纸俭,JAVA 在編譯的時(shí)候每個(gè) java 類(lèi)都會(huì)被編譯成一個(gè) class文件皇耗,但在編譯的時(shí)候虛擬機(jī)并不知道所引用類(lèi)的地址(實(shí)際地址),就用符號(hào)引用來(lái)代替揍很,而在類(lèi)的解析階段(后續(xù) JVM 類(lèi)加載會(huì)具體講到)就是為了把這個(gè)符號(hào)引用轉(zhuǎn)化成為真正的地址的階段郎楼。
一個(gè) java 類(lèi)(假設(shè)為 People 類(lèi))被編譯成一個(gè) class 文件時(shí)万伤,如果 People 類(lèi)引用了 Tool 類(lèi),但是在編譯時(shí) People 類(lèi)并不知道引用類(lèi)的實(shí)際內(nèi)存地址呜袁,因此只能使用符號(hào)引用(org.simple.Tool)來(lái)代替敌买。而在類(lèi)裝載器裝載 People 類(lèi)時(shí),此時(shí)可以通過(guò)虛擬機(jī)獲取 Tool 類(lèi)的實(shí)際內(nèi)存地址阶界,因此便可以既將符號(hào)org.simple.Tool 替換為 Tool 類(lèi)的實(shí)際內(nèi)存地址虹钮。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是每一個(gè)類(lèi)或接口的常量池(Constant_Pool)的運(yùn)行時(shí)表示形式,它包括了若干種不同的常量:從編譯期可知的數(shù)值字面量到必須運(yùn)行期解析后才能獲得的方法或字段引用膘融。(這個(gè)是虛擬機(jī)規(guī)范中的描述芙粱,很生澀)
運(yùn)行時(shí)常量池是在類(lèi)加載完成之后,將 Class 常量池中的符號(hào)引用值轉(zhuǎn)存到運(yùn)行時(shí)常量池中托启,類(lèi)在解析之后宅倒,將符號(hào)引用替換成直接引用攘宙。運(yùn)行時(shí)常量池在 JDK1.7 版本之后屯耸,就移到堆內(nèi)存中了,這里指的是物理空間诱告,而邏輯上還是屬于方法區(qū)(方法區(qū)是邏輯分區(qū))措伐。
在 JDK1.8 中踪区,使用元空間代替永久代來(lái)實(shí)現(xiàn)方法區(qū),但是方法區(qū)并沒(méi)有改變多矮,所謂"Your father will always be your father"。變動(dòng)的只是方法區(qū)中內(nèi)容的物理存放位置哈打,但是運(yùn)行時(shí)常量池和字符串常量池被移動(dòng)到了堆中塔逃。但是不論它們物理上如何存放,邏輯上還是屬于方法區(qū)的料仗。?
字符串常量池
字符串常量池這個(gè)概念是最有爭(zhēng)議的湾盗,翻閱了虛擬機(jī)規(guī)范等很多正式文檔,發(fā)現(xiàn)沒(méi)有這個(gè)概念的官方定義立轧,所以與運(yùn)行時(shí)常量池的關(guān)系不去抬杠格粪,我們從它的作用和 JVM 設(shè)計(jì)它用于解決什么問(wèn)題的點(diǎn)來(lái)分析它。
以 JDK1.8 為例氛改,字符串常量池是存放在堆中帐萎,并且與 java.lang.String 類(lèi)有很大關(guān)系。設(shè)計(jì)這塊內(nèi)存區(qū)域的原因在于:String 對(duì)象作為 Java 語(yǔ)言中重要的數(shù)據(jù)類(lèi)型胜卤,是內(nèi)存中占據(jù)空間最大的一個(gè)對(duì)象疆导。高效地使用字符串,可以提升系統(tǒng)的整體性能葛躏。
所以要徹底弄懂是鬼,我們的重心其實(shí)在于深入理解 String肤舞。
String
String 類(lèi)分析(JDK1.8)
String 對(duì)象是對(duì) char 數(shù)組進(jìn)行了封裝實(shí)現(xiàn)的對(duì)象,主要有 2 個(gè)成員變量:char 數(shù)組均蜜,hash 值李剖。
String 對(duì)象的不可變性
了解了 String 對(duì)象的實(shí)現(xiàn)后,你有沒(méi)有發(fā)現(xiàn)在實(shí)現(xiàn)代碼中 String 類(lèi)被 final 關(guān)鍵字修飾了囤耳,而且變量 char 數(shù)組也被 final 修飾了篙顺。
我們知道類(lèi)被 final 修飾代表該類(lèi)不可繼承,而 char[]被 final+private 修飾充择,代表了 String 對(duì)象不可被更改德玫。Java 實(shí)現(xiàn)的這個(gè)特性叫作 String 對(duì)象的不可變性,即 String 對(duì)象一旦創(chuàng)建成功椎麦,就不能再對(duì)它進(jìn)行改變观挎。
Java 這樣做的好處在哪里呢?
第一, 保證 String 對(duì)象的安全性。假設(shè) String 對(duì)象是可變的,那么 String 對(duì)象將可能被惡意修改。
第二碾局, 保證 hash 屬性值不會(huì)頻繁變更,確保了唯一性,使得類(lèi)似 HashMap 容器才能實(shí)現(xiàn)相應(yīng)的 key-value 緩存功能僧诚。
第三, 可以實(shí)現(xiàn)字符串常量池。在 Java 中,通常有兩種創(chuàng)建字符串對(duì)象的方式怖侦,一種是通過(guò)字符串常量的方式創(chuàng)建艳悔,如 String str=“abc”疾忍;另一種是字符串變量通過(guò) new 形式的創(chuàng)建杨幼,如 String str = new String(“abc”)。
String 的創(chuàng)建方式及內(nèi)存分配的方式
1、String str=“abc”差购;
當(dāng)代碼中使用這種方式創(chuàng)建字符串對(duì)象時(shí)四瘫,JVM 首先會(huì)檢查該對(duì)象是否在字符串常量池中,如果在欲逃,就返回該對(duì)象引用找蜜,否則新的字符串將在常量池中被創(chuàng)建。這種方式可以減少同一個(gè)值的字符串對(duì)象的重復(fù)創(chuàng)建稳析,節(jié)約內(nèi)存锹杈。(str 只是一個(gè)引用)
2、String str = new String(“abc”)
首先在編譯類(lèi)文件時(shí)迈着,"abc"常量字符串將會(huì)放入到常量結(jié)構(gòu)中竭望,在類(lèi)加載時(shí),“abc"將會(huì)在常量池中創(chuàng)建裕菠;其次咬清,在調(diào)用 new 時(shí),JVM 命令將會(huì)調(diào)用 String的構(gòu)造函數(shù)奴潘,同時(shí)引用常量池中的"abc” 字符串旧烧,在堆內(nèi)存中創(chuàng)建一個(gè) String 對(duì)象;最后画髓,str 將引用 String 對(duì)象掘剪。
3、 使用 new奈虾,對(duì)象會(huì)創(chuàng)建在堆中夺谁,同時(shí)賦值的話(huà),會(huì)在常量池中創(chuàng)建一個(gè)字符串對(duì)象肉微,復(fù)制到堆中匾鸥。
具體的復(fù)制過(guò)程是先將常量池中的字符串壓入棧中,在使用 String 的構(gòu)造方法是碉纳,會(huì)拿到棧中的字符串作為構(gòu)方法的參數(shù)勿负。
這個(gè)構(gòu)造函數(shù)是一個(gè) char 數(shù)組的賦值過(guò)程,而不是 new 出來(lái)的劳曹,所以是引用了常量池中的字符串對(duì)象奴愉。存在引用關(guān)系。
public class Location {
? ?private String city;
? ?private String region;
}
4铁孵、String str2= "ab"+ "cd"+ "ef";
編程過(guò)程中锭硼,字符串的拼接很常見(jiàn)。 String 對(duì)象是不可變的库菲,如果我們使用 String 對(duì)象相加账忘,拼接我們想要的字符串,是不是就會(huì)產(chǎn)生多個(gè)對(duì)象呢?
分析代碼可知:首先會(huì)生成 ab 對(duì)象鳖擒,再生成 abcd 對(duì)象溉浙,最后生成 abcdef 對(duì)象,從理論上來(lái)說(shuō)蒋荚,這段代碼是低效的戳稽。
編譯器自動(dòng)優(yōu)化了這行代碼,編譯后的代碼期升,你會(huì)發(fā)現(xiàn)編譯器自動(dòng)優(yōu)化了這行代碼惊奇,如下:
String str= "abcdef";
5、大循環(huán)使用+
intern
String 的 intern 方法播赁,如果常量池中有相同值颂郎,就會(huì)重復(fù)使用該對(duì)象,返回對(duì)象引用容为。
1乓序、new Sting() 會(huì)在堆內(nèi)存中創(chuàng)建一個(gè) a 的 String 對(duì)象,"king"將會(huì)在常量池中創(chuàng)建坎背。
2替劈、在調(diào)用 intern 方法之后,會(huì)去常量池中查找是否有等于該字符串對(duì)象的引用得滤,有就返回引用陨献。
3、調(diào)用 new Sting() 會(huì)在堆內(nèi)存中創(chuàng)建一個(gè) b 的 String 對(duì)象懂更。
4眨业、在調(diào)用 intern 方法之后,會(huì)去常量池中查找是否有等于該字符串對(duì)象的引用膜蛔,有就返回引用坛猪。
所以 a 和 b 引用的是同一個(gè)對(duì)象脖阵。