如何閱讀 Java 字節(jié)碼(Byte Code)

原文來自個人博客:如何閱讀 Java 字節(jié)碼(Byte Code) | Kori Lin

字節(jié)碼(Byte Code)

學習 Java 的都知道靖诗,我們所編寫的 .java 代碼文件通過編譯將會生成 .class 文件辩恼,最初的方式就是通過 JDK 的 javac 指令來編譯深员,再通過 java 命令執(zhí)行 main 方法所在的類,從而執(zhí)行我們的 Java 程序覆致。而在這中間所生成的 .class 文件中的內(nèi)容,就是 JVM 可以處理運行的字節(jié)碼(Byte Code)拷获,它由 JVM 解釋為對應系統(tǒng)可運行的機器指令待榔,這也是我們的 Java 程序能夠做到一處編譯處處執(zhí)行的原理。

對于 Java 開發(fā)人員來說猎莲,平時需要閱讀 Byte Code 的場景比較少绍弟,但和閱讀框架源碼能夠了解到框架的設計思路一樣,閱讀 Java Byte Code 也有利于我們理解 Java 一些深層的東西著洼,提高我們解決問題的能力樟遣。能夠閱讀 Byte Code 也有利于我們?nèi)ダ斫?Kotlin 或其它運行在 JVM 上的語言,是如何擴展 Java 所沒有的特性或語法糖身笤。

字節(jié)碼文件結(jié)構(gòu)

public class Hello {
    public static void main(String[] args) {
        int a = 1;
        int b = 1;
        int c = add(a, b);
        System.out.println(c);
    }

    public static int add(int a, int b) {
        return a + b;
    }
}

首先我們先編寫一個簡單的 Java 代碼作為演示例子豹悬,然后編譯這個 Hello.java 文件得到 Hello.class 文件。我們知道 .class 是二進制文件展鸡,它無法被直接查看屿衅,當然我們可以通過一些二進制文件查看工具來閱讀里面的內(nèi)容。

(在 Visual Studio Code 里使用 hexdump for VSCode 插件查看到的內(nèi)容)

一個符合標準的 .class 文件是以 CA FE BA BE 開頭莹弊,這個四個字節(jié)均為魔數(shù)涤久,JVM 根據(jù)這個開頭來判斷一個文件是否可能為 .class 文件涡尘,如果是才會繼續(xù)執(zhí)行。

魔數(shù)后面四個字節(jié) 00 00 00 34 是版本號响迂,前兩個字節(jié)為次版本號考抄,后兩個字節(jié)為主版本號,在對主版本號進行轉(zhuǎn)換可以得到 52蔗彤,該序號對應的Java 版本為1.8川梅。

如果需要查閱最新的 Java 版本對應的版本號,可以在官方查看 Java SE 最新版本的文檔

Oracle Java SE Specifications:https://docs.oracle.com/javase/specs/index.html

在版本號后面則是常量池然遏,它包含常量池計數(shù)器(constant_pool_count)和常量池數(shù)據(jù)區(qū)兩個部分贫途。前面兩個字節(jié) 00 28 是計數(shù)器,用于表示常量池的容量計數(shù)值待侵,代表常量池數(shù)據(jù)區(qū)有 constant_pool_count - 1 個常量丢早。

常量池結(jié)構(gòu)

在常量池后面還有訪問標志,很顯然這個文件對于我們來說閱讀起來并不方便秧倾,但是我們可以轉(zhuǎn)換為助記符來幫助我們閱讀怨酝。

使用 javap 命令

當我們擁有一個 .class 文件時,我們可以通過 javap 來將字節(jié)碼指令轉(zhuǎn)換為助記符那先,這個命令有一些參數(shù)农猬,你可以通過 javap -help 來查看所有參數(shù)的說明,這里為了顯示盡量詳細的內(nèi)容售淡,使用 javap -verbose Hello斤葱,其效果如下,但由于內(nèi)容太長勋又,我們不一次性展示所有內(nèi)容苦掘,而是分區(qū)域來進行閱讀。

Classfile /F:/project/java/JavaMain/out/production/JavaMain/Hello.class
  Last modified 2021-3-26; size 645 bytes
  MD5 checksum ca1b2193159aece89c05c7f9d3b54c7b
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

最開始幾行是關(guān)于這個文件的基本信息楔壤,之后是類的信息鹤啡,我們的關(guān)注點從類這里開始。
在類里面蹲嚣,它包含了主版本號(major version)递瑰,次版本號(minor version),訪問標志(flags)隙畜。
對于版本號抖部,和我們上面所講的一樣,因此這里不再重復议惰。而 flags 是關(guān)于這個類的相關(guān)修飾符慎颗,根據(jù)官方文檔,它可能擁有的值如下:

常量池

在類信息的下面,則是常量池俯萎,它類似一個表傲宜,每個常量由編號、類型夫啊、值函卒,這3個部分組成。我們列出一小部分來了解它的結(jié)構(gòu)撇眯。

Constant pool:
   #1 = Methodref          #6.#26         // java/lang/Object."<init>":()V
   #2 = Methodref          #5.#27         // Hello.add:(II)I
   #3 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #30.#31        // java/io/PrintStream.println:(I)V
   #5 = Class              #32            // Hello
   #6 = Class              #33            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LHello;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V

在這里列出了前15個常量报嵌,每個常量都有一個編號,編號以符號 # 開頭熊榛,編號后面是 = 加上該常量的類型锚国,具體類型說明請參考官方 jvms 文檔的 The Constant Pool 的解釋。這里我們從中挑選幾個常量來進行說明玄坦。

由于我們使用的命令是 javap -verbose 因此它會給我們加上一些注釋說明跷叉,我們可以看到 #2 常量后面的注釋是 Hello.add:(II)I,那么我們可以猜測它應該是我們代碼中所編寫的 add() 方法营搅,由于這個方法是靜態(tài),因此它被加入到這個類中的常量區(qū)梆砸。那么這個常量的值呢转质?

對于 Methodref 類型,它的值是該方法的名稱帖世,對于一個方法而言休蟹,它的名稱是一個字符串,因此構(gòu)成方法名稱的字符串也會被添加到常量池中日矫,#2 這個方法赂弓,它的名稱引用了常量池中其它的兩個常量,也就是 #5 和 #27哪轿,而這兩個常量以及與它們相關(guān)的其它常量在常量池中的值如下:

   #2 = Methodref          #5.#27         // Hello.add:(II)I
   #5 = Class              #32            // Hello
  #22 = Utf8               add
  #23 = Utf8               (II)I
  #27 = NameAndType        #22:#23        // add:(II)I
  #32 = Utf8               Hello

在常量池中我們可以看到盈魁,#5 是一個類,它的值和方法一樣都是名稱窃诉,因此它引用了常量 #32杨耙,對于 Utf8 類型的常量,其值則是一個字符串飘痛,也就是常量 #32 的值就是字符串 Hello珊膜。因此 #5 的值就是 Hello。同樣的 #27 的值是 add:(II)I宣脉,將它們組合起來 #2 的值就是 Hello.add:(II)I 了车柠。

包含的方法

與 Java 代碼一樣,我們所定義的方法在類里面,而在字節(jié)碼中我們定義在類中的方法也放在大括號里面竹祷,而這個大括號就在常量池下方谈跛。

對于每個方法,都包含首行的聲明溶褪,以及緊接在后面的 descriptor(描述符號)币旧,flags(訪問標識),Code(代碼)猿妈,我們把 Code 部分的內(nèi)容先省略吹菱,先看一下類中所擁有的所有方法。

{
  public Hello();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      // 省略

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      // 省略

  public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      // 省略
}

可以看到彭则,我們并沒有給類寫構(gòu)造方法鳍刷,但這里卻生成了 public Hello(); ,這也說明了為什么我們在 Java 中不給類寫構(gòu)造方法俯抖,默認會有一個無參的構(gòu)造方法输瓜。

另外我們可以看到到 add 方法的 descriptor 為 (II)I,這與我們上面觀察常量池時 Hello.add:(II)I 冒號后面帶的字符是一樣的芬萍,這其實是這個方法的參數(shù)以及返回值的縮寫尤揣。當我們在 Java 中編寫重載方法時,由于方法名一樣柬祠,JVM 可以通過 descriptor 來區(qū)分所調(diào)用的方法是哪一個北戏。

而 flags 與 Class 的 flags 類似,用于聲明方法所擁有的修飾符漫蛔。而最后的 Code 中包含的則是該方法的代碼所執(zhí)行的指令嗜愈。

Code 的結(jié)構(gòu)

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_1
         3: istore_2
         4: iload_1
         5: iload_2
         6: invokestatic  #2                  // Method add:(II)I
         9: istore_3
        10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 4
        line 6: 10
        line 7: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            2      16     1     a   I
            4      14     2     b   I
           10       8     3     c   I

以 main 方法為例子,在 Code 中可以分為3個部分莽龟,第一個部分是代碼的指令表蠕嫁,該部分第一行是該方法的指令以及執(zhí)行過程的相關(guān)信息,這一行信息包括:

  • args_size 是參數(shù)數(shù)量毯盈,在主函數(shù)中剃毒,因為有 args 這個參數(shù),所以在這里 args_size 為 1奶镶;
  • locals 是該方法中的本地變量有多少個迟赃,在我們的主函數(shù)里面有定義了 3 個變量,加上一個參數(shù)厂镇,因此有 4 個變量纤壁;
  • stack 是方法在執(zhí)行過程中,操作數(shù)棧中最大深度捺信,這個在之后講解指令執(zhí)行過程時可以看出酌媒。

在這一行信息之后是字節(jié)碼指令欠痴,一條指令包括偏移量以及執(zhí)行的指令碼,PC Register 利用偏移量來判斷指令執(zhí)行位置秒咨。

第二部分是 LineNumberTable喇辽,它是一個代碼行對照表,用于標識我們 Java 代碼中對應的行數(shù)的代碼在 Code 中從哪個位置開始雨席。

LineNumberTable:
  line 3: 0
  line 4: 2
  line 5: 4
  line 6: 10
  line 7: 17

line 3: 0 代表 Java 源碼文件中的第三行代碼從偏移量為 0 的位置開始菩咨,而繼續(xù)往下看可以看到第四行代碼從偏移量為 2 的位置開始,也就是說第三行代碼所對應的字節(jié)碼指令有 iconst_1 和 istore_1 兩條陡厘。這也可以讓 JVM 執(zhí)行指令出現(xiàn)錯誤時抽米,幫助我們定位到對應的源碼位置。

第三部分為 LocalVariableTable糙置,調(diào)試器可以使用它在方法執(zhí)行的過程中確定局部變量的值云茸,它是一個可選的屬性,在程序執(zhí)行的時候并不需要它谤饭。

  • 第一個屬性 start 為這個變量可見的起始偏移位置标捺,它的值必須是在Code 中存在的偏移量值。
  • 第二個屬性 length 為該變量的有效長度揉抵,在這個例子中亡容,我們的變量直到方法末尾都有效,因此你會發(fā)現(xiàn) start + lenth 的值都是 18 (方法中執(zhí)行的指令數(shù))冤今。當我們在一個局部的代碼塊里面聲明一個變量萍倡,那么它的有效期長度將會更短。
  • Slot 為變量在 local variable 中的位置辟汰,這可以幫助我們在指令中確定對應的變量,而 Name 則是變量名阱佛,Signature 為該變量的類型帖汞。

當我們初步了解了 Code 屬性的格式后,我們就可以對其中的指令執(zhí)行進行分析了凑术,但在此之前翩蘸,我們需要先有一點 JVM 內(nèi)存布局的基礎知識,這對于我們之后了解指令的執(zhí)行過程非常重要淮逊。

JVM 內(nèi)存結(jié)構(gòu)

我們的Java程序在運行時是通過 main() 方法啟動催首,它是程序的入口,我們的進程在啟動時會為該方法創(chuàng)建一個主線程來執(zhí)行代碼泄鹏。當我們使用多線程時郎任,那么程序的進程將會擁有多個線程。每個線程的資源都擁有獨自的資源备籽,當然它們也可以共享進程的資源舶治,那么在 JVM 中,根據(jù)資源的可用范圍,可將內(nèi)存區(qū)域分為線程獨占和線程共享兩個類別霉猛。

JVM 內(nèi)存布局

對于每一個線程尺锚,都可將其擁有的內(nèi)存空間分為 PC Register、Native Method Stack惜浅、JVM Stack 這3個區(qū)域瘫辩,這3個區(qū)域?qū)τ诰€程來說都是獨占的,其它線程無法進行訪問坛悉。

  • PC Register 用于記錄當前線程指令的執(zhí)行位置伐厌。由于一個進程可能有多個線程,而CPU會在不同線程之間切換吹散,為了能夠記錄各個線程的當前執(zhí)行的指令弧械,每個線程都需要有一個 PC Register,來保證各個線程都可以進行獨立運算空民。
  • JVM Stack 用于存放調(diào)用方法時壓入棧的棧幀刃唐。相信學過數(shù)據(jù)結(jié)構(gòu)的對棧應該不陌生,JVM Stack 壓入的單位為棧幀(Frame)界轩,用于存儲數(shù)據(jù)画饥、動態(tài)鏈接、方法返回值和調(diào)度異常等浊猾。每次調(diào)用一個方法都會創(chuàng)建一個新的棧幀壓入 JVM Stack 來存儲該方法的信息抖甘,當該方法調(diào)用完成時,對應的棧幀也會跟著被銷毀葫慎。一個棧幀都有自己的局部變量數(shù)組衔彻、操作數(shù)棧、對當前方法類的運行常量池的引用偷办。
  • Native Method Stack 則是用于調(diào)用操作系統(tǒng)本地方法時使用的椉瓒睿空間。

每個線程都可用訪問的內(nèi)存空間為線程共享區(qū)域椒涯,它包含 Head 和 Method Area 兩個部分柄沮,Head 用于存放實例對象,也是 GC 回收的主要區(qū)域废岂,而 Method Area 用于存放類結(jié)構(gòu)與靜態(tài)變量祖搓。

現(xiàn)在我們初步了解了 JVM 內(nèi)存的布局,那么接下來可以繼續(xù)看指令的執(zhí)行過程了湖苞。

指令的執(zhí)行過程

由于 Java 程序從 main() 方法開始拯欧,我們也是從這個方法的指令開始進行分析

假設程序運行 0 號指令前的狀態(tài)如下,在 mian 方法棧幀里面财骨,有著 operand stack(操作數(shù)棧)哈扮,它的最大長度為 2(與 Code 下的 stack 的值一致)纬纪,此外還有一個 local variable(本地變量表)來存放變量的值,其中下標為 0 的變量為主方法的參數(shù) args滑肉,我們直接用這個字符串填充在那里來做一個標識(實際的值可能是一個空數(shù)組)

[圖片上傳中...(image-d86095-1616931895122)]

接下來我們一步步執(zhí)行方法中的指令包各,在這里我們先對出現(xiàn)的幾個指令做一個簡單的介紹:

  • iconst_<i> 放一個 int 常量(-1, 0, 1, 2, 3, 4 or 5) 到 operand stack 中
  • istore_<n> 從 operand stack 中獲取一個 int 到 local variable 的 n 中
  • iload_<n> 從 local variable 中讀取 int 變量 n 的值到操作數(shù)棧中
  • invokestatic 調(diào)用一個 class 的 static 方法
  • getstatic 從 class 中獲取一個 static 字段
  • invokevirtual 調(diào)用一個實例方法,基于類的調(diào)度
  • return 從方法中返回一個 void靶庙,ireturn 從方法中返回 operand stack 棧頂?shù)?int

更多的指令與詳細的說明請查看文章最后參考中的官方指令文檔

現(xiàn)在我們開始分析指令的執(zhí)行问畅,我們在上面知道了,我們的 Java 代碼所對應的指令分別是偏移量為 0 和 1 的兩個六荒,最開始執(zhí)行的是 0: iconst_1护姆,該指令會把 int 常量 1 放置到 operand stack 中,之后執(zhí)行的是 1: istore_1掏击,把 operand stack 棧頂?shù)?int 常量取出放到 local variable 下標為 1 的變量中卵皂,該過程圖示如下。

iconst_1 與 istore_1 的執(zhí)行過程

我們可以通過查看 LocalVariableTable 得知下標為 1 的變量在我們的 Java 程序中是 int 變量 a砚亭,因此上面這兩條指令常量 1 賦值給變量 a灯变。同樣的,后面兩條指令則是將常量 1 賦值給變量 b捅膘。這里要注意添祸,操作數(shù)棧的數(shù)是被取出操作,被取出的數(shù)將不會繼續(xù)在 operand stack 里面寻仗。

執(zhí)行完 0~3 這 4 條指令后刃泌,就來到了本例中最為關(guān)鍵的方法調(diào)用了。在執(zhí)行 iload_1 和 iload_2 后署尤,operand stack 中將會存放著變量 a 和 b 的值耙替,作為 invokestatic 調(diào)用函數(shù)時傳入的參數(shù)。

而執(zhí)行到 invokestatic #2 這個指令的時候曹体,該指令為調(diào)用一個 class 的 static 方法林艘,也就是調(diào)用常量池中 #2 的方法,該方法為 Hello.add:(II)I 混坞。

當執(zhí)行 invokestatic 時會依次讀取 operand stack 的數(shù)據(jù)作為方法的參數(shù),并創(chuàng)建一個新的棧幀來執(zhí)行方法钢坦,將數(shù)據(jù)放到 local variable 對應變量位置究孕。

之后開始執(zhí)行 add() 方法中的指令,首先執(zhí)行的是兩個 iload 指令爹凹,將 loca variable 對應下標的變量的值放到 operand stack 中厨诸,之后執(zhí)行 iadd 取出 operand stack 中的值并進行加法運算,再把結(jié)果放到禾酱,最后執(zhí)行 ireturn 取出 operand stack 頂部的 int 值進行返回微酬。

執(zhí)行了兩個 iload 指令
執(zhí)行 iadd 指令
ireturn 取出棧頂?shù)?int 常量作為方法的返回值
執(zhí)行 istore_3

當執(zhí)行完 ireturn 后绘趋,add 方法也就執(zhí)行完成了,對應的棧幀也會跟著銷毀颗管。之后回到 main 方法中繼續(xù)往下執(zhí)行陷遮,到 istore_3 指令,該指令將棧頂?shù)?int 值取出放到了 local variable 中 Solt 為 3 的地方垦江,這樣執(zhí)行完 4~9 這幾條指令后就完成了我們代碼中的 int c = add(a, b); 這一行代碼帽馋。那么接下來就是執(zhí)行 System.out.println(c); 對應的指令將 2 打印到控制臺了。

到這里其實我們就已經(jīng)知道如何去閱讀我們代碼生成的 Byte Code 了比吭,這里我就不繼續(xù)往下分析本文例子的代碼了绽族,閱讀過程中如果遇到了沒見過的指令,我們可以在 Oracle 官方指令文檔里面查閱對應的說明衩藤。

那可能有人會覺得吧慢,如果每次查看一個類都需要去 command line 執(zhí)行 javap 來查看對應的助記符,這樣非常麻煩呀赏表。那么接下來我們講一下如何在 IntelliJ IDEA 里面直接閱讀 Byte Code检诗。

在 IntelliJ IDEA 閱讀

如果你希望在 IntelliJ IDEA 里閱讀 Byte Code,那么可以按照 Bytecode Viewer 這一個插件底哗,只需要在 Plugins 里面查找就能找到岁诉。

Bytecode Viewer

安裝完這個插件,在頂部菜單欄的 View 中將會多出一個 Show Bytecode 按鈕選項跋选,我們可以在對應的 .java 文件中點擊 View -> Show Bytecode涕癣,展示出該文件的 Byte Code。

View -> Show Bytecode

在這里所展示的 Byte Code 格式與我們上面使用 javap 顯示出來的不一樣前标。首先在這里我們看不到常量池坠韩,因此在指令里也不會用引用的方式來表示常量池的內(nèi)容。

在這里它會將每一行 Java 代碼的指令都區(qū)分開炼列。例如 main 方法中的第一行指令對應的就是 L0 那一塊只搁,第一行的 LINENUMBER 對應上面 javap 中的 LineNumberTable,直接在這里描述當前 L0 這一塊的指令對應的代碼在文件中的位置俭尖。也因此我們不會在生成的這個 Byte Code 里面看到 LineNumberTable氢惋,因為它直接分布在各個指令塊中了。

main 方法中的 L5

在方法的最后稽犁,會多出一塊內(nèi)容來描述方法的信息焰望,在這里會將 LocalVariableTable 里的變量都列出來,但格式與 javap 的 LocalVariableTable 中的描述格式不一樣已亥,每一行 LOCALVARIABLE 代表一個變量熊赖,描述格式從左到右依次為變量名、類型虑椎、開始可見時的指令塊震鹉、最后有效的位置俱笛、Solt。除了描述方法中出現(xiàn)的變量外传趾,操作數(shù)棧最大深度和本地變量個數(shù)也在放在這里迎膜。

由于該插件主要是為了閱讀 Byte Code 中的指令,因此是以一種更加方便閱讀指令的方式展示 Byte Code墨缘,例如對指令根據(jù)源碼做分塊星虹,并把對應代碼行數(shù)放在指令塊的第一行,這樣我們就不需要去對照 LineNumberTable 尋找當前指令的代碼所在的位置了镊讼,反過來由于進行了分塊查詢對應代碼的指令也很方便宽涌。但這個插件顯示的內(nèi)容也少了很多東西,如果需要查看初始常量池的內(nèi)容蝶棋,那就需要使用 javap 了卸亮。

參考

  1. 字節(jié)碼增強技術(shù)探索:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
  2. 一文看懂 JVM 內(nèi)存布局及 GC 原理:https://www.infoq.cn/article/3wyretkqrhivtw4frmr3
  3. Oracle 官方說明文檔:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-4.html#jvms-4.10
  4. Oracle 官方指令文檔:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html

原文首發(fā)于InfoQ:https://xie.infoq.cn/article/a9fbc16488d3ebbe4b758ff92

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市玩裙,隨后出現(xiàn)的幾起案子兼贸,更是在濱河造成了極大的恐慌,老刑警劉巖吃溅,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溶诞,死亡現(xiàn)場離奇詭異,居然都是意外死亡决侈,警方通過查閱死者的電腦和手機螺垢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赖歌,“玉大人枉圃,你說我怎么就攤上這事÷耄” “怎么了孽亲?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長展父。 經(jīng)常有香客問我返劲,道長,這世上最難降的妖魔是什么栖茉? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任篮绿,我火速辦了婚禮,結(jié)果婚禮上衡载,老公的妹妹穿的比我還像新娘。我一直安慰自己隙袁,他們只是感情好痰娱,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布弃榨。 她就那樣靜靜地躺著,像睡著了一般梨睁。 火紅的嫁衣襯著肌膚如雪鲸睛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天坡贺,我揣著相機與錄音官辈,去河邊找鬼。 笑死遍坟,一個胖子當著我的面吹牛拳亿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播愿伴,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼肺魁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了隔节?” 一聲冷哼從身側(cè)響起鹅经,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎怎诫,沒想到半個月后瘾晃,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡幻妓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年蹦误,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涌哲。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡胖缤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阀圾,到底是詐尸還是另有隱情哪廓,我是刑警寧澤,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布初烘,位于F島的核電站涡真,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏肾筐。R本人自食惡果不足惜哆料,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吗铐。 院中可真熱鬧东亦,春花似錦、人聲如沸唬渗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至壮啊,卻和暖如春嫉鲸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背歹啼。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工玄渗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狸眼。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓藤树,卻偏偏與公主長得像,于是被迫代替她去往敵國和親份企。 傳聞我的和親對象是個殘疾皇子也榄,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

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