摘要:編譯參見深入理解虛擬機(jī)節(jié)走進(jìn)之一自己編譯源碼內(nèi)存模型運(yùn)行時(shí)數(shù)據(jù)區(qū)域根據(jù)虛擬機(jī)規(guī)范的規(guī)定,的內(nèi)存包括以下幾個(gè)運(yùn)運(yùn)行時(shí)數(shù)據(jù)區(qū)域程序計(jì)數(shù)器程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,他可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
點(diǎn)擊進(jìn)入我的博客 1.1 基礎(chǔ)知識(shí) 1.1.1 一些基本概念
JDK(Java Development Kit):Java語言、Java虛擬機(jī)、Java API類庫
JRE(Java Runtime Environment):Java虛擬機(jī)、Java API類庫
JIT(Just In Time):Java虛擬機(jī)內(nèi)置JIT編譯器,將字節(jié)碼編譯成本機(jī)機(jī)器代碼。
OpenJDK:OpenJDK是基于Oracle JDK基礎(chǔ)上的JDK的開源版本,但由于歷史原因缺少了部分(不太重要)的代碼。Sun JDK > SCSL > JRL > OpenJDK
JCP組織(Java Community Process):由Java開發(fā)者以及被授權(quán)者組成,負(fù)責(zé)維護(hù)和發(fā)展技術(shù)規(guī)范、參考實(shí)現(xiàn)(RI)、技術(shù)兼容包。
參見《深入理解Java虛擬機(jī)》1.6節(jié)
走進(jìn)JVM之一 自己編譯openjdk源碼
根據(jù)Java虛擬機(jī)規(guī)范(Java SE7)的規(guī)定,JVM的內(nèi)存包括以下幾個(gè)運(yùn)運(yùn)行時(shí)數(shù)據(jù)區(qū)域:
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,他可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
在虛擬機(jī)的概念模型里(僅是概念模型,各種虛擬機(jī)可能會(huì)通過一些更高效的方式去實(shí)現(xiàn)),字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
程序計(jì)數(shù)器是線程私有的,每條線程都有一個(gè)獨(dú)立的獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響。
如果線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的Native方法,這個(gè)計(jì)數(shù)器值則為空(Undefined)。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
Java虛擬機(jī)棧是線程私有的,他的生命周期與線程相同。
虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame),用于包含局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。每個(gè)方法從調(diào)用到執(zhí)行完成這個(gè)過程,就對(duì)應(yīng)這一個(gè)棧幀在虛擬機(jī)棧中的入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型和對(duì)象引用(reference類型,他不等同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令的地址) 。
其中64位長(zhǎng)度的long和double類型會(huì)占用2個(gè)局部變量空間,其余的數(shù)據(jù)類型只會(huì)占用1個(gè)局部變量空間。局部變量表所需的內(nèi)存空間在編譯期間完成內(nèi)存分配。當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的內(nèi)存空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
在Java虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀態(tài):如果線程請(qǐng)求的棧的深度大于虛擬機(jī)允許的深度,將拋出StackOverFlowError異常(棧溢出);如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展(現(xiàn)在大部分Java虛擬機(jī)都可以動(dòng)態(tài)擴(kuò)展,只不過Java虛擬機(jī)規(guī)范中也允許固定長(zhǎng)度的java虛擬機(jī)棧),如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存空間,就會(huì)拋出OutOfMemoryError異常(沒有足夠的內(nèi)存)。
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,他們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的本地Native方法服務(wù)。
在虛擬機(jī)規(guī)范中對(duì)本地方法棧中的使用方法、語言、數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至有的虛擬機(jī)(例如Sun HotSpot虛擬機(jī))直接就把本地方法棧和虛擬機(jī)棧合二為一。
本地方法棧也會(huì)拋出StackOverFlowError和OutOfmMemoryError異常。
Java堆(Java Heap)是Java虛擬機(jī)管理內(nèi)存中的最大一塊。
Java堆是所有線程共享的一塊內(nèi)存管理區(qū)域。
此內(nèi)存區(qū)域唯一目的就是存放對(duì)象的實(shí)例,幾乎所有對(duì)象實(shí)例都在堆中分配內(nèi)存。這一點(diǎn)在Java虛擬機(jī)規(guī)范中的描述是:所有對(duì)象實(shí)例以及數(shù)組都要在堆上分配,但是隨著JIT編譯器的發(fā)展與逃逸技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化發(fā)生,所有的對(duì)象都分配在堆上也不是變的那么“絕對(duì)”了。
Java堆是垃圾回收器管理的主要區(qū)域,因此很多時(shí)候也被稱為GC堆(Garbage Collected Heap)。
從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆中還可以細(xì)分為:新生代和年老代。再在細(xì)致一點(diǎn)的劃分可以分為:Eden空間、From Survivor空間、To Survivor空間等。
從內(nèi)存分配的角度來看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)。
不過無論如何劃分,都與存放內(nèi)容無關(guān),無論哪個(gè)區(qū)域存放的都是對(duì)象實(shí)例。進(jìn)一步劃分的目的是為了更好的回收內(nèi)存,或者更快的分配內(nèi)存。
Java堆可以處在物理上不連續(xù)的內(nèi)存空間,只要邏輯上是連續(xù)的即可。在實(shí)現(xiàn)上既可以實(shí)現(xiàn)成固定大小,也可以是可擴(kuò)展的大小,不過當(dāng)前主流的虛擬機(jī)都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過-Xmx和-Xms控制)。
如果在堆中沒有內(nèi)存實(shí)例完成分配,并且堆也無法在擴(kuò)展時(shí)將會(huì)拋出OutOfMemoryError異常。
方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域
方法區(qū)用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
雖然Java虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一部分,但是他還有個(gè)別名叫做Non-heap(非堆),目的應(yīng)該是與Java堆區(qū)分開來。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemoryError 異常。
Java虛擬機(jī)規(guī)范對(duì)方法區(qū)的限制非常寬松,除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不實(shí)現(xiàn)垃圾收集。相對(duì)而言,垃圾收集在這個(gè)區(qū)域是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就如永久代的名字一樣永久存在了。這區(qū)域的內(nèi)存回收目標(biāo)重要是針對(duì)常量池的回收和類型的卸載,一般來說這個(gè)內(nèi)存區(qū)域的回收‘成績(jī)’比較難以令人滿意。尤其是類型的卸載條件非??量蹋沁@部分的回收確實(shí)是必要的。在Sun公司的bug列表中,曾出現(xiàn)過的若干個(gè)嚴(yán)重的bug就是由于低版本的HotSpot虛擬機(jī)對(duì)此區(qū)域未完成回收導(dǎo)致的內(nèi)存溢出。
對(duì)于習(xí)慣在HotSpot虛擬機(jī)上開發(fā)、部署程序的開發(fā)者來說,很多人都更愿意把方法區(qū)稱為“永久代”(Permanent Generation),本質(zhì)上兩者并不等價(jià)。
僅僅是因?yàn)镠otSpot虛擬機(jī)的設(shè)計(jì)團(tuán)隊(duì)選擇把GC分代收集擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內(nèi)存,能夠省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。
對(duì)于其他虛擬機(jī)(如 BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。
對(duì)于HotSpot虛擬機(jī),根據(jù)官方發(fā)布的路線圖信息,現(xiàn)在也有放棄永久代并逐步改為采用Native Memory來實(shí)現(xiàn)方法區(qū)的規(guī)劃了,在目前已經(jīng)發(fā)布的JDK1.7的HotSpot中,已經(jīng)把原本放在永久代的字符串常量池移出。
(見1.2.2)
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致 OutOfMemoryError 異常出現(xiàn)。
在 JDK 1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的 I/O 方式,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。
顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到 Java 堆大小的限制。但是,既然是內(nèi)存,肯定還是會(huì)受到本機(jī)總內(nèi)存(包括 RAM 以及 SWAP 區(qū)或者分頁文件)大小以及處理器尋址空間的限制。服務(wù)器管理員在配置虛擬機(jī)參數(shù)時(shí),會(huì)根據(jù)實(shí)際內(nèi)存設(shè)置 -Xms 等參數(shù)信息,但經(jīng)常忽略直接內(nèi)存,使得各個(gè)內(nèi)存區(qū)域總和大于物理內(nèi)存限制(包括物理的和操作系統(tǒng)級(jí)的限制),從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn) OutOfMemoryError 異常。
1.2.2 常量池Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號(hào)引用。
字面量(Literal):文本字符串(如String str = "SpiderLucas"中SpiderLucas就是字面量)、八種基本類型的值(如int i = 0中0就是字面量)、被聲明為final的常量等;
符號(hào)引用(Symbolic References):類和方法的全限定名、字段的名稱和描述符、方法的名稱和描述符。
每個(gè)class文件都有一個(gè)class常量池。
參考資料來源:Java中的常量池、徹底弄懂字符串常量池等相關(guān)問題、Java中String字符串常量池
字符串常量池中的字符串只存在一份。
字符串常量池(String Constant Pool)是存儲(chǔ)Java String的一塊內(nèi)存區(qū)域,在JDK 6之前是放置于方法區(qū)的,在JDK 7之后放置于堆中。
在HotSpot中實(shí)現(xiàn)的字符串常量池功能的是一個(gè)StringTable類,它是一個(gè)Hash表,默認(rèn)值大小長(zhǎng)度是1009;這個(gè)StringTable在每個(gè)HotSpot VM的實(shí)例只有一份,被所有的類共享。字符串常量由一個(gè)一個(gè)字符組成,放在了StringTable上。
StringTable的長(zhǎng)度:在JDK 6中,StringTable的長(zhǎng)度是固定的,因此如果放入String Pool中的String非常多,就會(huì)造成hash沖突,導(dǎo)致鏈表過長(zhǎng),當(dāng)調(diào)用String#intern()時(shí)會(huì)需要到鏈表上一個(gè)一個(gè)找,從而導(dǎo)致性能大幅度下降;在JDK 7中,StringTable的長(zhǎng)度可以通過參數(shù)指定:-XX:StringTableSize=1024。
字符串常量池中存放的內(nèi)容:在JDK 6及之前版本中,String Pool里放的都是字符串常量;JDK 7中,由于String#intern()發(fā)生了改變,因此String Pool中也可以存放放于堆內(nèi)的字符串對(duì)象的引用。
在JDK 6中,intern()的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該常量;如果沒有找到,則將該字符串常量加入到字符串常量區(qū),也就是在字符串常量區(qū)建立該常量。
在JDK 7中,intern()的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該常量,如果沒有找到,說明該字符串常量在堆中,則處理是把堆區(qū)該對(duì)象的引用加入到字符串常量池中,以后別人拿到的是該字符串常量的引用,實(shí)際存在堆中。
String s1 = new String("Spider"); // s1 -> 堆 // 該行代碼創(chuàng)建了幾個(gè)對(duì)象 // 兩個(gè)對(duì)象(不考慮對(duì)象內(nèi)部的對(duì)象):首先創(chuàng)建了一個(gè)字符串常量池的對(duì)象,然后創(chuàng)建了堆里的對(duì)象 s1.intern(); // 字符串常量池中存在"Spider",直接返回該常量 String s2 = "Spider"; // s2 -> 字符串字符串常量池 System.out.println(s1 == s2); // false String s3 = new String("Str") + new String("ing"); // s3 -> 堆 // 該行代碼創(chuàng)建了幾個(gè)對(duì)象? // 反編譯后的代碼:String s3 = (new StringBuilder()).append(new String("Str")).append(new String("ing")).toString(); // 六個(gè)對(duì)象(不考慮對(duì)象內(nèi)部的對(duì)象):兩個(gè)字符串常量池的對(duì)象"Str"和"ing",兩個(gè)堆的對(duì)象"Str"和"ing",一個(gè)StringBuilder,一個(gè)toString方法創(chuàng)建的new String對(duì)象 s3.intern(); // 字符串常量池中沒有,在JDK 7中以后會(huì)把堆中該對(duì)象的引用放在字符串常量池中(JDK 6中創(chuàng)建一個(gè)jdk1.6中會(huì)在字符串常量池中建立該常量) String s4 = "String"; // s4 -> 堆(JDK 6:s4 -> 字符串字符串常量池) System.out.println(s3 == s4); // true(JDK6 false) String s5 = "AAA"; String s6 = "BBB"; String s7 = "AAABBB"; // s7 -> 字符串常量池 String s8 = s5 + s6; // s8 -> 堆(原因就是如上字符串+的重載) String s9 = "AAA" + "BBB"; // JVM會(huì)對(duì)此代碼進(jìn)行優(yōu)化,直接創(chuàng)建字符串常量池 System.out.println(s7 == s8); // false System.out.println(s7 == s9); // true(都指向字符串常量池)
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。
Class常量池的內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
Java虛擬機(jī)對(duì)Class文件每一部分(自然也包括常量池)的格式都有嚴(yán)格規(guī)定,每一個(gè)字節(jié)用于存儲(chǔ)哪種數(shù)據(jù)都必須符合規(guī)范的要求才會(huì)被虛擬機(jī)認(rèn)可、裝載和執(zhí)行,但對(duì)于運(yùn)行時(shí)常量池,Java 虛擬機(jī)規(guī)范沒有做任何細(xì)節(jié)的要求,不同的提供商實(shí)現(xiàn)的虛擬機(jī)可以按照自己的需要來實(shí)現(xiàn)這個(gè)內(nèi)存區(qū)域。
運(yùn)行時(shí)常量池相對(duì)于Class文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java 語言并不要求常量一定只有編譯期才能產(chǎn)生,也就是并非預(yù)置如Class文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是 String類的intern()方法。
既然運(yùn)行時(shí)常量池是方法區(qū)的一部分,自然受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常。
1.3 HotSpot中的對(duì)象 1.3.1 對(duì)象的創(chuàng)建從常量池中查找該類的符號(hào)引用,并且檢查該符號(hào)引用代表的類是否被加載、解析、初始化。如果類已經(jīng)被加載,則跳轉(zhuǎn)至3;否則跳轉(zhuǎn)至2。
執(zhí)行類的加載過程。
為新對(duì)象分配內(nèi)存空間:由于對(duì)象所需要內(nèi)存大小在類加載完成時(shí)可以確定,所以可以直接從Java堆中劃分一塊確定大小的內(nèi)存。
把分配的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),如果使用TLAB則該操作可以提前至TLAB中,這是為了保證對(duì)象的字段都被初始為默認(rèn)值。
執(zhí)行init方法,按照程序員的意愿進(jìn)行初始化。
指針碰撞:如果堆內(nèi)存是規(guī)整,已經(jīng)分配和為分配的內(nèi)存有一個(gè)指針作為分界點(diǎn),那么只需要將指針向空閑內(nèi)存移動(dòng)即可。
空閑列表:如果內(nèi)存是不規(guī)整的,虛擬機(jī)需要維護(hù)一個(gè)列表,記錄那些內(nèi)存塊是可用的。在分配的時(shí)候從足夠大的空間劃分給對(duì)象,并更新該列表。
Java堆是否規(guī)整取決于GC是否會(huì)壓縮整理,Serial、ParNew等帶Compact過程的收集器,分配算法是指針碰撞;是用CMS這種基于Mark-Sweep算法的收集器時(shí),分配算法是空閑列表。
無論是指針碰撞還是空閑列表,都有可能因?yàn)椴l(fā)而產(chǎn)生問題,解決方法有兩種:
對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上JVM采用CAS(Compare And Swap)配上失敗重試的方式保證更新操作的原子性。
把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間,每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,成為本地緩沖內(nèi)存(Tread Local Allocation Buffer,TLAB)。哪個(gè)線程需要分配內(nèi)存,就在哪個(gè)線程的TLAB上分配,只有TLAB用完了,才需要同步鎖定??梢酝ㄟ^-XX:+/-UseTLAB參數(shù)來設(shè)定。
CAS原理
一個(gè)CAS方法包含三個(gè)參數(shù)CAS(V,E,N)。V表示要更新的變量,E表示預(yù)期的值,N表示新值。只有當(dāng)V的值等于E時(shí),才會(huì)將V的值修改為N。如果V的值不等于E,說明已經(jīng)被其他線程修改了,當(dāng)前線程可以放棄此操作,也可以再次嘗試次操作直至修改成功?;谶@樣的算法,CAS操作即使沒有鎖,也可以發(fā)現(xiàn)其他線程對(duì)當(dāng)前線程的干擾(臨界區(qū)值的修改),并進(jìn)行恰當(dāng)?shù)奶幚怼?/pre> 1.3.2 對(duì)象的內(nèi)存布局在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中的存儲(chǔ)布局可以分為3部分:對(duì)象頭(Object Header)、實(shí)例數(shù)據(jù)(Instance Data)、對(duì)齊填充(Padding)。
對(duì)象頭第一部分
對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù), 如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(暫不考慮開啟壓縮指針的場(chǎng)景)中分別為32個(gè)和64個(gè)Bits,官方稱它為“Mark Word”。
對(duì)象需要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出了32、64位所能記錄的限度,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,原理是它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。
例如在32位的HotSpot虛擬機(jī)中對(duì)象未被鎖定的狀態(tài)下,Mark Word的32個(gè)Bits空間中的25Bits用于存儲(chǔ)對(duì)象哈希碼(HashCode),4Bits用于存儲(chǔ)對(duì)象分代年齡,2Bits用于存儲(chǔ)鎖標(biāo)志位,1Bit固定為0,在其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)內(nèi)容如下表所示。??
對(duì)象頭第二部分
對(duì)象頭的第二部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,換句話說,查找對(duì)象的元數(shù)據(jù)信息不一定要經(jīng)過對(duì)象本身。
如果對(duì)象是一個(gè)數(shù)組:對(duì)象頭中還需要一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。
實(shí)例數(shù)據(jù)
接下來實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也既是我們?cè)诔绦虼a里面所定義的各種類型的字段內(nèi)容。無論是從父類繼承下來的,還是在子類中定義的都需要記錄下來。
這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機(jī)默認(rèn)的分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿足這個(gè)前提條件的情況下,在父類中定義的變量會(huì)出現(xiàn)在子類之前。
如果 CompactFields參數(shù)值為true(默認(rèn)為true),那子類之中較窄的變量也可能會(huì)插入到父類變量的空隙之中。
對(duì)齊填充
第三部分對(duì)齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話說就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍。對(duì)象頭正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊的話,就需要通過對(duì)齊填充來補(bǔ)全。
1.3.3 對(duì)象的訪問定位建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對(duì)象。由于reference類型在Java虛擬機(jī)規(guī)范里面只規(guī)定了是一個(gè)指向?qū)ο蟮囊?,并沒有定義這個(gè)引用應(yīng)該通過什么種方式去定位、訪問到堆中的對(duì)象的具體位置,對(duì)象訪問方式也是取決于虛擬機(jī)實(shí)現(xiàn)而定的。
對(duì)象的兩種訪問定位方式
主流的訪問方式有使用句柄和直接指針兩種。
句柄訪問:Java堆中將會(huì)劃分出一塊內(nèi)存來作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)的具體各自的地址信息,如下圖所示。
直接指針:Java堆對(duì)象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference中存儲(chǔ)的直接就是對(duì)象地址,如下圖所示。
兩種方式比較
句柄訪問的優(yōu)勢(shì):reference中存儲(chǔ)的是穩(wěn)定句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要被修改。
直接指針的優(yōu)勢(shì):最大的好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開銷,由于對(duì)象訪問的在Java中非常頻繁,因此這類開銷積小成多也是一項(xiàng)非??捎^的執(zhí)行成本。
HotSpot虛擬機(jī):它是使用第二種方式進(jìn)行對(duì)象訪問。
但在整個(gè)軟件開發(fā)的范圍來看,各種語言、框架中使用句柄來訪問的情況也十分常見。
1.4 OOM異常分類 1.4.1 堆溢出Java堆用于存儲(chǔ)對(duì)象實(shí)例,只要不斷創(chuàng)建對(duì)象,并且保證GC Roots到對(duì)象之間有可達(dá)路徑來避免GC,那么在對(duì)象數(shù)量到達(dá)最大堆容量限制之后便會(huì)產(chǎn)生堆溢出。
/** * VM args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * 1.將堆的最小值-Xms與最大值-Xmx參數(shù)設(shè)置為一樣可以避免堆自動(dòng)擴(kuò)展 * 2.通過參數(shù)-XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機(jī)出現(xiàn)內(nèi)存異常時(shí)Dump當(dāng)前堆內(nèi)存堆轉(zhuǎn)儲(chǔ)快照 * 3.快照位置默認(rèn)為user.dir */ public class HeapOOM { static class OOMObject {} public static void main(String[] args) { // 保留引用,防止GC Listlist = new ArrayList<>(); for (;;) { list.add(new OOMObject()); } } } // 運(yùn)行結(jié)果 // java.lang.OutOfMemoryError: Java heap space // Dumping heap to java_pid72861.hprof ... // Heap dump file created [27888072 bytes in 0.086 secs] 堆轉(zhuǎn)儲(chǔ)快照
以下是JProfiler對(duì)轉(zhuǎn)儲(chǔ)快照的分析
內(nèi)存泄漏與內(nèi)存溢出
重點(diǎn):確認(rèn)內(nèi)存中的對(duì)象是否是必要的,也就是分清楚到底是出現(xiàn)了內(nèi)存泄漏(Memory Leak)還是內(nèi)存溢出(Memory Overflow)
內(nèi)存泄漏:是指程序在申請(qǐng)內(nèi)存后,無法釋放已申請(qǐng)的內(nèi)存空間,一次內(nèi)存泄漏似乎不會(huì)有大的影響,但內(nèi)存泄漏堆積后的后果就是內(nèi)存溢出。
內(nèi)存溢出:是指程序在申請(qǐng)內(nèi)存時(shí),沒有足夠的內(nèi)存空間供其使用。內(nèi)存泄漏的堆積最終會(huì)導(dǎo)致內(nèi)存溢出。
內(nèi)存泄漏的分類(按發(fā)生方式來分類)
常發(fā)性內(nèi)存泄漏:發(fā)生內(nèi)存泄漏的代碼會(huì)被多次執(zhí)行到,每次被執(zhí)行的時(shí)候都會(huì)導(dǎo)致一塊內(nèi)存泄漏。
偶發(fā)性內(nèi)存泄漏:發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會(huì)發(fā)生。常發(fā)性和偶發(fā)性是相對(duì)的。對(duì)于特定的環(huán)境,偶發(fā)性的也許就變成了常發(fā)性的。所以測(cè)試環(huán)境和測(cè)試方法對(duì)檢測(cè)內(nèi)存泄漏至關(guān)重要。
一次性內(nèi)存泄漏:發(fā)生內(nèi)存泄漏的代碼只會(huì)被執(zhí)行一次,或者由于算法上的缺陷,導(dǎo)致總會(huì)有一塊僅且一塊內(nèi)存發(fā)生泄漏。比如,在類的構(gòu)造函數(shù)中分配內(nèi)存,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存,所以內(nèi)存泄漏只會(huì)發(fā)生一次。
隱式內(nèi)存泄漏:程序在運(yùn)行過程中不停的分配內(nèi)存,但是直到結(jié)束的時(shí)候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏,因?yàn)樽罱K程序釋放了所有申請(qǐng)的內(nèi)存。但是對(duì)于一個(gè)服務(wù)器程序,需要運(yùn)行幾天,幾周甚至幾個(gè)月,不及時(shí)釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。
處理方式
如果是內(nèi)存泄漏:需要找到泄漏對(duì)象的類型信息,和對(duì)象與GC Roots的引用鏈的信息,分析GC無法自動(dòng)回收它們的原因。
如果不存在內(nèi)存泄漏,即內(nèi)存中的對(duì)象的確必須存活:那就應(yīng)當(dāng)檢查JVM的參數(shù)能否調(diào)大;從代碼上檢查是否某些對(duì)象生命周期過長(zhǎng)、持有狀態(tài)時(shí)間過長(zhǎng),嘗試減少程序運(yùn)行期的內(nèi)存消耗。
1.4.2 棧溢出在HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)棧和本地方法棧,對(duì)于HotSpot來說,雖然-Xoss參數(shù)(設(shè)置本地方法棧大小)存在,但實(shí)際上是無效的。棧容量只由-Xss參數(shù)設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常:
如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError異常。
如果虛擬機(jī)在擴(kuò)展棧時(shí)無法申請(qǐng)到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常。
這里把異常分成兩種情況,看似更加嚴(yán)謹(jǐn),但卻存在著一些互相重疊的地方:當(dāng)??臻g無法繼續(xù)分配時(shí),到底是內(nèi)存太小,還是已使用的??臻g太大,其本質(zhì)上只是對(duì)同一件事情的兩種描述而已。
StackOverflowError
/** * VM args: -Xss256k * 1. 設(shè)置-Xss參數(shù)減小棧內(nèi)存 * 2. 死遞歸增大此方法棧中本地變量表的長(zhǎng)度 */ public class SOF { int stackLength = 1; void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) { SOF sof = new SOF(); try { sof.stackLeak(); } catch (Throwable e) { System.out.println("Stack Length:" + sof.stackLength); throw e; } } } // Stack Length:2006 // Exception in thread "main" java.lang.StackOverflowError // at s1.SOF.stackLeak(SOF.java:13) // at s1.SOF.stackLeak(SOF.java:13)多線程導(dǎo)致棧OOM異常
/** * VM Args: -Xss20M * 通過不斷創(chuàng)建線程的方式產(chǎn)生OOM */ public class StackOOM { private void dontStop() { for (;;) { } } private void stackLeakByThread() { for (;;) { Thread thread = new Thread(this::dontStop); thread.start(); } } public static void main(String[] args) { new StackOOM().stackLeakByThread(); } }通過不斷創(chuàng)建線程的方式產(chǎn)生OOM異常,但是這樣產(chǎn)生的內(nèi)存溢出異常與??臻g是否足夠大并不存在任何聯(lián)系。或者準(zhǔn)確地說,在這種情況下,為每個(gè)線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。
1.4.3 方法區(qū)和運(yùn)行時(shí)常量池溢出
原因:操作系統(tǒng)分配給每個(gè)進(jìn)程的內(nèi)存是有限制的,假設(shè)操作系統(tǒng)的內(nèi)存為2GB,剩余的內(nèi)存為2GB(操作系統(tǒng)限制)減去Xmx(最大堆容量),再減去MaxPermSize(最大方法區(qū)容量),程序計(jì)數(shù)器消耗內(nèi)存很小,可以忽略掉。如果虛擬機(jī)進(jìn)程本身耗費(fèi)的內(nèi)存不計(jì)算在內(nèi),剩下的內(nèi)存就由虛擬機(jī)棧和本地方法棧“瓜分”了。所以每個(gè)線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少,建立線程時(shí)就越容易把剩下的內(nèi)存耗盡。
解決方法:如果是建立過多線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)或者更換64位虛擬機(jī)的情況下,就只能通過“減少內(nèi)存”的手段來解決內(nèi)存溢出——減少最大堆和減少棧容量來換取更多的線程。由于運(yùn)行時(shí)常量池是方法區(qū)的一部分,因此這兩個(gè)區(qū)域的溢出測(cè)試就放在一起進(jìn)行。方法區(qū)用于存放Class的相關(guān)信息,如類名、訪問修飾符、常量池、字段描述、方法描述等,所以對(duì)于動(dòng)態(tài)生成類的情況比較容易出現(xiàn)永久代的內(nèi)存溢出。對(duì)于這些區(qū)域的測(cè)試,基本的思路是運(yùn)行時(shí)產(chǎn)生大量的類去填滿方法區(qū),直到溢出。
/** * (JDK 8)VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10m * (JDK 7之前)VM Args: -XX:PermSize=10M -XX:MaxPermSize=10m */ public class MethodAreaOOM { static class OOMClass {} public static void main(final String[] args) { for (;;) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMClass.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(o, objects); } }); enhancer.create(); } } } // Exception in thread "main" java.lang.OutOfMemoryError: Metaspace // at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237) // at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377) // at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285) // at com.ankeetc.commons.Main.main(Main.java:28)方法區(qū)溢出場(chǎng)景
方法區(qū)溢出也是一種常見的內(nèi)存溢出異常,一個(gè)類要被垃圾收集器回收掉,判定條件是比較苛刻的。在經(jīng)常動(dòng)態(tài)生成大量Class的應(yīng)用中,需要特別注意類的回收狀況。這類場(chǎng)景主要有:
使用了CGLib字節(jié)碼增強(qiáng),當(dāng)前的很多主流框架,如Spring、Hibernate,在對(duì)類進(jìn)行增強(qiáng)時(shí),都會(huì)使用到CGLib這類字節(jié)碼技術(shù),增強(qiáng)的類越多,就需要越大的方法區(qū)來保證動(dòng)態(tài)生成的Class可以加載入內(nèi)存。
大量JSP或動(dòng)態(tài)產(chǎn)生JSP文件的應(yīng)用(JSP第一次運(yùn)行時(shí)需要編譯為Java類)
基于OSGi的應(yīng)用(即使是同一個(gè)類文件,被不同的加載器加載也會(huì)視為不同的類)等
JVM上的動(dòng)態(tài)語言(例如Groovy等)通常都會(huì)持續(xù)創(chuàng)建類來實(shí)現(xiàn)語言的動(dòng)態(tài)性
1.4.4 本機(jī)直接內(nèi)存溢出下面代碼越過了DirectByteBuffer類,直接通過反射獲取Unsafe實(shí)例進(jìn)行內(nèi)存分配(Unsafe類的getUnsafe()方法限制了只有引導(dǎo)類加載器才會(huì)返回實(shí)例,也就是設(shè)計(jì)者希望只有rt.jar中的類才能使用Unsafe的功能)。因?yàn)椋m然使用DirectByteBuffer分配內(nèi)存也會(huì)拋出內(nèi)存溢出異常,但它拋出異常時(shí)并沒有真正向操作系統(tǒng)申請(qǐng)分配內(nèi)存,而是通過計(jì)算得知內(nèi)存無法分配,于是手動(dòng)拋出異常,真正申請(qǐng)分配內(nèi)存的方法是unsafe.allocateMemory()。
/** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * DirectMemory容量可通過-XX:MaxDirectMemorySize指定 * 如果不指定,則默認(rèn)與Java堆最大值(-Xmx指定)一樣。 */ public class Main { private static final long _1024MB = 1024 * 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1024MB); } } } // Exception in thread "main" java.lang.OutOfMemoryError // at sun.misc.Unsafe.allocateMemory(Native Method) // at com.ankeetc.commons.Main.main(Main.java:25)DirectMemory特征
由DirectMemory導(dǎo)致的內(nèi)存溢出,一個(gè)明顯的特征是在Heap Dump文件中不會(huì)看見明顯的異常。
如果發(fā)現(xiàn)OOM之后Dump文件很小,而程序中又直接或間接使用了NIO,那就可以考慮檢查一下是不是這方面的原因。
1.5 不同版本的JDK參考資料
Java8內(nèi)存模型—永久代(PermGen)和元空間(Metaspace)
JDK8-廢棄永久代(PermGen)迎來元空間(Metaspace)
關(guān)于永久代和方法區(qū)
在 HotSpot VM 中 “PermGen Space” 其實(shí)指的就是方法區(qū)
“PermGen Space” 和方法區(qū)有本質(zhì)的區(qū)別。前者是 JVM 規(guī)范的一種實(shí)現(xiàn)(HotSpot),后者是 JVM 的規(guī)范。
只有 HotSpot 才有 “PermGen Space”,而對(duì)于其他類型的虛擬機(jī),如 JRockit、J9并沒有 “PermGen Space”。
不同版本JDK總結(jié)
JDK 7之后將字符串常量池由永久代轉(zhuǎn)移到堆中
JDK 8中, HotSpot 已經(jīng)沒有 “PermGen space”這個(gè)區(qū)間了,取而代之是一個(gè)叫做 Metaspace(元空間) 的東西。
元空間的本質(zhì)和永久代類似,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)。不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制。
-XX:MetaspaceSize:初始空間大小,達(dá)到該值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載。同時(shí)GC會(huì)對(duì)該值進(jìn)行調(diào)整——如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時(shí),適當(dāng)提高該值。
-XX:MaxMetaspaceSize:最大空間,默認(rèn)是沒有限制的。
-XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空間容量的百分比,減少為分配空間所導(dǎo)致的垃圾收集
-XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空間容量的百分比,減少為釋放空間所導(dǎo)致的垃圾收集
PermSize、MaxPermSize參數(shù)已移除
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/72764.html
摘要:虛擬機(jī)包括一套字節(jié)碼指令集一組寄存器一個(gè)棧一個(gè)垃圾回收堆和一個(gè)存儲(chǔ)方法域。而使用虛擬機(jī)是實(shí)現(xiàn)這一特點(diǎn)的關(guān)鍵。虛擬機(jī)在執(zhí)行字節(jié)碼時(shí),把字節(jié)碼解釋成具體平臺(tái)上的機(jī)器指令執(zhí)行。此內(nèi)存區(qū)域是唯一一個(gè)在虛擬機(jī)規(guī)范中沒有規(guī)定任何情況的區(qū)域。 1、 什么是JVM? JVM是Java Virtual Machine(Java虛擬機(jī))的縮寫,JVM是一種用于計(jì)算設(shè)備的規(guī)范,它是一個(gè)虛構(gòu)出來的計(jì)算機(jī),...
摘要:如問到是否使用某框架,實(shí)際是是問該框架的使用場(chǎng)景,有什么特點(diǎn),和同類可框架對(duì)比一系列的問題。這兩個(gè)方向的區(qū)分點(diǎn)在于工作方向的側(cè)重點(diǎn)不同。 [TOC] 這是一份來自嗶哩嗶哩的Java面試Java面試 32個(gè)核心必考點(diǎn)完全解析(完) 課程預(yù)習(xí) 1.1 課程內(nèi)容分為三個(gè)模塊 基礎(chǔ)模塊: 技術(shù)崗位與面試 計(jì)算機(jī)基礎(chǔ) JVM原理 多線程 設(shè)計(jì)模式 數(shù)據(jù)結(jié)構(gòu)與算法 應(yīng)用模塊: 常用工具集 ...
摘要:我的是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)。因?yàn)槲倚睦砗芮宄?,我的目?biāo)是阿里。所以在收到阿里之后的那晚,我重新規(guī)劃了接下來的學(xué)習(xí)計(jì)劃,將我的短期目標(biāo)更新成拿下阿里轉(zhuǎn)正。 我的2017是忙碌的一年,從年初備戰(zhàn)實(shí)習(xí)春招,年三十都在死磕JDK源碼,三月份經(jīng)歷了阿里五次面試,四月順利收到實(shí)習(xí)offer。然后五月懷著忐忑的心情開始了螞蟻金...
摘要:堆內(nèi)存的劃分在里面的示意圖垃圾回收一判斷對(duì)象是否要回收的方法可達(dá)性分析法可達(dá)性分析法通過一系列對(duì)象作為起點(diǎn)進(jìn)行搜索,如果在和一個(gè)對(duì)象之間沒有可達(dá)路徑,則稱該對(duì)象是不可達(dá)的。 工作之余,想總結(jié)一下JVM相關(guān)知識(shí)。 Java運(yùn)行時(shí)數(shù)據(jù)區(qū): Java虛擬機(jī)在執(zhí)行Java程序的過程中會(huì)將其管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,這些區(qū)域有各自的用途、創(chuàng)建和銷毀的時(shí)間,有些區(qū)域隨虛擬機(jī)進(jìn)程的啟動(dòng)而...
摘要:一內(nèi)存區(qū)域虛擬機(jī)在運(yùn)行時(shí),會(huì)把內(nèi)存空間分為若干個(gè)區(qū)域,根據(jù)虛擬機(jī)規(guī)范版的規(guī)定,虛擬機(jī)所管理的內(nèi)存區(qū)域分為如下部分方法區(qū)堆內(nèi)存虛擬機(jī)棧本地方法棧程序計(jì)數(shù)器。前言 在JVM的管控下,Java程序員不再需要管理內(nèi)存的分配與釋放,這和在C和C++的世界是完全不一樣的。所以,在JVM的幫助下,Java程序員很少會(huì)關(guān)注內(nèi)存泄露和內(nèi)存溢出的問題。但是,一旦JVM發(fā)生這些情況的時(shí)候,如果你不清楚JVM內(nèi)存的...
閱讀 3266·2021-09-06 15:02
閱讀 2300·2019-08-30 15:48
閱讀 3500·2019-08-29 11:08
閱讀 3340·2019-08-26 13:55
閱讀 2512·2019-08-26 13:35
閱讀 3214·2019-08-26 12:11
閱讀 2660·2019-08-26 11:48
閱讀 956·2019-08-26 11:42