學(xué)習(xí)JVM是如何從入門(mén)到放棄的抢腐?

前言

只有光頭才能變強(qiáng)

JVM在準(zhǔn)備面試的時(shí)候就有看了姑曙,一直沒(méi)時(shí)間寫(xiě)筆記。現(xiàn)在到了一家公司實(shí)習(xí)伤靠,閑的時(shí)候就寫(xiě)寫(xiě),刷刷JVM博客授瘦,刷刷電子書(shū)醋界。

學(xué)習(xí)JVM的目的也很簡(jiǎn)單:

  • 能夠知道JVM是什么,為我們干了什么提完,具體是怎么干的形纺。能夠理解到一些初學(xué)時(shí)不懂的東西
  • 在面試的時(shí)候有談資
  • 能裝逼
image

(圖片來(lái)源:https://zhuanlan.zhihu.com/p/25511795,侵刪)

聲明:全文默認(rèn)指的是HotSpot VM

一、簡(jiǎn)單聊聊JVM

1.1先來(lái)看看簡(jiǎn)單的Java程序

現(xiàn)在我有一個(gè)JavaBean:


public class Java3y {

    // 姓名
    private String name;

    // 年齡
    private int age;

       //.....各種get/set方法/toString
}

一個(gè)測(cè)試類(lèi):


public class Java3yTest {

    public static void main(String[] args) {
        
        Java3y java3y = new Java3y();
        java3y.setName("Java3y");
        System.out.println(java3y);

    }
}

我們?cè)诔鯇W(xué)的時(shí)候肯定用過(guò)javac來(lái)編譯.java文件代碼徒欣,用過(guò)java命令來(lái)執(zhí)行編譯后生成的.class文件逐样。

image

Java源文件:

image

在使用IDE點(diǎn)擊運(yùn)行的時(shí)候其實(shí)就是將這兩個(gè)命令結(jié)合起來(lái)了(編譯并運(yùn)行),方便我們開(kāi)發(fā)打肝。

image

生成class文件

image

解析class文件得到結(jié)果

image

1.2編譯過(guò)程

.java文件是由Java源碼編譯器(上述所說(shuō)的javac.exe)來(lái)完成脂新,流程圖如下所示:

image

Java源碼編譯由以下三個(gè)過(guò)程組成:

  • 分析和輸入到符號(hào)表
  • 注解處理
  • 語(yǔ)義分析和生成class文件
image

1.2.1編譯時(shí)期-語(yǔ)法糖

語(yǔ)法糖可以看做是編譯器實(shí)現(xiàn)的一些“小把戲”,這些“小把戲”可能會(huì)使得效率“大提升”粗梭。

最值得說(shuō)明的就是泛型了争便,這個(gè)語(yǔ)法糖可以說(shuō)我們是經(jīng)常會(huì)使用到的!

  • 泛型只會(huì)在Java源碼中存在断医,編譯過(guò)后會(huì)被替換為原來(lái)的原生類(lèi)型(Raw Type滞乙,也稱(chēng)為裸類(lèi)型)了。這個(gè)過(guò)程也被稱(chēng)為:泛型擦除鉴嗤。

有了泛型這顆語(yǔ)法糖以后:

  • 代碼更加簡(jiǎn)潔【不用強(qiáng)制轉(zhuǎn)換】
  • 程序更加健壯【只要編譯時(shí)期沒(méi)有警告斩启,那么運(yùn)行時(shí)期就不會(huì)出現(xiàn)ClassCastException異常】
  • 可讀性和穩(wěn)定性【在編寫(xiě)集合的時(shí)候醉锅,就限定了類(lèi)型】

了解泛型更多的知識(shí):

1.3JVM實(shí)現(xiàn)跨平臺(tái)

至此兔簇,我們通過(guò)javac.exe編譯器編譯我們的.java源代碼文件生成出.class文件了!

image

這些.class文件很明顯是不能直接運(yùn)行的,它不像C語(yǔ)言(編譯cpp后生成exe文件直接運(yùn)行)

這些.class文件是交由JVM來(lái)解析運(yùn)行垄琐!

  • JVM是運(yùn)行在操作系統(tǒng)之上的边酒,每個(gè)操作系統(tǒng)的指令是不同的,而JDK是區(qū)分操作系統(tǒng)的此虑,只要你的本地系統(tǒng)裝了JDK甚纲,這個(gè)JDK就是能夠和當(dāng)前系統(tǒng)兼容的口锭。
  • 而class字節(jié)碼運(yùn)行在JVM之上朦前,所以不用關(guān)心class字節(jié)碼是在哪個(gè)操作系統(tǒng)編譯的,只要符合JVM規(guī)范鹃操,那么韭寸,這個(gè)字節(jié)碼文件就是可運(yùn)行的。
  • 所以Java就做到了跨平臺(tái)--->一次編譯荆隘,到處運(yùn)行恩伺!
image

1.4class文件和JVM的恩怨情仇

1.4.1類(lèi)的加載時(shí)機(jī)

現(xiàn)在我們例子中生成的兩個(gè).class文件都會(huì)直接被加載到JVM中嗎?椰拒?

虛擬機(jī)規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”(class文件加載到JVM中):

  • 創(chuàng)建類(lèi)的實(shí)例(new 的方式)晶渠。訪問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值燃观,調(diào)用類(lèi)的靜態(tài)方法
  • 反射的方式
  • 初始化某個(gè)類(lèi)的子類(lèi)褒脯,則其父類(lèi)也會(huì)被初始化
  • Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類(lèi)的類(lèi),直接使用java.exe命令來(lái)運(yùn)行某個(gè)主類(lèi)(包含main方法的那個(gè)類(lèi))
  • 當(dāng)使用JDK1.7的動(dòng)態(tài)語(yǔ)言支持時(shí)(....)

所以說(shuō):

  • Java類(lèi)的加載是動(dòng)態(tài)的缆毁,它并不會(huì)一次性將所有類(lèi)全部加載后再運(yùn)行番川,而是保證程序運(yùn)行的基礎(chǔ)類(lèi)(像是基類(lèi))完全加載到j(luò)vm中,至于其他類(lèi)脊框,則在需要的時(shí)候才加載颁督。這當(dāng)然就是為了節(jié)省內(nèi)存開(kāi)銷(xiāo)

1.4.2如何將類(lèi)加載到j(luò)vm

class文件是通過(guò)類(lèi)的加載器裝載到j(luò)vm中的浇雹!

Java默認(rèn)有三種類(lèi)加載器

image

各個(gè)加載器的工作責(zé)任:

  • 1)Bootstrap ClassLoader:負(fù)責(zé)加載$JAVA_HOME中jre/lib/rt.jar里所有的class沉御,由C++實(shí)現(xiàn),不是ClassLoader子類(lèi)
  • 2)Extension ClassLoader:負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包昭灵,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包
  • 3)App ClassLoader:負(fù)責(zé)記載classpath中指定的jar包及目錄中class

工作過(guò)程:

  • 1吠裆、當(dāng)AppClassLoader加載一個(gè)class時(shí),它首先不會(huì)自己去嘗試加載這個(gè)類(lèi)虎锚,而是把類(lèi)加載請(qǐng)求委派給父類(lèi)加載器ExtClassLoader去完成她我。
  • 2、當(dāng)ExtClassLoader加載一個(gè)class時(shí)咒林,它首先也不會(huì)自己去嘗試加載這個(gè)類(lèi)买羞,而是把類(lèi)加載請(qǐng)求委派給BootStrapClassLoader去完成。
  • 3柱徙、如果BootStrapClassLoader加載失敾和馈(例如在$JAVA_HOME/jre/lib里未查找到該class)奇昙,會(huì)使用ExtClassLoader來(lái)嘗試加載;
  • 4敌完、若ExtClassLoader也加載失敗储耐,則會(huì)使用AppClassLoader來(lái)加載
  • 5、如果AppClassLoader也加載失敗滨溉,則會(huì)報(bào)出異常ClassNotFoundException

其實(shí)這就是所謂的雙親委派模型什湘。簡(jiǎn)單來(lái)說(shuō):如果一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類(lèi)晦攒,而是把請(qǐng)求委托給父加載器去完成闽撤,依次向上

好處:

  • 防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼(安全性角度)

特別說(shuō)明:

  • 類(lèi)加載器在成功加載某個(gè)類(lèi)之后脯颜,會(huì)把得到的 java.lang.Class類(lèi)的實(shí)例緩存起來(lái)哟旗。下次再請(qǐng)求加載該類(lèi)的時(shí)候,類(lèi)加載器會(huì)直接使用緩存的類(lèi)的實(shí)例栋操,而不會(huì)嘗試再次加載闸餐。

1.4.2類(lèi)加載詳細(xì)過(guò)程

加載器加載到j(luò)vm中,接下來(lái)其實(shí)又分了好幾個(gè)步驟

  • 加載矾芙,查找并加載類(lèi)的二進(jìn)制數(shù)據(jù)舍沙,在Java堆中也創(chuàng)建一個(gè)java.lang.Class類(lèi)的對(duì)象
  • 連接蠕啄,連接又包含三塊內(nèi)容:驗(yàn)證场勤、準(zhǔn)備、初始化歼跟。
    • 1)驗(yàn)證和媳,文件格式、元數(shù)據(jù)哈街、字節(jié)碼留瞳、符號(hào)引用驗(yàn)證;
    • 2)準(zhǔn)備骚秦,為類(lèi)的靜態(tài)變量分配內(nèi)存她倘,并將其初始化為默認(rèn)值;
    • 3)解析作箍,把類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用
  • 初始化硬梁,為類(lèi)的靜態(tài)變量賦予正確的初始值。
image

1.4.3JIT即時(shí)編輯器

一般我們可能會(huì)想:JVM在加載了這些class文件以后胞得,針對(duì)這些字節(jié)碼荧止,逐條取出,逐條執(zhí)行-->解析器解析。

但如果是這樣的話跃巡,那就太慢了危号!

我們的JVM是這樣實(shí)現(xiàn)的:

  • 就是把這些Java字節(jié)碼重新編譯優(yōu)化,生成機(jī)器碼素邪,讓CPU直接執(zhí)行外莲。這樣編出來(lái)的代碼效率會(huì)更高。
  • 編譯也是要花費(fèi)時(shí)間的兔朦,我們一般對(duì)熱點(diǎn)代碼做編譯偷线,非熱點(diǎn)代碼直接解析就好了。

熱點(diǎn)代碼解釋?zhuān)阂缓嬲馈⒍啻握{(diào)用的方法淋昭。二、多次執(zhí)行的循環(huán)體

使用熱點(diǎn)探測(cè)來(lái)檢測(cè)是否為熱點(diǎn)代碼安接,熱點(diǎn)探測(cè)有兩種方式:

  • 采樣
  • 計(jì)數(shù)器

目前HotSpot使用的是計(jì)數(shù)器的方式,它為每個(gè)方法準(zhǔn)備了兩類(lèi)計(jì)數(shù)器:

  • 方法調(diào)用計(jì)數(shù)器(Invocation Counter)
  • 回邊計(jì)數(shù)器(Back EdgeCounter)英融。
  • 在確定虛擬機(jī)運(yùn)行參數(shù)的前提下盏檐,這兩個(gè)計(jì)數(shù)器都有一個(gè)確定的閾值,當(dāng)計(jì)數(shù)器超過(guò)閾值溢出了驶悟,就會(huì)觸發(fā)JIT編譯胡野。
image

1.4.4回到例子中

按我們程序來(lái)走,我們的Java3yTest.class文件會(huì)被AppClassLoader加載器(因?yàn)镋xtClassLoader和BootStrap加載器都不會(huì)加載它[雙親委派模型])加載到JVM中痕鳍。

隨后發(fā)現(xiàn)了要使用Java3y這個(gè)類(lèi)硫豆,我們的Java3y.class文件會(huì)被AppClassLoader加載器(因?yàn)镋xtClassLoader和BootStrap加載器都不會(huì)加載它[雙親委派模型])加載到JVM中

image

詳情參考:

擴(kuò)展閱讀:

1.5類(lèi)加載完以后JVM干了什么笼呆?

在類(lèi)加載檢查通過(guò)后熊响,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存

1.5.1JVM的內(nèi)存模型

首先我們來(lái)了解一下JVM的內(nèi)存模型的怎么樣的:

  • 基于jdk1.8畫(huà)的JVM的內(nèi)存模型--->我畫(huà)得比較細(xì)诗赌。
2018-10-15_141336.png

簡(jiǎn)單看了一下內(nèi)存模型汗茄,簡(jiǎn)單看看每個(gè)區(qū)域究竟存儲(chǔ)的是什么(干的是什么):

  • 堆:存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存
  • 虛擬機(jī)棧:虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表铭若、操作棧洪碳、動(dòng)態(tài)鏈接、方法出口等信息
  • 本地方法棧:本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)叼屠。
  • 方法區(qū):存儲(chǔ)已被虛擬機(jī)加載的類(lèi)元數(shù)據(jù)信息(元空間)
  • 程序計(jì)數(shù)器:當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器

1.5.2例子中的流程

image

我來(lái)宏觀簡(jiǎn)述一下我們的例子中的工作流程:

  • 1瞳腌、通過(guò)java.exe運(yùn)行Java3yTest.class,隨后被加載到JVM中镜雨,元空間存儲(chǔ)著類(lèi)的信息(包括類(lèi)的名稱(chēng)嫂侍、方法信息、字段信息..)。
  • 2吵冒、然后JVM找到Java3yTest的主函數(shù)入口(main)纯命,為main函數(shù)創(chuàng)建棧幀,開(kāi)始執(zhí)行main函數(shù)
  • 3痹栖、main函數(shù)的第一條命令是Java3y java3y = new Java3y();就是讓JVM創(chuàng)建一個(gè)Java3y對(duì)象亿汞,但是這時(shí)候方法區(qū)中沒(méi)有Java3y類(lèi)的信息,所以JVM馬上加載Java3y類(lèi)揪阿,把Java3y類(lèi)的類(lèi)型信息放到方法區(qū)中(元空間)
  • 4疗我、加載完Java3y類(lèi)之后,Java虛擬機(jī)做的第一件事情就是在堆區(qū)中為一個(gè)新的Java3y實(shí)例分配內(nèi)存, 然后調(diào)用構(gòu)造函數(shù)初始化Java3y實(shí)例南捂,這個(gè)Java3y實(shí)例持有著指向方法區(qū)的Java3y類(lèi)的類(lèi)型信息(其中包含有方法表吴裤,java動(dòng)態(tài)綁定的底層實(shí)現(xiàn))的引用
  • 5、當(dāng)使用java3y.setName("Java3y");的時(shí)候溺健,JVM根據(jù)java3y引用找到Java3y對(duì)象麦牺,然后根據(jù)Java3y對(duì)象持有的引用定位到方法區(qū)中Java3y類(lèi)的類(lèi)型信息的方法表,獲得setName()函數(shù)的字節(jié)碼的地址
  • 6鞭缭、為setName()函數(shù)創(chuàng)建棧幀剖膳,開(kāi)始運(yùn)行setName()函數(shù)

從微觀上其實(shí)還做了很多東西,正如上面所說(shuō)的類(lèi)加載過(guò)程(加載-->連接(驗(yàn)證岭辣,準(zhǔn)備吱晒,解析)-->初始化),在類(lèi)加載完之后jvm為其分配內(nèi)存(分配內(nèi)存中也做了非常多的事)沦童。由于這些步驟并不是一步一步往下走仑濒,會(huì)有很多的“混沌bootstrap”的過(guò)程,所以很難描述清楚偷遗。

參考資料:

1.6簡(jiǎn)單聊聊各種常量池

在寫(xiě)這篇文章的時(shí)候墩瞳,原本以為我對(duì)String s = "aaa";類(lèi)似這些題目已經(jīng)是不成問(wèn)題了,直到我遇到了String.intern()這樣的方法與諸如String s1 = new String("1") + new String("2");混合一起用的時(shí)候

  • 我發(fā)現(xiàn)鹦肿,我還是太年輕了矗烛。

首先我是先閱讀了美團(tuán)技術(shù)團(tuán)隊(duì)的這篇文章:https://tech.meituan.com/in_depth_understanding_string_intern.html---深入解析String#intern

嗯,然后就懵逼了箩溃。我摘抄一下他的例子:


public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

打印結(jié)果是

  • jdk7,8下false true

調(diào)換一下位置后:


public static void main(String[] args) {

    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}

打印結(jié)果為:

  • jdk7,8下false false

文章中有很詳細(xì)的解析瞭吃,但我簡(jiǎn)單閱讀了幾次以后還是很懵逼。所以我知道了自己的知識(shí)點(diǎn)還存在漏洞涣旨,后面閱讀了一下R大之前寫(xiě)過(guò)的文章:

看完了之后歪架,就更加懵逼了。

后來(lái)霹陡,在zhihu上看到了這個(gè)回答:

結(jié)合網(wǎng)上資料和自己的思考和蚪,下面整理一下對(duì)常量池的理解~~

1.6.1各個(gè)常量池的情況

針對(duì)于jdk1.7之后:

  • 運(yùn)行時(shí)常量池位于堆中
  • 字符串常量池位于堆中

常量池存儲(chǔ)的是:

  • 字面量(Literal):文本字符串等---->用雙引號(hào)引起來(lái)的字符串字面量都會(huì)進(jìn)這里面
  • 符號(hào)引用(Symbolic References)
    • 類(lèi)和接口的全限定名(Full Qualified Name)
    • 字段的名稱(chēng)和描述符(Descriptor)
    • 方法的名稱(chēng)和描述符

常量池(Constant Pool Table)止状,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放--->來(lái)源:深入理解Java虛擬機(jī) JVM高級(jí)特性與最佳實(shí)踐(第二版)

現(xiàn)在我們的運(yùn)行時(shí)常量池只是換了一個(gè)位置(原本來(lái)方法區(qū)攒霹,現(xiàn)在在堆中),但可以明確的是:類(lèi)加載后怯疤,常量池中的數(shù)據(jù)會(huì)在運(yùn)行時(shí)常量池中存放

別人總結(jié)的常量池:

它是Class文件中的內(nèi)容催束,還不是運(yùn)行時(shí)的內(nèi)容集峦,不要理解它是個(gè)池子,其實(shí)就是Class文件中的字節(jié)碼指

HotSpot VM里抠刺,記錄interned string的一個(gè)全局表叫做StringTable塔淤,它本質(zhì)上就是個(gè)HashSet<String>。注意它只存儲(chǔ)對(duì)java.lang.String實(shí)例的引用速妖,而不存儲(chǔ)String對(duì)象的內(nèi)容

字符串常量池只存儲(chǔ)引用高蜂,不存儲(chǔ)內(nèi)容

再來(lái)看一下我們的intern方法:


 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 
  • 如果常量池中存在當(dāng)前字符串罕容,那么直接返回常量池中它的引用备恤。
  • 如果常量池中沒(méi)有此字符串, 會(huì)將此字符串引用保存到常量池中后, 再直接返回該字符串的引用

1.6.2解析題目

本來(lái)打算寫(xiě)注釋的方式來(lái)解釋的杀赢,但好像挺難說(shuō)清楚的烘跺。我還是畫(huà)圖吧...


public static void main(String[] args) {

 
    String s = new String("1");

    s.intern();


    String s2 = "1";

    System.out.println(s == s2);// false
    System.out.println("-----------關(guān)注公眾號(hào):Java3y-------------");
}

第一句:String s = new String("1");

image

第二句:s.intern();發(fā)現(xiàn)字符串常量池中已經(jīng)存在"1"字符串對(duì)象,直接返回字符串常量池中對(duì)堆的引用(但沒(méi)有接收)-->此時(shí)s引用還是指向著堆中的對(duì)象

image

第三句:String s2 = "1";發(fā)現(xiàn)字符串常量池已經(jīng)保存了該對(duì)象的引用了脂崔,直接返回字符串常量池對(duì)堆中字符串的引用

image

很容易看到,兩條引用是不一樣的梧喷!所以返回false砌左。



    public static void main(String[] args) {

        System.out.println("-----------關(guān)注公眾號(hào):Java3y-------------");

        String s3 = new String("1") + new String("1");


        s3.intern();


        String s4 = "11";
        System.out.println(s3 == s4); // true
    }

第一句:String s3 = new String("1") + new String("1");注意:此時(shí)"11"對(duì)象并沒(méi)有在字符串常量池中保存引用

image

第二句:s3.intern();發(fā)現(xiàn)"11"對(duì)象并沒(méi)有在字符串常量池中铺敌,于是將"11"對(duì)象在字符串常量池中保存當(dāng)前字符串的引用汇歹,并返回當(dāng)前字符串的引用(但沒(méi)有接收)

image

第三句:String s4 = "11";發(fā)現(xiàn)字符串常量池已經(jīng)存在引用了,直接返回(拿到的也是與s3相同指向的引用)

image

根據(jù)上述所說(shuō)的:最后會(huì)返回true~~~

如果還是不太清楚的同學(xué)偿凭,可以試著接收一下intern()方法的返回值产弹,再看看上述的圖,應(yīng)該就可以理解了弯囊。


下面的就由各位來(lái)做做痰哨,看是不是掌握了:


    public static void main(String[] args) {

        String s = new String("1");
        String s2 = "1";
        s.intern();
        System.out.println(s == s2);//false

        String s3 = new String("1") + new String("1");
        String s4 = "11";
        s3.intern();
        System.out.println(s3 == s4);//false
    }

還有:


    public static void main(String[] args) {
        String s1 = new String("he") + new String("llo");
        String s2 = new String("h") + new String("ello");
        String s3 = s1.intern();
        String s4 = s2.intern();
        System.out.println(s1 == s3);// true
        System.out.println(s1 == s4);// true
    }

1.7GC垃圾回收

可以說(shuō)GC垃圾回收是JVM中一個(gè)非常重要的知識(shí)點(diǎn),應(yīng)該非常詳細(xì)去講解的匾嘱。但在我學(xué)習(xí)的途中斤斧,我已經(jīng)發(fā)現(xiàn)了有很好的文章去講解垃圾回收的了。

所以霎烙,這里我只簡(jiǎn)單介紹一下垃圾回收的東西撬讽,詳細(xì)的可以到下面的面試題中查閱和最后給出相關(guān)的資料閱
讀吧~

1.7.1JVM垃圾回收簡(jiǎn)單介紹

在C++中蕊连,我們知道創(chuàng)建出的對(duì)象是需要手動(dòng)去delete掉的。我們Java程序運(yùn)行在JVM中游昼,JVM可以幫我們“自動(dòng)”回收不需要的對(duì)象甘苍,對(duì)我們來(lái)說(shuō)是十分方便的。

雖然說(shuō)“自動(dòng)”回收了我們不需要的對(duì)象烘豌,但如果我們想變強(qiáng)载庭,就要變禿..不對(duì),就要去了解一下它究竟是怎么干的扇谣,理論的知識(shí)有哪些昧捷。

首先,JVM回收的是垃圾罐寨,垃圾就是我們程序中已經(jīng)是不需要的了靡挥。垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是要確定這些對(duì)象之中哪些還“存活”著鸯绿,哪些已經(jīng)“死去”跋破。判斷哪些對(duì)象“死去”常用有兩種方式:

  • 引用計(jì)數(shù)法-->這種難以解決對(duì)象之間的循環(huán)引用的問(wèn)題
  • 可達(dá)性分析算法-->主流的JVM采用的是這種方式
image

現(xiàn)在已經(jīng)可以判斷哪些對(duì)象已經(jīng)“死去”了,我們現(xiàn)在要對(duì)這些“死去”的對(duì)象進(jìn)行回收瓶蝴,回收也有好幾種算法:

  • 標(biāo)記-清除算法
  • 復(fù)制算法
  • 標(biāo)記-整理算法
  • 分代收集算法

(這些算法詳情可看下面的面試題內(nèi)容)~

無(wú)論是可達(dá)性分析算法毒返,還是垃圾回收算法,JVM使用的都是準(zhǔn)確式GC舷手。JVM是使用一組稱(chēng)為OopMap的數(shù)據(jù)結(jié)構(gòu)拧簸,來(lái)存儲(chǔ)所有的對(duì)象引用(這樣就不用遍歷整個(gè)內(nèi)存去查找了,空間換時(shí)間)男窟。
并且不會(huì)將所有的指令都生成OopMap盆赤,只會(huì)在安全點(diǎn)上生成OopMap,在安全區(qū)域上開(kāi)始GC歉眷。

  • 在OopMap的協(xié)助下牺六,HotSpot可以快速且準(zhǔn)確地完成GC Roots枚舉(可達(dá)性分析)。

上面所講的垃圾收集算法只能算是方法論汗捡,落地實(shí)現(xiàn)的是垃圾收集器

  • Serial收集器
  • ParNew收集器
  • Parallel Scavenge收集器
  • Serial Old收集器
  • Parallel Old收集器
  • CMS收集器
  • G1收集器

上面這些收集器大部分是可以互相組合使用

image

1.8JVM參數(shù)與調(diào)優(yōu)

很多做過(guò)JavaWeb項(xiàng)目(ssh/ssm)這樣的同學(xué)可能都會(huì)遇到過(guò)OutOfMemory這樣的錯(cuò)誤淑际。一般解決起來(lái)也很方便,在啟動(dòng)的時(shí)候加個(gè)參數(shù)就行了扇住。

上面也說(shuō)了很多關(guān)于JVM的東西--->JVM對(duì)內(nèi)存的劃分啊春缕,JVM各種的垃圾收集器啊。

內(nèi)存的分配的大小啊台囱,使用哪個(gè)收集器啊淡溯,這些都可以由我們根據(jù)需求,現(xiàn)實(shí)情況來(lái)指定的簿训,這里就不詳細(xì)說(shuō)了咱娶,等真正用到的時(shí)候才回來(lái)填坑吧~~~~

參考資料:

二、JVM面試題

拿些常見(jiàn)的JVM面試題來(lái)做做膘侮,加深一下理解和查缺補(bǔ)漏

  • 1屈糊、詳細(xì)jvm內(nèi)存模型
  • 2、講講什么情況下回出現(xiàn)內(nèi)存溢出琼了,內(nèi)存泄漏逻锐?
  • 3、說(shuō)說(shuō)Java線程棧
  • 4雕薪、JVM 年輕代到年老代的晉升過(guò)程的判斷條件是什么呢昧诱?
  • 5、JVM 出現(xiàn) fullGC 很頻繁所袁,怎么去線上排查問(wèn)題盏档?
  • 6、類(lèi)加載為什么要使用雙親委派模式燥爷,有沒(méi)有什么場(chǎng)景是打破了這個(gè)模式蜈亩?
  • 7、類(lèi)的實(shí)例化順序
  • 8前翎、JVM垃圾回收機(jī)制稚配,何時(shí)觸發(fā)MinorGC等操作
  • 9、JVM 中一次完整的 GC 流程(從 ygc 到 fgc)是怎樣的
  • 10港华、各種回收器道川,各自?xún)?yōu)缺點(diǎn),重點(diǎn)CMS立宜、G1
  • 11愤惰、各種回收算法
  • 12、OOM錯(cuò)誤赘理,stackoverflow錯(cuò)誤,permgen space錯(cuò)誤

題目來(lái)源:

2.1詳細(xì)jvm內(nèi)存模型

根據(jù) JVM 規(guī)范扇单,JVM 內(nèi)存共分為虛擬機(jī)棧商模、堆、方法區(qū)蜘澜、程序計(jì)數(shù)器施流、本地方法棧五個(gè)部分。

image

具體可能會(huì)聊聊jdk1.7以前的PermGen(永久代)鄙信,替換成Metaspace(元空間)

  • 原本永久代存儲(chǔ)的數(shù)據(jù):符號(hào)引用(Symbols)轉(zhuǎn)移到了native heap瞪醋;字面量(interned strings)轉(zhuǎn)移到了java heap;類(lèi)的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap
  • Metaspace(元空間)存儲(chǔ)的是類(lèi)的元數(shù)據(jù)信息(metadata)
  • 元空間的本質(zhì)和永久代類(lèi)似装诡,都是對(duì)JVM規(guī)范中方法區(qū)的實(shí)現(xiàn)银受。不過(guò)元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中践盼,而是使用本地內(nèi)存
  • 替換的好處:一宾巍、字符串存在永久代中咕幻,容易出現(xiàn)性能問(wèn)題和內(nèi)存溢出耍贾。二流椒、永久代會(huì)為 GC 帶來(lái)不必要的復(fù)雜度,并且回收效率偏低
image

圖片來(lái)源:https://blog.csdn.net/tophawk/article/details/78704074

參考資料:

2.2講講什么情況下回出現(xiàn)內(nèi)存溢出得湘,內(nèi)存泄漏选浑?

內(nèi)存泄漏的原因很簡(jiǎn)單:

  • 對(duì)象是可達(dá)的(一直被引用)
  • 但是對(duì)象不會(huì)被使用

常見(jiàn)的內(nèi)存泄漏例子:


 public static void main(String[] args) {

        Set set = new HashSet();

        for (int i = 0; i < 10; i++) {
            Object object = new Object();
            set.add(object);

            // 設(shè)置為空蓝厌,這對(duì)象我不再用了
            object = null;
        }

        // 但是set集合中還維護(hù)這obj的引用,gc不會(huì)回收object對(duì)象
        System.out.println(set);
    }

解決這個(gè)內(nèi)存泄漏問(wèn)題也很簡(jiǎn)單古徒,將set設(shè)置為null拓提,那就可以避免上訴內(nèi)存泄漏問(wèn)題了。其他內(nèi)存泄漏得一步一步分析了描函。

內(nèi)存泄漏參考資料:

內(nèi)存溢出的原因:

  • 內(nèi)存泄露導(dǎo)致堆棧內(nèi)存不斷增大崎苗,從而引發(fā)內(nèi)存溢出。
  • 大量的jar舀寓,class文件加載胆数,裝載類(lèi)的空間不夠,溢出
  • 操作大量的對(duì)象導(dǎo)致堆內(nèi)存空間已經(jīng)用滿(mǎn)了互墓,溢出
  • nio直接操作內(nèi)存必尼,內(nèi)存過(guò)大導(dǎo)致溢出

解決:

  • 查看程序是否存在內(nèi)存泄漏的問(wèn)題
  • 設(shè)置參數(shù)加大空間
  • 代碼中是否存在死循環(huán)或循環(huán)產(chǎn)生過(guò)多重復(fù)的對(duì)象實(shí)體、
  • 查看是否使用了nio直接操作內(nèi)存篡撵。

參考資料:

2.3說(shuō)說(shuō)線程棧

這里的線程棧應(yīng)該指的是虛擬機(jī)棧吧...

JVM規(guī)范讓每個(gè)Java線程擁有自己的獨(dú)立的JVM棧判莉,也就是Java方法的調(diào)用棧。

當(dāng)方法調(diào)用的時(shí)候育谬,會(huì)生成一個(gè)棧幀券盅。棧幀是保存在虛擬機(jī)棧中的,棧幀存儲(chǔ)了方法的局部變量表膛檀、操作數(shù)棧锰镀、動(dòng)態(tài)連接和方法返回地址等信息

線程運(yùn)行過(guò)程中,只有一個(gè)棧幀是處于活躍狀態(tài)咖刃,稱(chēng)為“當(dāng)前活躍棧幀”泳炉,當(dāng)前活動(dòng)棧幀始終是虛擬機(jī)棧的棧頂元素

通過(guò)jstack工具查看線程狀態(tài)

參考資料:

2.4JVM 年輕代到年老代的晉升過(guò)程的判斷條件是什么呢嚎杨?

  1. 部分對(duì)象會(huì)在From和To區(qū)域中復(fù)制來(lái)復(fù)制去,如此交換15次(由JVM參數(shù)MaxTenuringThreshold決定,這個(gè)參數(shù)默認(rèn)是15),最終如果還是存活,就存入到老年代花鹅。
  2. 如果對(duì)象的大小大于Eden的二分之一會(huì)直接分配在old,如果old也分配不下枫浙,會(huì)做一次majorGC刨肃,如果小于eden的一半但是沒(méi)有足夠的空間古拴,就進(jìn)行minorgc也就是新生代GC。
  3. minor gc后之景,survivor仍然放不下斤富,則放到老年代
  4. 動(dòng)態(tài)年齡判斷 ,大于等于某個(gè)年齡的對(duì)象超過(guò)了survivor空間一半 锻狗,大于等于某個(gè)年齡的對(duì)象直接進(jìn)入老年代

2.5JVM 出現(xiàn) fullGC 很頻繁满力,怎么去線上排查問(wèn)題

這題就依據(jù)full GC的觸發(fā)條件來(lái)做:

  • 如果有perm gen的話(jdk1.8就沒(méi)了),要給perm gen分配空間轻纪,但沒(méi)有足夠的空間時(shí)油额,會(huì)觸發(fā)full gc。
    • 所以看看是不是perm gen區(qū)的值設(shè)置得太小了刻帚。
  • System.gc()方法的調(diào)用
    • 這個(gè)一般沒(méi)人去調(diào)用吧~~~
  • 當(dāng)統(tǒng)計(jì)得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間潦嘶,則會(huì)觸發(fā)full gc(這就可以從多個(gè)角度上看了)
    • 是不是頻繁創(chuàng)建了大對(duì)象(也有可能eden區(qū)設(shè)置過(guò)小)(大對(duì)象直接分配在老年代中,導(dǎo)致老年代空間不足--->從而頻繁gc)
    • 是不是老年代的空間設(shè)置過(guò)小了(Minor GC幾個(gè)對(duì)象就大于老年代的剩余空間了)
image

2.6類(lèi)加載為什么要使用雙親委派模式崇众,有沒(méi)有什么場(chǎng)景是打破了這個(gè)模式掂僵?

雙親委托模型的重要用途是為了解決類(lèi)載入過(guò)程中的安全性問(wèn)題

  • 假設(shè)有一個(gè)開(kāi)發(fā)者自己編寫(xiě)了一個(gè)名為java.lang.Object的類(lèi)顷歌,想借此欺騙JVM∶膛睿現(xiàn)在他要使用自定義ClassLoader來(lái)加載自己編寫(xiě)的java.lang.Object類(lèi)。
  • 然而幸運(yùn)的是眯漩,雙親委托模型不會(huì)讓他成功芹扭。因?yàn)镴VM會(huì)優(yōu)先在Bootstrap ClassLoader的路徑下找到java.lang.Object類(lèi),并載入它

Java的類(lèi)加載是否一定遵循雙親委托模型赦抖?

參考資料:

2.7類(lèi)的實(shí)例化順序

  • 1. 父類(lèi)靜態(tài)成員和靜態(tài)初始化塊 要尔,按在代碼中出現(xiàn)的順序依次執(zhí)行
  • 2. 子類(lèi)靜態(tài)成員和靜態(tài)初始化塊 交胚,按在代碼中出現(xiàn)的順序依次執(zhí)行
  • 3. 父類(lèi)實(shí)例成員和實(shí)例初始化塊 ,按在代碼中出現(xiàn)的順序依次執(zhí)行
  • 4. 父類(lèi)構(gòu)造方法
  • 5. 子類(lèi)實(shí)例成員和實(shí)例初始化塊 盈电,按在代碼中出現(xiàn)的順序依次執(zhí)行
  • 6. 子類(lèi)構(gòu)造方法

檢驗(yàn)一下是不是真懂了:


class Dervied extends Base {


    private String name = "Java3y";

    public Dervied() {
        tellName();
        printName();
    }

    public void tellName() {
        System.out.println("Dervied tell name: " + name);
    }

    public void printName() {
        System.out.println("Dervied print name: " + name);
    }

    public static void main(String[] args) {

        new Dervied();
    }
}

class Base {

    private String name = "公眾號(hào)";

    public Base() {
        tellName();
        printName();
    }

    public void tellName() {
        System.out.println("Base tell name: " + name);
    }

    public void printName() {
        System.out.println("Base print name: " + name);
    }
}

輸出數(shù)據(jù):


Dervied tell name: null
Dervied print name: null
Dervied tell name: Java3y
Dervied print name: Java3y

第一次做錯(cuò)的同學(xué)點(diǎn)個(gè)贊,加個(gè)關(guān)注不過(guò)分吧(hahaha

2.8JVM垃圾回收機(jī)制杯活,何時(shí)觸發(fā)MinorGC等操作

當(dāng)young gen中的eden區(qū)分配滿(mǎn)的時(shí)候觸發(fā)MinorGC(新生代的空間不夠放的時(shí)候).

2.9JVM 中一次完整的 GC 流程(從 ygc 到 fgc)是怎樣的

這題不是很明白意思(水平有限...如果知道這題的意思可在評(píng)論區(qū)留言呀~~)

  • 因?yàn)榘次业睦斫猓簣?zhí)行fgc是不會(huì)執(zhí)行ygc的呀~~

YGC和FGC是什么

  • YGC :對(duì)新生代堆進(jìn)行g(shù)c匆帚。頻率比較高,因?yàn)榇蟛糠謱?duì)象的存活壽命較短旁钧,在新生代里被回收吸重。性能耗費(fèi)較小互拾。
  • FGC :全堆范圍的gc。默認(rèn)堆空間使用到達(dá)80%(可調(diào)整)的時(shí)候會(huì)觸發(fā)fgc嚎幸。以我們生產(chǎn)環(huán)境為例颜矿,一般比較少會(huì)觸發(fā)fgc,有時(shí)10天或一周左右會(huì)有一次嫉晶。

什么時(shí)候執(zhí)行YGC和FGC

  • a.eden空間不足,執(zhí)行 young gc
  • b.old空間不足骑疆,perm空間不足,調(diào)用方法System.gc() 替废,ygc時(shí)的悲觀策略, dump live的內(nèi)存信息時(shí)(jmap –dump:live)箍铭,都會(huì)執(zhí)行full gc

2.10各種回收算法

GC最基礎(chǔ)的算法有三種:

  • 標(biāo)記 -清除算法
  • 復(fù)制算法
  • 標(biāo)記-壓縮算法
  • 我們常用的垃圾回收器一般都采用分代收集算法(其實(shí)就是組合上面的算法,不同的區(qū)域使用不同的算法)椎镣。

具體:

  • 標(biāo)記-清除算法诈火,“標(biāo)記-清除”(Mark-Sweep)算法,如它的名字一樣状答,算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象冷守,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象。
  • 復(fù)制算法惊科,“復(fù)制”(Copying)的收集算法拍摇,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊译断。當(dāng)這一塊的內(nèi)存用完了授翻,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉孙咪。
  • 標(biāo)記-壓縮算法堪唐,標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理翎蹈,而是讓所有存活的對(duì)象都向一端移動(dòng)淮菠,然后直接清理掉端邊界以外的內(nèi)存
  • 分代收集算法,“分代收集”(Generational Collection)算法荤堪,把Java堆分為新生代和老年代合陵,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/li>

2.11各種回收器,各自?xún)?yōu)缺點(diǎn)澄阳,重點(diǎn)CMS拥知、G1

圖來(lái)源于《深入理解Java虛擬機(jī):JVM高級(jí)特效與最佳實(shí)現(xiàn)》,圖中兩個(gè)收集器之間有連線碎赢,說(shuō)明它們可以配合使用.

image
  • Serial收集器低剔,串行收集器是最古老,最穩(wěn)定以及效率高的收集器,但可能會(huì)產(chǎn)生較長(zhǎng)的停頓襟齿,只使用一個(gè)線程去回收姻锁。
  • ParNew收集器,ParNew收集器其實(shí)就是Serial收集器的多線程版本猜欺。
  • Parallel收集器位隶,Parallel Scavenge收集器類(lèi)似ParNew收集器,Parallel收集器更關(guān)注系統(tǒng)的吞吐量开皿。
  • Parallel Old收集器涧黄,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程“標(biāo)記-整理”算法
  • CMS收集器副瀑,CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器弓熏。它需要消耗額外的CPU和內(nèi)存資源,在CPU和內(nèi)存資源緊張糠睡,CPU較少時(shí)挽鞠,會(huì)加重系統(tǒng)負(fù)擔(dān)。CMS無(wú)法處理浮動(dòng)垃圾狈孔。CMS的“標(biāo)記-清除”算法信认,會(huì)導(dǎo)致大量空間碎片的產(chǎn)生
  • G1收集器均抽,G1 (Garbage-First)是一款面向服務(wù)器的垃圾收集器,主要針對(duì)配備多顆處理器及大容量?jī)?nèi)存的機(jī)器. 以極高概率滿(mǎn)足GC停頓時(shí)間要求的同時(shí),還具備高吞吐量性能特征嫁赏。

2.12stackoverflow錯(cuò)誤,permgen space錯(cuò)誤

stackoverflow錯(cuò)誤主要出現(xiàn):

  • 在虛擬機(jī)棧中(線程請(qǐng)求的棧深度大于虛擬機(jī)棧鎖允許的最大深度)

permgen space錯(cuò)誤(針對(duì)jdk之前1.7版本):

  • 大量加載class文件
  • 常量池內(nèi)存溢出

三油挥、總結(jié)

總的來(lái)說(shuō)潦蝇,JVM在初級(jí)的層面上還是偏理論多,可能要做具體的東西才會(huì)有更深的體會(huì)深寥。這篇主要是入個(gè)門(mén)吧~

這篇文章懶懶散散也算把JVM比較重要的知識(shí)點(diǎn)理了一遍了攘乒,后面打算學(xué)學(xué),寫(xiě)寫(xiě)SpringCloud的東西惋鹅。

參考資料:

如果文章有錯(cuò)的地方歡迎指正则酝,大家互相交流。習(xí)慣在微信看技術(shù)文章闰集,想要獲取更多的Java資源的同學(xué)沽讹,可以關(guān)注微信公眾號(hào):Java3y。為了大家方便武鲁,剛新建了一下qq群:742919422爽雄,大家也可以去交流交流。謝謝支持了沐鼠!希望能多介紹給其他有需要的朋友

文章的目錄導(dǎo)航

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盲链,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刽沾,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件排拷,死亡現(xiàn)場(chǎng)離奇詭異侧漓,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)监氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)布蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人浪腐,你說(shuō)我怎么就攤上這事纵揍。” “怎么了议街?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵泽谨,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我特漩,道長(zhǎng)吧雹,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任涂身,我火速辦了婚禮雄卷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蛤售。我一直安慰自己丁鹉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布悴能。 她就那樣靜靜地躺著揣钦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搜骡。 梳的紋絲不亂的頭發(fā)上拂盯,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音记靡,去河邊找鬼谈竿。 笑死,一個(gè)胖子當(dāng)著我的面吹牛摸吠,可吹牛的內(nèi)容都是我干的空凸。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼寸痢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼呀洲!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤道逗,失蹤者是張志新(化名)和其女友劉穎兵罢,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體滓窍,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卖词,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吏夯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片此蜈。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖噪生,靈堂內(nèi)的尸體忽然破棺而出裆赵,到底是詐尸還是另有隱情,我是刑警寧澤跺嗽,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布战授,位于F島的核電站,受9級(jí)特大地震影響抛蚁,放射性物質(zhì)發(fā)生泄漏陈醒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一瞧甩、第九天 我趴在偏房一處隱蔽的房頂上張望钉跷。 院中可真熱鬧,春花似錦肚逸、人聲如沸爷辙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)膝晾。三九已至,卻和暖如春务冕,著一層夾襖步出監(jiān)牢的瞬間血当,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工禀忆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臊旭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓箩退,卻偏偏與公主長(zhǎng)得像离熏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戴涝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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