Java虛擬機(jī)理解-內(nèi)存管理

運(yùn)行時數(shù)據(jù)區(qū)域

jdk 1.8之前與之后的內(nèi)存模型有差異,方法區(qū)有變化(https://cloud.tencent.com/developer/article/1470519)蜈漓。

jdk1.8前

java的內(nèi)存數(shù)據(jù)區(qū)域劃分:

  • 程序計數(shù)器
  • 虛擬機(jī)棧
  • 本地方法棧
  • 方法區(qū)

程序計數(shù)器(Program Counter Register)

理解為當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器工作時通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令惠况,分支、循環(huán)宁仔、跳轉(zhuǎn)稠屠、異常、線程恢復(fù)等基礎(chǔ)功能依賴于此翎苫。
每個線程獨(dú)立存儲权埠,互不影響,生命周期與線程相同
java方法的計數(shù)器內(nèi)容時正在執(zhí)行的虛擬機(jī)字節(jié)碼的指令地址煎谍,Native方法則是空(Undefined)攘蔽,此區(qū)域無OOM。

虛擬機(jī)棧(Java Virtual Machine Stacks)

線程私有呐粘,生命周期與線程相同满俗。
描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表转捕、操作數(shù)棧、動態(tài)鏈接唆垃、方法出口等信息五芝。每個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程辕万。
常說的Java內(nèi)存區(qū)分為堆(Heap)棧(Stack)兩塊比較粗糙枢步,實(shí)際的內(nèi)存劃分復(fù)雜很多。常用的對象內(nèi)存分配關(guān)系最密切的內(nèi)存區(qū)域是這兩塊渐尿,其中的棧在這里就是指虛擬機(jī)棧的局部變量表部分醉途。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型、對象引用(reference類型砖茸,不等同于對象本身隘擎,可能是指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€代表對象的句柄或者其他與此對象相關(guān)的位置)和returnAddress類型(指向了一條字節(jié)碼指令地址)
64位的long和double占用2個局部變量空間(Slot)凉夯,其他占用一個货葬。
這個區(qū)域有兩種異常:
如果線程請求的棧深度大于虛擬機(jī)允許的深度,將拋出StackOverflowError恍涂。
如果虛擬機(jī)可以動態(tài)擴(kuò)展內(nèi)存,當(dāng)擴(kuò)展時無法申請到足夠內(nèi)存是植榕,會拋出OutOfMemoryError再沧。

本地方法棧(Native Method Stack)

與虛擬機(jī)棧的作用相似,虛擬機(jī)棧執(zhí)行的是java方法尊残,而本地方法棧執(zhí)行的是Native方法炒瘸。 Sun HotSpot將本地方法棧與虛擬機(jī)棧合二為一。
拋出的異常也相同

Java堆(Java Heap)

被所有線程共享寝衫,在虛擬機(jī)啟動時創(chuàng)建顷扩。唯一的目的就是存放對象,幾乎所有的對象實(shí)例都在這里分配內(nèi)存(棧上分配慰毅,標(biāo)量替換等優(yōu)化技術(shù)會有影響)(Java虛擬機(jī)規(guī)范原文 The heap is the runtime data area from which memory for all class instances and arrays is allocated)隘截。可以通過(-Xmx -Xms控制堆的擴(kuò)展)
堆是GC管理的主要區(qū)域汹胃,現(xiàn)在的收集器基本都采用分待收集算法婶芭,所以堆還可以分為:新生代和老年代;再細(xì)一點(diǎn)分為Eden空間着饥、From Survivor空間犀农、To Survivor空間等。從內(nèi)存分配的角度來看宰掉,線程共享的Java堆中可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)

方法區(qū)(Method Area)

被所有線程共享的內(nèi)存區(qū)域呵哨,用于存儲被虛擬機(jī)加載的類信息赁濒、常量、靜態(tài)變量孟害、即時編譯期編譯后的代碼等數(shù)據(jù)拒炎。Java虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它別名是Non-Heap(非堆)纹坐。
很多人將方法區(qū)稱為永久代(Permanent Generation)枝冀,本質(zhì)上兩者并不等價,僅僅因?yàn)镠otSpot設(shè)計團(tuán)隊(duì)將GC分代收集擴(kuò)展至方法區(qū)耘子,或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已果漾。這樣HotSpot的GC可以像管理Java堆一樣管理這部分內(nèi)存,省去寫專門代碼谷誓。其他虛擬機(jī)(JRockit绒障,J9)是不存在永久代的概念。
但是這樣容易出現(xiàn)OOM捍歪,存在(-XX:MaxPermSize上限户辱,其他的VM只要沒有觸及進(jìn)程可用內(nèi)存上限就不會有問題),因此有極少數(shù)方法可能會出現(xiàn)問題(String.intern())糙臼,這個區(qū)域的內(nèi)存回收主要針對常量池的回收和對類型的卸載庐镐。

運(yùn)行時常量池(Runtime Constant Pool)

是方法區(qū)的一部分,Class文件中除了有類的版本变逃、字段必逆、方法、接口等描述信息外揽乱,還有一項(xiàng)信息是常量池(Constant Pool Table)名眉,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時常量池中存放凰棉。
JVM對于Class文件每一個字節(jié)用于存儲哪種數(shù)據(jù)都必須符合規(guī)范损拢,對于運(yùn)行時常量池沒有任何細(xì)節(jié)要求。運(yùn)行時常量池具有動態(tài)性撒犀,常量并非編譯時產(chǎn)生福压,運(yùn)行時也可以產(chǎn)生新的常量,如String.intern()

直接內(nèi)存(Direct Memory)

并非虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分或舞,也不是虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域隧膏。
JDK1.4中加入了NIO(New Input/Output),引入了基于通道(Channel)與緩沖區(qū)(Buffer)的IO方式嚷那,使用Native函數(shù)庫直接分配堆外內(nèi)存胞枕,然后通過存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。這部分是不受到Java堆大小的限制魏宽。

HotSpot虛擬機(jī)對象

對象的創(chuàng)建

普通對象的創(chuàng)建(不包括數(shù)組和Class對象)腐泻,當(dāng)虛擬機(jī)遇到new指令時决乎,首先檢查這個指令的參數(shù)能否在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否被加載派桩、解析和初始化過构诚。如果沒有會先執(zhí)行相應(yīng)的類加載過程。
類加載檢查通過后铆惑,VM為新生對象分配內(nèi)存范嘱。對象所需的內(nèi)存大小在加載后可以完全確定。若java分配和空閑內(nèi)存區(qū)域是絕對規(guī)整的员魏,那分配過程僅僅是將指針向空閑空間移動一個與對象大小相等的距離丑蛤,這個稱之為“指針碰撞(Bump the Pointer)”。如果不是規(guī)整的撕阎,那VM要維護(hù)一個列表記錄可用內(nèi)存受裹,分配時找到足夠大的內(nèi)存空間分配同時更新列表,這個稱之為“空閑列表(Free List)”虏束。
Java堆是否規(guī)整是由GC是否帶有壓縮整理功能決定棉饶,所以Serial,ParNew等帶Compact過程的收集器使用的是指針碰撞镇匀,CMS這種基于Mark-Sweep算法的收集器采用空閑列表照藻。
并發(fā)情況下的線程安全考慮有兩種方案,
一種是堆內(nèi)存分配空間的動作進(jìn)行同步處理汗侵,虛擬機(jī)會采用CAS(compare and swap)配上失敗重試的方式保證更新操作的原子性幸缕。【CAS 操作包含三個操作數(shù) —— 內(nèi)存位置(V)晃择、預(yù)期原值(A)和新值(B)冀值。 如果內(nèi)存位置的值與預(yù)期原值相匹配也物,那么處理器會自動將該位置值更新為新值 宫屠。否則,處理器不做任何操作滑蚯。無論哪種情況浪蹂,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功告材,而不提取當(dāng)前 值坤次。)】
一種是把內(nèi)存分配的動作按照線程劃分在不同的空間之中進(jìn)行,就是每個線程都事先分配一小塊內(nèi)存區(qū)域稱為本地線程分配緩沖(Thread Local Allocation Buffer斥赋,TLAB)缰猴,只要在要分配新的TLAB才需要同步鎖定,VM是否使用TLAB疤剑,可以使用-XX:+/-UseTLAB參數(shù)設(shè)定
內(nèi)存分配完成后滑绒,虛擬機(jī)會將分配到的內(nèi)存空間都初始化為零值闷堡,保證了字段再java代碼中可以不賦值直接使用。
接下來虛擬機(jī)設(shè)置對象頭(Object Header)疑故,如對象是哪個類的實(shí)例杠览,如何找到類的元數(shù)據(jù)信息,對象的哈希值纵势,對象GC分代年齡等信息踱阿。

對象的內(nèi)存布局

在HotSpot虛擬機(jī)中,對象在內(nèi)存中存儲布局分為3塊區(qū)域:對象頭(Header)钦铁,實(shí)例數(shù)據(jù)(Instance Data)软舌,對齊填充(Padding)。
對象頭包含兩部分信息育瓜,第一部分用于存儲對象自身的運(yùn)行時數(shù)據(jù)葫隙,如HashCode,GC分代年齡躏仇,鎖狀態(tài)標(biāo)志恋脚,線程持有的鎖,偏向線程ID焰手,偏向時間戳等糟描,官方稱之為“Mark Word”。
對象投的另外一部分是類型指針书妻,即對象指向它的類元數(shù)據(jù)的指針船响,虛擬機(jī)通過這個指針來確定這個對象時哪個類的實(shí)例。數(shù)組還會額外存儲一塊記錄數(shù)組長度的數(shù)據(jù)躲履。
接下來的實(shí)例數(shù)據(jù)部分是對象真正存儲的有效信息见间,各種字段類型內(nèi)容。無論是父類還是子類工猜,這部分的存儲屬性會收到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響米诉。
第三部分對齊填充并不是必然存在的,也沒有特別含義篷帅,僅起著占位符的作用史侣。HotSpot VM自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,因此對象實(shí)例部分沒有對齊時需要用填充來補(bǔ)齊魏身。

對象的訪問定位

Java程序要通過棧上的reference數(shù)據(jù)來操作堆上的具體對象惊橱。目前主流的訪問對象的方式分為使用句柄和直接指針兩種。

句柄訪問
Java堆中會劃分出一塊內(nèi)存來作為句柄池箭昵,reference中存儲的就是對象的句柄地址税朴,句柄中包含了對象實(shí)例數(shù)據(jù)(存儲在堆中)和類型數(shù)據(jù)(存儲在方法區(qū)中)各自的具體地址信息。
好處是穩(wěn)定,對象移動(如GC時移動)只會改變句柄中的實(shí)例數(shù)據(jù)指針正林。
直接指針訪問
在Java堆對象的布局中放置訪問類型數(shù)據(jù)的相關(guān)信息茧跋。reference中存儲的直接是對象地址。速度快卓囚,因?yàn)楣?jié)省了一次指針定位的時間開銷瘾杭。

Sun HotSpot使用的是直接指針訪問的方式。

垃圾收集器與內(nèi)存分配策略

程序計數(shù)器哪亿,虛擬機(jī)棧粥烁,本地方法棧3個區(qū)域是隨線程生命周期,棧中的棧幀隨著方法的進(jìn)入退出執(zhí)行出入棧蝇棉。這幾個區(qū)域的內(nèi)存分配和回收都具備確定性讨阻。
主要是Java堆和方法區(qū)是動態(tài)的。

對象存活算法

引用計數(shù)法(Reference Counting)篡殷,添加引用計數(shù)器钝吮,引用加一失效減一,主流Java虛擬機(jī)沒有使用板辽,因?yàn)楸容^難解決對象相互循環(huán)引用奇瘦。
可達(dá)性分析算法(Reachability Analysis),從“GC Roots”對象作為起始點(diǎn)向下搜索劲弦,搜索路徑稱為“引用鏈(Reference Chain)”耳标,不可達(dá)則證明對象不可用,就可以回收邑跪。
GC Roots對象包括如下:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象次坡。
方法區(qū)中類靜態(tài)屬性引用的對象。
方法區(qū)中常量引用的對象画畅。
本地方法棧中JNI引用的對象砸琅。

JDK1.2之后,Java對引用的概念進(jìn)行了擴(kuò)充轴踱,分為

  • 強(qiáng)引用(Strong Reference)
    在程序代碼中普遍存在 Object o=new Object()症脂,強(qiáng)引用存在永遠(yuǎn)不會回收對象
  • 軟引用(Soft Reference)
    有用但并非必需的對象,在內(nèi)存溢出異常前寇僧,會把這些對象列進(jìn)回收范圍進(jìn)行第二次回收摊腋。這次之后如果沒有足夠內(nèi)存才會拋出內(nèi)存溢出沸版。JDK1.2之后提供了SoftReference類實(shí)現(xiàn)軟引用
  • 弱引用(Weak Reference)
    強(qiáng)度比軟引用更弱嘁傀,無論當(dāng)前內(nèi)存是否足夠都會回收。JDK1.2之后提供了WeakReference類實(shí)現(xiàn)軟引用
  • 虛引用(Phantom Reference)
    虛引用不會影響生存時間视粮,也無法通過虛引用取得對象實(shí)例细办,僅用于在GC時收到一個系統(tǒng)通知。JDK1.2之后提供了PhantomReference類實(shí)現(xiàn)軟引用

可達(dá)性分析算法中不可達(dá)的對象,要經(jīng)歷2次標(biāo)記之后才會真正死亡笑撞〉盒ィ可達(dá)性算法計算后第一次標(biāo)記并且進(jìn)行一次篩選,條件是次對象是否有必要執(zhí)行finalize()方法茴肥。如果沒有該方法坚踩,或者虛擬機(jī)調(diào)用過該方法,則視為無需執(zhí)行瓤狐。
如果被判定有必要執(zhí)行瞬铸,這個對象會被放在F-Queue隊(duì)列中,并且稍后在一個由虛擬機(jī)自動建立的础锐,低優(yōu)先級的Finalizer線程去執(zhí)行嗓节。執(zhí)行意味著觸發(fā)但并不會等待。對象如果與引用鏈上任何一個對象建立關(guān)聯(lián)皆警,則第二次標(biāo)記時將移出“即將回收”的集合拦宣;否則就回收。

方法區(qū)回收

Java虛擬機(jī)規(guī)范中說過可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾回收信姓,因?yàn)樾詢r比比較低鸵隧,堆中的新生代垃圾回收一次一般可以回收70%-95%的空間,永久代遠(yuǎn)低于此意推。
永久代垃圾回收主要包含兩部分:廢棄常量和無用的類掰派。
如常量“abc”,當(dāng)前系統(tǒng)中沒有任何String對象或者其他地方引用左痢,則會被清理出常量池靡羡。
無用的類條件則苛刻很多:

  • 該類所有的實(shí)例都已經(jīng)被回收,Java堆中不存在該類的任何實(shí)例俊性。
  • 加載該類的ClassLoader已經(jīng)被回收略步。
  • 該類的java.lang.Class對象沒有任何地方引用,無法在任何地方通過反射訪問該類的方法定页。

在大量使用反射趟薄、動態(tài)代理、CGLib等ByteCode框架典徊,動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機(jī)具備類卸載功能杭煎,防止永久代內(nèi)存溢出。

垃圾回收算法

標(biāo)記-清除算法(Mark-Sweep)分為標(biāo)記和清除兩個階段卒落。這是最基礎(chǔ)的收集算法羡铲,因?yàn)楹罄m(xù)收集算法都是基于這種思路并且對其不足進(jìn)行改進(jìn)而來。
其主要不足有兩個:

  • 效率問題:標(biāo)記和清除兩個過程效率都不高
  • 空間問題:標(biāo)記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片儡毕,可能會導(dǎo)致后續(xù)在內(nèi)存分配較大對象時無法找到連續(xù)內(nèi)存而提前觸發(fā)另一次垃圾收集動作也切。

復(fù)制算法(Copying),它將內(nèi)存劃分為大小相等的兩塊,每次只用其中的一塊雷恃。當(dāng)這塊內(nèi)存用完就將存活的對象復(fù)制到另外一塊上疆股,然后把這塊內(nèi)存直接清理掉。代價就是內(nèi)存縮小為原來的一半倒槐。
現(xiàn)在的商業(yè)虛擬機(jī)都采用本算法來收集新生代旬痹。IBM公司研究表明新生代中98%對象生命周期短暫,不需要劃分一半的內(nèi)存讨越,而是劃分為一塊較大的Eden空間和兩塊較小的Survivor空間唱凯。每次使用Eden和一塊Survivor,回收時將Eden和Survivor中存活對象一次性復(fù)制到另外一塊Survivor谎痢。HotSpot默認(rèn)Eden和Survivor的比例是8:1.當(dāng)Survivor空間不夠時磕昼,需要依賴其他內(nèi)存(老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。
復(fù)制算法在對象存活率較高時需要進(jìn)行較多的復(fù)制操作节猿,效率變低票从。而且如果不想浪費(fèi)50%的空間,就需要額外的空間進(jìn)行分配擔(dān)保(100%對象存貨)滨嘱,老年代一般不能直接用這種算法

標(biāo)記-整理算法(Mark-Compact)峰鄙,與標(biāo)記清除相似,但是第二步不是直接清理太雨,而是讓所有存活的對象都向一段移動吟榴,然后直接清理掉邊界意外的內(nèi)存。

分代收集算法(Generational Collection)囊扳,根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊吩翻,一般是將Java堆分為新生代和老年代,這樣可以根據(jù)各個年代采用適當(dāng)?shù)氖占惴ā?/p>

有關(guān)年輕代的JVM參數(shù)
1)-XX:NewSize和-XX:MaxNewSize
用于設(shè)置年輕代的大小锥咸,建議設(shè)為整個堆大小的1/3或者1/4,兩個值設(shè)為一樣大狭瞎。
2)-XX:SurvivorRatio
用于設(shè)置Eden和其中一個Survivor的比值,這個值也比較重要搏予。
3)-XX:+PrintTenuringDistribution
這個參數(shù)用于顯示每次Minor GC時Survivor區(qū)中各個年齡段的對象的大小熊锭。
4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold
用于設(shè)置晉升到老年代的對象年齡的最小值和最大值,每個對象在堅(jiān)持過一次Minor GC之后雪侥,年齡就加1碗殷。

HotSpot算法實(shí)現(xiàn)

枚舉根節(jié)點(diǎn)

可以作為GC ROOT的節(jié)點(diǎn)主要在全局性的引用(如常量或類靜態(tài)屬性)與執(zhí)行上下文(棧幀中的本地變量表)中∷儆В可達(dá)性分析堆執(zhí)行時間的敏感還體現(xiàn)在GC停頓上锌妻,分析工作必須在一個能確保一致性的快照中進(jìn)行。一致性是指整個分析期間執(zhí)行系統(tǒng)像是被凍結(jié)在某個時間點(diǎn)上鸟廓,不可以發(fā)生任何變化从祝。GC進(jìn)行時必須停頓所有java執(zhí)行線程(Sun稱之為 Stop The World)的其中一個重要原因,CMS收集器中引谜,枚舉根節(jié)點(diǎn)時也是必須要停頓的牍陌。
目前的主流Java虛擬機(jī)都是準(zhǔn)確式GC,使用一組稱為OopMap(Ordinary Object Pointer)的數(shù)據(jù)結(jié)構(gòu)存儲哪些地方存放著對象引用员咽。在JIT編譯過程中毒涧,會在特定的位置記錄下棧和寄存器中是哪些位置是引用,GC掃描時可以直接得知這些信息贝室。

安全點(diǎn)(safepoint)

如果為每一條指令生成OopMap契讲,這些可能會需要大量的額外空間,成本較高滑频。所以HotSpot是在特定位置記錄這些信息捡偏,這些位置稱為安全點(diǎn)。安全點(diǎn)的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”峡迷,例如方法調(diào)用银伟、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等绘搞。
如何在GC發(fā)生時讓所有的線程都在最近的安全點(diǎn)上停頓彤避。這里有兩種方案選擇:

  • 搶先式中斷(Preemptive Suspension)
    GC發(fā)生時首先中斷全部線程,發(fā)現(xiàn)有線程中斷的點(diǎn)不在安全點(diǎn)上就恢復(fù)讓其運(yùn)行到安全點(diǎn)『幌剑現(xiàn)在幾乎沒有虛擬機(jī)使用類似的方式琉预。
  • 主動式中斷(Voluntary Suspension)
    GC發(fā)生時設(shè)置標(biāo)志,各個線程執(zhí)行時主動去輪詢這個標(biāo)志蒿褂,為真時中斷掛起浆熔。輪詢標(biāo)志與安全點(diǎn)所在的地點(diǎn)重合遏考。

SafePoint一般出現(xiàn)在以下位置:

  • 循環(huán)體的結(jié)尾
  • 方法返回前
  • 調(diào)用方法的call之后
  • 拋出異常的位置

安全區(qū)域(Safe Region)

當(dāng)線程處于Sleep或者Block時,線程無法響應(yīng)JVM的中斷請求,這時候就需要安全區(qū)域摆马。安全區(qū)域是指在一段代碼片段中,引用關(guān)系不會發(fā)生變化脂信。在這個區(qū)域中的任意地方開始GC都是安全的佣渴。當(dāng)線程進(jìn)入SR中的代碼時,首先標(biāo)志自己進(jìn)入了SR桂肌,當(dāng)這段時間JVM發(fā)起GC時数焊,不用管SR狀態(tài)的線程。線程要離開SR時崎场,會檢查系統(tǒng)是否完成根節(jié)點(diǎn)枚舉(或者整個GC過程)佩耳,完成就繼續(xù)執(zhí)行否則就等待信號。

垃圾收集器

上圖展示了7種不同分代的收集器谭跨,如果中間有線干厚,表示他們可以搭配使用李滴。

Serial收集器

最基本,發(fā)展歷史最悠久的收集器蛮瞄。在JDK1.3.1之前時虛擬機(jī)新生代手機(jī)的唯一選擇所坯。單線程收集器,Stop The World的執(zhí)行者挂捅。虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)新生代收集器芹助,因?yàn)樗唵味咝АT谧烂鎴鼍爸邢邢龋律鷥?nèi)存一般是幾十或者一兩百兆状土,停頓時間控制在幾十毫秒最多一百多毫秒。

ParNew收集器

ParNew就是Serial的多線程版本伺糠,它是Server模式下的虛擬機(jī)中首選的新生代收集器蒙谓,因?yàn)槌薙erial,只有它能和CMS(Concurrent Mark Sweep)收集器配合工作训桶。因?yàn)镴DK1.5用CMS收集老年代時彼乌,新生代只能選擇Serial或者ParNew中的一個。

  • 并行(Parallel):指多條垃圾收集線程并行工作渊迁,但此時用戶線程仍然處于等待狀態(tài)
  • 并發(fā)(Concurrent):用戶線程與垃圾收集器同時執(zhí)行(不一定是并行慰照,可能交替執(zhí)行)

Parallel Scavenge收集器

新生代收集器,復(fù)制算法琉朽,并行多線程收集器毒租。
CMS等收集器的關(guān)注點(diǎn)是盡可能縮短垃圾收集時用戶線程的停頓時間,PS收集器目標(biāo)是達(dá)到一個可控制的吞吐量(Throughput)箱叁。所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時間與CPU消耗時間的比值墅垮,即吞吐量=運(yùn)行用戶代碼時間/(運(yùn)行用戶代碼時間+垃圾收集時間)
停頓時間端適合與用戶交互的程序,高吞吐量可以高效利用CPU時間耕漱,盡快完成程序運(yùn)算任務(wù)算色,適合后臺運(yùn)算而不需要太多交互的任務(wù)。
Parallel Scavenge提供了兩個參數(shù)用于精確控制吞吐量螟够,最大垃圾收集停頓時間 -XX:MaxGCPauseMillis灾梦,直接設(shè)置吞吐量大小-XX:GCTimeRatio。
還有一個參數(shù)-XX:+UseAdaptiveSizePolicy妓笙,打開之后若河,新生代大小、Eden和Survivor區(qū)比例寞宫,晉升老年代對象年齡等細(xì)節(jié)參數(shù)不需要設(shè)置萧福。虛擬機(jī)會根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量辈赋,這種調(diào)節(jié)方式稱之為GC自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)

Serial Old收集器

這是Serial收集器的老年代版本鲫忍,這個收集器的主要意義也是在于給Client模式下的虛擬機(jī)使用膏燕。在Server模式下有兩大用途:一種是用在JDK1.5以及之前的版本與Parallel Scavenge收集器搭配使用,另一種就是作為CMS收集器的后備預(yù)案悟民,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用坝辫。

Parallel Old收集器

這是Parallel Scavenge收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法逾雄。是在JDK1.6提供阀溶。注重吞吐量以及CPU資源敏感的場合腻脏,可以考慮Parallel Scavenge加Parallel Old收集器鸦泳。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以捉去最短回收停頓時間為目標(biāo)的收集器。運(yùn)作過程分為4個步驟:

  • 初始標(biāo)記(CMS initial mark)
  • 并發(fā)標(biāo)記(CMS concurrent mark)
  • 重新標(biāo)記(CMS remark)
  • 并發(fā)清除(CMS concurrent sweep)
    其中初始標(biāo)記永品、重新標(biāo)記兩個步驟仍然需要“Stop The World”做鹰,初始標(biāo)記僅標(biāo)記GC Roots能直接關(guān)聯(lián)的對象,速度很快鼎姐。并發(fā)標(biāo)記就是進(jìn)行GC Roots Tracing的過程钾麸,重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因?yàn)橛脩舫绦蚶^續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記,這個階段停頓時間一般比初始標(biāo)記階段稍長一些炕桨,但是比并發(fā)標(biāo)記的時間短饭尝。
    CMS的優(yōu)點(diǎn):并發(fā)收集、低停頓
    3個明顯缺點(diǎn):
  1. CMS收集器對CPU資源非常敏感献宫。其實(shí)面向并發(fā)設(shè)計的程序?qū)PU資源都比較敏感钥平,在并發(fā)階段雖然不會暫停用戶線程,但是會因?yàn)檎加昧艘徊糠志€程(CPU)資源導(dǎo)致應(yīng)用程序變慢姊途,總吞吐量會降低涉瘾。CMS默認(rèn)啟動回收線程是(CPU數(shù)量+3)/4,也就是CPU在4個以上時會占用不少于25%的CPU資源并且隨著CPU數(shù)量增加而下降捷兰,當(dāng)CPU不足4個是立叛,影響可能就很大。
  2. CMS收集器無法處理浮動垃圾(Floating Garbage)贡茅,可能出現(xiàn)Concurrent Mode Failure失敗導(dǎo)致另一次Full GC的產(chǎn)生秘蛇。CMS并發(fā)清理階段用戶線程還在運(yùn)行,還會由新的垃圾產(chǎn)生顶考。這部分在標(biāo)記過程后彤叉,CMS無法在當(dāng)次收集中處理掉他們,要等到下一次GC時再清理村怪,這些垃圾稱為“浮動垃圾”秽浇。CMS在GC過程中用戶線程還要繼續(xù)運(yùn)行,因此需要預(yù)留一部分內(nèi)存供用戶使用甚负,因此老年代不能等到完全填滿柬焕。
  3. 因?yàn)镸ark-Sweep算法本身可能會引發(fā)的空間碎片审残。

G1收集器

G1(Garbage-First)收集器是最前沿的成果之一。目標(biāo)是替換CMS斑举,有如下特點(diǎn):

  • 并行與并發(fā):G1能充分利用多CPU搅轿、多環(huán)境下的硬件優(yōu)勢,縮短StopTheWorld停頓時間富玷。
  • 分代收集:不需要其他收集器配合能夠獨(dú)立管理整個GC堆
  • 空間整合:整體看來是基于“標(biāo)記-整理”實(shí)現(xiàn)璧坟,局部(2個Reginon之間)上來看是基于“復(fù)制”算法實(shí)現(xiàn)。這意味這G1運(yùn)作期間不會產(chǎn)生內(nèi)存空間碎片赎懦,收集后可以提供規(guī)整的可用內(nèi)存雀鹃。
  • 可預(yù)測的停頓:能夠建立可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi)励两,消耗在垃圾收集上的時間不能超過N毫秒黎茎,這個幾乎是實(shí)時java(RTSJ,Real-Time Java Specification)的垃圾收集器特征当悔。

使用G1收集器時傅瞻,Java堆內(nèi)存被劃分為多個大小相等的獨(dú)立區(qū)域(Region),雖然保留新生代和老年代的概念盲憎,但是新生代和老年代不再是物理隔離的了嗅骄,他們是一部分Region(不需要連續(xù))的集合。
G1能夠建立可預(yù)測的停頓時間模型饼疙,是因?yàn)樗梢杂杏媱澋谋苊庠谡麄€Java堆中進(jìn)行全區(qū)域的垃圾收集溺森。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需要時間的經(jīng)驗(yàn)值)宏多,在后臺維護(hù)一個優(yōu)先列表儿惫,根據(jù)允許的收集時間,有限回收價值最大的Region伸但。
G1算法實(shí)現(xiàn)從2004年Sun實(shí)驗(yàn)室發(fā)表第一篇G1論文肾请,直到j(luò)dk7u4才移除了“Experimental”標(biāo)識達(dá)到商用程度。

JDK9之后G1稱為默認(rèn)收集器更胖,
JDK11開始引入ZGC铛铁,是一種可擴(kuò)展的低延遲垃圾收集器,旨在實(shí)現(xiàn)以下目標(biāo):

  • 暫停時間不超過10毫秒
  • 暫停時間不會隨堆或?qū)崟r設(shè)置大小而增加
  • 處理堆范圍從幾百M(fèi)到幾T字節(jié)大小

JDK12開始引入Shenandoah GC却妨,Shenandoah是一款concurrent及parallel的垃圾收集器饵逐;跟ZGC一樣也是面向low-pause-time的垃圾收集器,不過ZGC是基于colored pointers來實(shí)現(xiàn)彪标,而Shenandoah GC是基于brooks pointers來實(shí)現(xiàn)倍权。

內(nèi)存分配與回收策略

大部分情況下,對象在新生代Eden區(qū)分配捞烟,當(dāng)Eden沒有足夠空間薄声,虛擬機(jī)發(fā)起一次Minor GC当船。之后仍然不足進(jìn)行Full GC。
大對象直接進(jìn)入老年代默辨,所以代碼中出現(xiàn)大對象是要很慎重德频,特別是一堆生命周期很短的大對象。
長期存活的對象進(jìn)入老年代缩幸。每熬過一次Minor GC壹置,年齡增加1歲,默認(rèn)15歲就會晉升到老年代表谊。
動態(tài)對象年齡判定钞护,如果Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進(jìn)入老年代铃肯,無需等待MaxTenuringThreshold中要求的年齡患亿。
空間分配擔(dān)保传蹈,Minor GC之前虛擬機(jī)會先檢查老年代最大可用連續(xù)空間是否大于新生代所有對象總空間押逼,如果成立可以確保Minor GC安全。如果不成立惦界,檢查老年代最大連續(xù)空間是否大于歷次晉升到老年代對象的平均大小挑格,如果大于則嘗試進(jìn)行Minor GC。如果虛擬機(jī)設(shè)置值不允許冒險沾歪,或者小于平均大小漂彤,則改為一次Full GC。擔(dān)保失斣植(Handle Promotion Failure)后挫望,會發(fā)起一次Full GC。

Java工具

  • jps(JVM Process Status Tool)
    可以列出正在運(yùn)行的虛擬機(jī)進(jìn)程狂窑,并顯示虛擬機(jī)執(zhí)行主類(MainClass媳板,main()函數(shù)所在的類)以及本地虛擬機(jī)唯一ID(Local Virtual Machine Identifier, LVMID)
    [jira@iz2ze6589vyznj8lm7cbk7z ~]$ jps -lv
    6610 org.apache.catalina.startup.Bootstrap -Djava.util.logging.config.file=/home/jira/atlassian-jira-software-7.5.2-standalone/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms1384m -Xmx3768m -Djava.awt.headless=true -Datlassian.standalone=JIRA -Dorg.apache.jasper.runtime.BodyContentImpl.LIMIT_BUFFER=true -Dmail.mime.decodeparameters=true -Dorg.dom4j.factory=com.atlassian.core.xml.InterningDocumentFactory -XX:-OmitStackTraceInFastThrow -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Xloggc:/home/jira/atlassian-jira-software-7.5.2-standalone/logs/atlassian-jira-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintGCCause -Dcatalina.base=/home/jira/atlassian-jira-software-7.5.2-standalone -Dcatalina.home=/home/jira/atlassian-jira-software-7.5.2-standalone -Djava.io.tmpdir=/home/jira/atlassian-jira-software-7.5.2-standalone/temp
    25652 synchrony.core -Xss2048k -Xmx1g
    25431 org.apache.catalina.startup.Bootstrap -Djava.util.logging.config.file=/home/jira/atlassian-confluence-6.3.1/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Xms1256m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dconfluence.context.path= -Dorg.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE=32768 -Dsynchrony.enable.xhr.fallback=true -Xms1024m -Xmx2024m -XX:+UseG1GC -Datlassian.plugins.enable.wait=300 -Djava.awt.headless=true -XX:G1ReservePercent=20 -Xloggc:/home/jira/atlassian-confluence-6.3.1/logs/gc-2019-10-27_01-10-04.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=2M -XX:-PrintGCDetails -XX:+PrintGCDateStamps -XX:-PrintTenuringDistribution -Xms1256m -Xmx2048m -XX:PermSize=128m -XX:MaxPermSize=512m -Djava.endorsed.dirs=/home/jira/atlassian-confluence-6.3.1/endorsed -Dcatalina.base=/home/jira/atlassian-confluence-6.3.1 -Dcatalina.home=/home/jira/atlassian-confluence-6.3.1 -Djava.io.tm
    5372 sun.tools.jps.Jps -Dapplication.home=/usr/java/jdk1.8.0_181-amd64 -Xms8m

  • jstat(JVM Statistics Monitoring Tool)虛擬機(jī)統(tǒng)計信息監(jiān)視工具
    它可以顯示本地或者遠(yuǎn)程虛擬機(jī)進(jìn)程中的類裝載、內(nèi)存泉哈、垃圾回收蛉幸、JIT編譯等運(yùn)行數(shù)據(jù)。
    [jira@iz2ze6589vyznj8lm7cbk7z ~]$ jstat -gcutil 6610
    S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
    69.10 0.00 52.43 92.57 90.47 83.80 5613 460.532 12 13.624 474.156
    這個是我們Jira服務(wù)器GC情況丛晦,E表示Eden區(qū)使用了52.43%奕纫,兩個Survivor區(qū)(S0(69.10%),S1(0%))烫沙,老年代O(表示Old)占用了92.57%匹层,jdk1.8之前永生代是P(Permanent)之后是M(Metaspace)90.47%,CCS(Compressed Class Space)83.8%锌蓄。YGC(Young GC)5613次升筏,YGCT總耗時460秒仲器,F(xiàn)GC(Full GC)12次,F(xiàn)GCT耗時13秒仰冠,GCT總GC耗時474秒乏冀。

  • jinfo(Java infomation)
    java配置信息工具,可以實(shí)時地查看和調(diào)整虛擬機(jī)各項(xiàng)參數(shù)洋只,jps -v可以查看顯式指定參數(shù)辆沦,jinfo -flag可以進(jìn)行默認(rèn)值查詢,jinfo -sysprops可以把虛擬機(jī)進(jìn)程中System.getProperties()內(nèi)容打印出來识虚。

  • jmap(Memory Map for Java)
    用于生成堆轉(zhuǎn)儲快照(一般稱為heapdump或者dump文件)肢扯,還可以查詢finalize執(zhí)行隊(duì)列,Java堆和永久代的詳細(xì)信息担锤,如空間使用率蔚晨,使用的收集器等。jmap -dump:format=b,file=文件名 [pid] 會生成一個bin文件

  • jhat(JVM Heap Analysis Tool)
    虛擬機(jī)堆轉(zhuǎn)儲快照分析工具肛循,與jmap搭配使用铭腕,來分析jmap生成的堆轉(zhuǎn)儲快照。內(nèi)置了一個http服務(wù)器多糠,可以再完成之后啟動網(wǎng)站訪問累舷。命令就是直接 jhat xxx.bin

  • jstack(Stack Trace for Java)
    Java堆棧跟蹤工具,用于生成虛擬機(jī)當(dāng)前時刻的線程快照(一般稱為threaddump或者javacore文件)夹孔,線程快照就是虛擬機(jī)內(nèi)每一條線程當(dāng)前執(zhí)行的方法堆棧信息集合被盈。jstatck -l pid。后續(xù)的JDK1.5中搭伤,java.lang.Thread類新增了一個getAllStackTraces()方法只怎,可以寫個頁面直接查詢。

  • HSDIS(JIT生成代碼反匯編)
    Sun官方推薦的HotSpot虛擬機(jī)JIT編譯代碼的反匯編插件怜俐。
    在主流商用虛擬機(jī)中身堡,HotSpot和J9可以采用混合模式(解釋器與編譯器配搭使用),而JRockit內(nèi)部沒有解釋器佑菩,采用純編譯模式盾沫。
    Java程序最初是通過解釋器進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個方法或代碼塊運(yùn)行的特別頻繁時殿漠,就會把這些代碼認(rèn)定為“熱點(diǎn)代碼”(Hot Spot Code)赴精。為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時绞幌,虛擬機(jī)將會把這些代碼編譯成為本地平臺相關(guān)的機(jī)器碼蕾哟,并進(jìn)行優(yōu)化,而完成這個任務(wù)的編譯器稱為及時編譯器(Just In Time Compiler,簡稱JIT)谭确。
    解釋器與編譯器
    解釋器優(yōu)勢:當(dāng)程序需要迅速啟動和執(zhí)行時帘营,解釋器可以首先發(fā)揮作用,省去編譯的時間逐哈,立即執(zhí)行芬迄。同時解釋器還可作為編譯器激進(jìn)優(yōu)化時的一個“逃生門”,當(dāng)激進(jìn)優(yōu)化假設(shè)不成立時可退回到解釋狀態(tài)繼續(xù)執(zhí)行昂秃。
    編譯器優(yōu)勢:編譯之后得到優(yōu)化后的本地代碼禀梳,執(zhí)行效率高,但優(yōu)化耗時較長肠骆,且占用內(nèi)存資源較大算途。

JDK可視化工具

最強(qiáng)的兩個可視化工具:JConsole和Visual VM(All-in-One Java Troubleshooting Tool),這個是正式的JDK成員蚀腿。
Visual VMI可以做到:
顯示虛擬機(jī)進(jìn)程以及配置嘴瓤、環(huán)境信息(jps,jinfo)
監(jiān)視應(yīng)用程序的CPU莉钙、GC廓脆、堆、方法區(qū)以及線程信息(jstat胆胰,jstack)
dump以及分析堆轉(zhuǎn)儲快照(jmap狞贱,jhat)
方法及的程序運(yùn)行性能分析刻获,找出被調(diào)用最多蜀涨、運(yùn)行時間最長的方法。
離線程序快照:收集程序運(yùn)行時配置蝎毡、線程dump厚柳、內(nèi)存dump等信息建立快照。

調(diào)優(yōu)案例分析與實(shí)戰(zhàn)

高性能硬件程序部署

4CPU沐兵,16G內(nèi)存别垮,64位JDK1.5,-Xmx和-Xms固定在12G扎谎,但是網(wǎng)站長時間失去響應(yīng)碳想。
排查后發(fā)現(xiàn)是GC停頓,Server模式默認(rèn)使用吞吐量優(yōu)先毁靶,回收12GB胧奔,一次FullGC停頓高達(dá)14秒,讀取文檔序列號產(chǎn)生大對象直接進(jìn)入老年代预吆。因此出現(xiàn)每個十幾分鐘出現(xiàn)十幾秒的停頓龙填。

堆外內(nèi)存導(dǎo)致的溢出錯誤

普通PC機(jī),內(nèi)存2G,JVM分配了1.6G岩遗,GC正常扇商,堆內(nèi)存正常,頻繁O(jiān)OM宿礁。因?yàn)榭蚣躈IO操作使用到Direct Memory內(nèi)存案铺,這塊是比較難直接回收的,只有在FullGC時順便回收梆靖。

外部命令導(dǎo)致系統(tǒng)緩慢

每個用戶請求的處 理都需要執(zhí)行一個外部shell腳本來獲得系統(tǒng)的一些信息红且。執(zhí)行這個shell腳本是通過Java的 Runtime.getRuntime().exec()方法來調(diào)用的。這種調(diào)用方式可以達(dá)到目的涤姊,但是它在Java 虛擬機(jī)中是非常消耗資源的操作暇番,即使外部命令本身能很快執(zhí)行完畢,頻繁調(diào)用時創(chuàng)建進(jìn)程 的開銷也非乘己埃可觀壁酬。Java虛擬機(jī)執(zhí)行這個命令的過程是:首先克隆一個和當(dāng)前虛擬機(jī)擁有一 樣環(huán)境變量的進(jìn)程,再用這個新的進(jìn)程去執(zhí)行外部命令恨课,最后再退出這個進(jìn)程舆乔。如果頻繁執(zhí) 行這個操作,系統(tǒng)的消耗會很大剂公,不僅是CPU希俩,內(nèi)存負(fù)擔(dān)也很重。
用戶根據(jù)建議去掉這個Shell腳本執(zhí)行的語句纲辽,改為使用Java的API去獲取這些信息后颜武, 系統(tǒng)很快恢復(fù)了正常。

JVM進(jìn)程崩潰

BS系統(tǒng)拖吼,正常運(yùn)行一段時間之后JVM自動關(guān)閉鳞上,留下一個hs_err_pdi###.log文件。由于異步啟用了Socket請求另外一個較慢的站點(diǎn)吊档,超過虛擬機(jī)承受能力后導(dǎo)致崩潰篙议。改為消息隊(duì)列后正常。

不恰當(dāng)?shù)臄?shù)據(jù)結(jié)構(gòu)

在HashMap<Long,Long>結(jié)構(gòu)中怠硼,只有Key和Value所存放 的兩個長整型數(shù)據(jù)是有效數(shù)據(jù)鬼贱,共16B(2×8B)。這兩個長整型數(shù)據(jù)包裝成java.lang.Long對 象之后香璃,就分別具有8B的MarkWord这难、8B的Klass指針,在加8B存儲數(shù)據(jù)的long值增显。在這兩個 Long對象組成Map.Entry之后雁佳,又多了16B的對象頭脐帝,然后一個8B的next字段和4B的int型的 hash字段,為了對齊糖权,還必須添加4B的空白填充堵腹,最后還有HashMap中對這個Entry的8B的引 用,這樣增加兩個長整型數(shù)字星澳,實(shí)際耗費(fèi)的內(nèi)存為 (Long(24B)×2)+Entry(32B)+HashMap Ref(8B)=88B疚顷,空間效率為16B/88B=18%, 實(shí)在太低了禁偎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腿堤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子如暖,更是在濱河造成了極大的恐慌笆檀,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盒至,死亡現(xiàn)場離奇詭異酗洒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)枷遂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門樱衷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人酒唉,你說我怎么就攤上這事矩桂。” “怎么了痪伦?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵侄榴,是天一觀的道長。 經(jīng)常有香客問我流妻,道長牲蜀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任绅这,我火速辦了婚禮,結(jié)果婚禮上在辆,老公的妹妹穿的比我還像新娘证薇。我一直安慰自己,他們只是感情好匆篓,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布浑度。 她就那樣靜靜地躺著,像睡著了一般鸦概。 火紅的嫁衣襯著肌膚如雪箩张。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音先慷,去河邊找鬼饮笛。 笑死,一個胖子當(dāng)著我的面吹牛论熙,可吹牛的內(nèi)容都是我干的福青。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼脓诡,長吁一口氣:“原來是場噩夢啊……” “哼无午!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祝谚,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤宪迟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后交惯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體踩验,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年商玫,在試婚紗的時候發(fā)現(xiàn)自己被綠了箕憾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡拳昌,死狀恐怖袭异,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情炬藤,我是刑警寧澤御铃,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站沈矿,受9級特大地震影響上真,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜羹膳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一睡互、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧陵像,春花似錦就珠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泞歉,卻和暖如春逼侦,著一層夾襖步出監(jiān)牢的瞬間匿辩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工榛丢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铲球,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓涕滋,卻偏偏與公主長得像睬辐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宾肺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容