JAVA NIO之淺談內(nèi)存映射文件原理與DirectMemory(轉(zhuǎn))

轉(zhuǎn)自 [Java][IO]JAVA NIO之淺談內(nèi)存映射文件原理與DirectMemory

Java類庫中的NIO包相對于IO 包來說有一個新功能是內(nèi)存映射文件,日常編程中并不是經(jīng)常用到,但是在處理大文件時是比較理想的提高效率的手段。本文我主要想結(jié)合操作系統(tǒng)中(OS)相關(guān)方面的知識介紹一下原理。

在傳統(tǒng)的文件IO操作中,我們都是調(diào)用操作系統(tǒng)提供的底層標(biāo)準(zhǔn)IO系統(tǒng)調(diào)用函數(shù) read()、write() ,此時調(diào)用此函數(shù)的進(jìn)程(在JAVA中即java進(jìn)程)由當(dāng)前的用戶態(tài)切換到內(nèi)核態(tài),然后OS的內(nèi)核代碼負(fù)責(zé)將相應(yīng)的文件數(shù)據(jù)讀取到內(nèi)核的IO緩沖區(qū),然 后再把數(shù)據(jù)從內(nèi)核IO緩沖區(qū)拷貝到進(jìn)程的私有地址空間中去,這樣便完成了一次IO操作。至于為什么要多此一舉搞一個內(nèi)核IO緩沖區(qū)把原本只需一次拷貝數(shù)據(jù) 的事情搞成需要2次數(shù)據(jù)拷貝呢? 我想學(xué)過操作系統(tǒng)或者計算機系統(tǒng)結(jié)構(gòu)的人都知道,這么做是為了減少磁盤的IO操作,為了提高性能而考慮的,因為我們的程序訪問一般都帶有局部性,也就是所 謂的局部性原理,在這里主要是指的空間局部性,即我們訪問了文件的某一段數(shù)據(jù),那么接下去很可能還會訪問接下去的一段數(shù)據(jù),由于磁盤IO操作的速度比直接 訪問內(nèi)存慢了好幾個數(shù)量級,所以O(shè)S根據(jù)局部性原理會在一次 read()系統(tǒng)調(diào)用過程中預(yù)讀更多的文件數(shù)據(jù)緩存在內(nèi)核IO緩沖區(qū)中,當(dāng)繼續(xù)訪問的文件數(shù)據(jù)在緩沖區(qū)中時便直接拷貝數(shù)據(jù)到進(jìn)程私有空間,避免了再次的低 效率磁盤IO操作。在JAVA中當(dāng)我們采用IO包下的文件操作流,如:

FileInputStream in = new FileInputStream("D:\\java.txt");
in.read();

JAVA虛擬機內(nèi)部便會調(diào)用OS底層的 read()系統(tǒng)調(diào)用完成操作,如上所述,在第二次調(diào)用 in.read()的時候可能就是從內(nèi)核緩沖區(qū)直接返回數(shù)據(jù)了(可能還有經(jīng)過 native堆做一次中轉(zhuǎn),因為這些函數(shù)都被聲明為 native,即本地平臺相關(guān),所以可能在C代碼中有做一次中轉(zhuǎn),如 win32中是通過 C代碼從OS讀取數(shù)據(jù),然后再傳給JVM內(nèi)存)。既然如此,JAVA的IO包中為啥還要提供一個 BufferedInputStream 類來作為緩沖區(qū)呢。關(guān)鍵在于四個字,"系統(tǒng)調(diào)用"!當(dāng)讀取OS內(nèi)核緩沖區(qū)數(shù)據(jù)的時候,便發(fā)起了一次系統(tǒng)調(diào)用操作(通過native的C函數(shù)調(diào)用),而系統(tǒng) 調(diào)用的代價相對來說是比較高的,涉及到進(jìn)程用戶態(tài)和內(nèi)核態(tài)的上下文切換等一系列操作,所以我們經(jīng)常采用如下的包裝:

FileInputStream in = new FileInputStream("D:\\java.txt"); BufferedInputStream buf_in = new BufferedInputStream(in); buf_in.read();

這樣一來,我們每一次 buf_in.read() 時候,BufferedInputStream 會根據(jù)情況自動為我們預(yù)讀更多的字節(jié)數(shù)據(jù)到它自己維護(hù)的一個內(nèi)部字節(jié)數(shù)組緩沖區(qū)中,這樣我們便可以減少系統(tǒng)調(diào)用次數(shù),從而達(dá)到其緩沖區(qū)的目的。所以要明確 的一點是 BufferedInputStream 的作用不是減少 磁盤IO操作次數(shù)(這個OS已經(jīng)幫我們做了),而是通過減少系統(tǒng)調(diào)用次數(shù)來提高性能的。同理 BufferedOuputStream , BufferedReader/Writer 也是一樣的。在 C語言的函數(shù)庫中也有類似的實現(xiàn),如 fread(),這個函數(shù)就是 C語言中的緩沖IO,作用與BufferedInputStream()相同.
這里簡單的引用下JDK6 中 BufferedInputStream 的源碼驗證下:

public  class BufferedInputStream extends FilterInputStream {  
 
   private static int defaultBufferSize = 8192;  
 
   /** 
    * The internal buffer array where the data is stored. When necessary, 
    * it may be replaced by another array of 
    * a different size. 
    */  
   protected volatile byte buf[];  
 /** 
    * The index one greater than the index of the last valid byte in  
    * the buffer.  
    * This value is always 
    * in the range <code>0</code> through <code>buf.length</code>; 
    * elements <code>buf[0]</code>  through <code>buf[count-1] 
    * </code>contain buffered input data obtained 
    * from the underlying  input stream. 
    */  
   protected int count;  
 
   /** 
    * The current position in the buffer. This is the index of the next  
    * character to be read from the <code>buf</code> array.  
    * <p> 
    * This value is always in the range <code>0</code> 
    * through <code>count</code>. If it is less 
    * than <code>count</code>, then  <code>buf[pos]</code> 
    * is the next byte to be supplied as input; 
    * if it is equal to <code>count</code>, then 
    * the  next <code>read</code> or <code>skip</code> 
    * operation will require more bytes to be 
    * read from the contained  input stream. 
    * 
    * @see     java.io.BufferedInputStream#buf 
    */  
   protected int pos;  
 
/* 這里省略去 N 多代碼 ------>>  */  
 
 /** 
    * See 
    * the general contract of the <code>read</code> 
    * method of <code>InputStream</code>. 
    * 
    * @return     the next byte of data, or <code>-1</code> if the end of the 
    *             stream is reached. 
    * @exception  IOException  if this input stream has been closed by 
    *              invoking its {@link #close()} method, 
    *              or an I/O error occurs.  
    * @see        java.io.FilterInputStream#in 
    */  
   public synchronized int read() throws IOException {  
   if (pos >= count) {  
       fill();  
       if (pos >= count)  
       return -1;  
   }  
   return getBufIfOpen()[pos++] & 0xff;  
   }  ```
    

  我們可以看到,BufferedInputStream 內(nèi)部維護(hù)著一個 字節(jié)數(shù)組 byte[] buf 來實現(xiàn)緩沖區(qū)的功能,我們調(diào)用的  buf_in.read() 方法在返回數(shù)據(jù)之前有做一個 if 判斷,如果 buf 數(shù)組的當(dāng)前索引不在有效的索引范圍之內(nèi),即 if 條件成立, buf 字段維護(hù)的緩沖區(qū)已經(jīng)不夠了,這時候會調(diào)用 內(nèi)部的 fill() 方法進(jìn)行填充,而fill()會預(yù)讀更多的數(shù)據(jù)到 buf 數(shù)組緩沖區(qū)中去,然后再返回當(dāng)前字節(jié)數(shù)據(jù),如果 if 條件不成立便直接從 buf緩沖區(qū)數(shù)組返回數(shù)據(jù)了。其中g(shù)etBufIfOpen()返回的就是 buf字段的引用。順便說下,源碼中的 buf 字段聲明為  protected volatile byte buf[];  主要是為了通過 volatile 關(guān)鍵字保證 buf數(shù)組在多線程并發(fā)環(huán)境中的內(nèi)存可見性.

  和 [Java ](http://lib.csdn.net/base/java)NIO 的內(nèi)存映射無關(guān)的部分說了這么多篇幅,主要是為了做個鋪墊,這樣才能建立起一個知識體系,以便更好的理解內(nèi)存映射文件的優(yōu)點。

  內(nèi)存映射文件和之前說的 標(biāo)準(zhǔn)IO操作最大的不同之處就在于它雖然最終也是要從磁盤讀取數(shù)據(jù),但是它并不需要將數(shù)據(jù)讀取到OS內(nèi)核緩沖區(qū),而是直接將進(jìn)程的用戶私有地址空間中的一 部分區(qū)域與文件對象建立起映射關(guān)系,就好像直接從內(nèi)存中讀、寫文件一樣,速度當(dāng)然快了。為了說清楚這個,我們以[Linux](http://lib.csdn.net/base/linux)操作系統(tǒng)為例子,看下圖:
  ![](http://upload-images.jianshu.io/upload_images/1986284-757609afe70530fb.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  此圖為 Linux 2.X 中的進(jìn)程虛擬存儲器,即進(jìn)程的虛擬地址空間,如果你的機子是 32 位,那么就有  2^32 = 4G的虛擬地址空間,我們可以看到圖中有一塊區(qū)域: “Memory mapped region for shared libraries” ,這段區(qū)域就是在內(nèi)存映射文件的時候?qū)⒛骋欢蔚奶摂M地址和文件對象的某一部分建立起映射關(guān)系,此時并沒有拷貝數(shù)據(jù)到內(nèi)存中去,而是當(dāng)進(jìn)程代碼第一次引用這 段代碼內(nèi)的虛擬地址時,觸發(fā)了缺頁異常,這時候OS根據(jù)映射關(guān)系直接將文件的相關(guān)部分?jǐn)?shù)據(jù)拷貝到進(jìn)程的用戶私有空間中去,當(dāng)有操作第N頁數(shù)據(jù)的時候重復(fù)這 樣的OS頁面調(diào)度程序操作。注意啦,原來內(nèi)存映射文件的效率比標(biāo)準(zhǔn)IO高的重要原因就是因為少了把數(shù)據(jù)拷貝到OS內(nèi)核緩沖區(qū)這一步(可能還少了 native堆中轉(zhuǎn)這一步)。

  java中提供了3種內(nèi)存映射模式,即:只讀(readonly)、讀寫(read_write)、專用(private) ,對于  只讀模式來說,如果程序試圖進(jìn)行寫操作,則會拋出ReadOnlyBufferException異 常;第二種的讀寫模式表明了通過內(nèi)存映射文件的方式寫或修改文件內(nèi)容的話是會立刻反映到磁盤文件中去的,別的進(jìn)程如果共享了同一個映射文件,那么也會立即 看到變化!而不是像標(biāo)準(zhǔn)IO那樣每個進(jìn)程有各自的內(nèi)核緩沖區(qū),比如JAVA代碼中,沒有執(zhí)行 IO輸出流的 flush() 或者  close() 操作,那么對文件的修改不會更新到磁盤去,除非進(jìn)程運行結(jié)束;最后一種專用模式采用的是OS的“寫時拷貝”原則,即在沒有發(fā)生寫操作的情況下,多個進(jìn)程之 間都是共享文件的同一塊物理內(nèi)存(進(jìn)程各自的虛擬地址指向同一片物理地址),一旦某個進(jìn)程進(jìn)行寫操作,那么將會把受影響的文件數(shù)據(jù)單獨拷貝一份到進(jìn)程的私 有緩沖區(qū)中,不會反映到物理文件中去。

  在JAVA NIO中可以很容易的創(chuàng)建一塊內(nèi)存映射區(qū)域,代碼如下:  

File file = new File("E:\download\office2007pro.chs.ISO");
FileInputStream in = new FileInputStream(file);
FileChannel channel = in.getChannel();
MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 0,channel.size());


  這里創(chuàng)建了一個只讀模式的內(nèi)存映射文件區(qū)域,接下來我就來[測試](http://lib.csdn.net/base/softwaretest)下與普通NIO中的通道操作相比性能上的優(yōu)勢,先看如下代碼:
   

public class IOTest {
static final int BUFFER_SIZE = 1024;

public static void main(String[] args) throws Exception {  

    File file = new File("F:\\aa.pdf");  
    FileInputStream in = new FileInputStream(file);  
    FileChannel channel = in.getChannel();  
    MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 0,  
            channel.size());  

    byte[] b = new byte[1024];  
    int len = (int) file.length();  

    long begin = System.currentTimeMillis();  

    for (int offset = 0; offset < len; offset += 1024) {  

        if (len - offset > BUFFER_SIZE) {  
            buff.get(b);  
        } else {  
            buff.get(new byte[len - offset]);  
        }  
    }  

    long end = System.currentTimeMillis();  
    System.out.println("time is:" + (end - begin));  

}  

} ```

輸出為 63,即通過內(nèi)存映射文件的方式讀取 86M多的文件只需要78毫秒,我現(xiàn)在改為普通NIO的通道操作看下:

File file = new File("F:\\liq.pdf");  
FileInputStream in = new FileInputStream(file);  
FileChannel channel = in.getChannel();  
ByteBuffer buff = ByteBuffer.allocate(1024);   
  
long begin = System.currentTimeMillis();  
while (channel.read(buff) != -1) {  
    buff.flip();  
    buff.clear();  
}  
long end = System.currentTimeMillis();  
System.out.println("time is:" + (end - begin));  ```

輸出為 468毫秒,幾乎是 6 倍的差距,文件越大,差距便越大。所以內(nèi)存映射文件特別適合于對大文件的操作,JAVA中的限制是最大不得超過 Integer.MAX_VALUE,即2G左右,不過我們可以通過分次映射文件(channel.map)的不同部分來達(dá)到操作整個文件的目的。

按照jdk文檔的官方說法,內(nèi)存映射文件屬于JVM中的直接緩沖區(qū),還可以通過 ByteBuffer.allocateDirect() ,即DirectMemory的方式來創(chuàng)建直接緩沖區(qū)。他們相比基礎(chǔ)的 IO操作來說就是少了中間緩沖區(qū)的數(shù)據(jù)拷貝開銷。同時他們屬于JVM堆外內(nèi)存,不受JVM堆內(nèi)存大小的限制。
 
其中 DirectMemory 默認(rèn)的大小是等同于JVM最大堆,理論上說受限于 進(jìn)程的虛擬地址空間大小,比如 32位的windows上,每個進(jìn)程有4G的虛擬空間除去 2G為OS內(nèi)核保留外,再減去 JVM堆的最大值,剩余的才是DirectMemory大小。通過 設(shè)置 JVM參數(shù) -Xmx64M,即JVM最大堆為64M,然后執(zhí)行以下程序可以證明DirectMemory不受JVM堆大小控制:

public static void main(String[] args) {
ByteBuffer.allocateDirect(10241024100); // 100MB
} ```

我們設(shè)置了JVM堆 64M限制,然后在 直接內(nèi)存上分配了 100MB空間,程序執(zhí)行后直接報錯:Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory。接著我設(shè)置 -Xmx200M,程序正常結(jié)束。然后我修改配置: -Xmx64M -XX:MaxDirectMemorySize=200M,程序正常結(jié)束。因此得出結(jié)論: 直接內(nèi)存DirectMemory的大小默認(rèn)為 -Xmx 的JVM堆的最大值,但是并不受其限制,而是由JVM參數(shù) MaxDirectMemorySize單獨控制。接下來我們來證明直接內(nèi)存不是分配在JVM堆中。我們先執(zhí)行以下程序,并設(shè)置 JVM參數(shù) -XX:+PrintGC,

public static void main(String[] args) {         
 for(int i=0;i<20000;i++) {  
           ByteBuffer.allocateDirect(1024*100);  //100K  
      }  
  }  ```

  輸出結(jié)果如下:
[GC 1371K->1328K(61312K), 0.0070033 secs]    
[Full GC 1328K->1297K(61312K), 0.0329592 secs]    
[GC 3029K->2481K(61312K), 0.0037401 secs]     
[Full GC 2481K->2435K(61312K), 0.0102255 secs]

   我們看到這里執(zhí)行 GC的次數(shù)較少,但是觸發(fā)了 兩次 Full GC,原因在于直接內(nèi)存不受 GC(新生代的Minor GC)影響,只有當(dāng)執(zhí)行老年代的 Full GC時候才會順便回收直接內(nèi)存!而直接內(nèi)存是通過存儲在JVM堆中的DirectByteBuffer對象來引用的,所以當(dāng)眾多的 DirectByteBuffer對象從新生代被送入老年代后才觸發(fā)了 full gc。
 
  再看直接在JVM堆上分配內(nèi)存區(qū)域的情況:

public static void main(String[] args) {
r(int i=0;i<10000;i++) {
ByteBuffer.allocate(1024*100); //100K
} ```

ByteBuffer.allocate 意味著直接在 JVM堆上分配內(nèi)存,所以受 新生代的 Minor GC影響,輸出如下:

[GC 16023K->224K(61312K), 0.0012432 secs]
[GC 16211K->192K(77376K), 0.0006917 secs]
[GC 32242K->176K(77376K), 0.0010613 secs]
[GC 32225K->224K(109504K), 0.0005539 secs]
[GC 64423K->192K(109504K), 0.0006151 secs]
[GC 64376K->192K(171392K), 0.0004968 secs]
[GC 128646K->204K(171392K), 0.0007423 secs]
[GC 128646K->204K(299968K), 0.0002067 secs]
[GC 257190K->204K(299968K), 0.0003862 secs]
[GC 257193K->204K(287680K), 0.0001718 secs]
[GC 245103K->204K(276480K), 0.0001994 secs]
[GC 233662K->204K(265344K), 0.0001828 secs]
[GC 222782K->172K(255232K), 0.0001998 secs]
[GC 212374K->172K(245120K), 0.0002217 secs]

可以看到,由于直接在 JVM堆上分配內(nèi)存,所以觸發(fā)了多次GC,且不會觸及 Full GC,因為對象根本沒機會進(jìn)入老年代。

我想提個疑問,NIO中的DirectMemory和內(nèi)存文件映射同屬于直接緩沖區(qū),但是前者和 -Xmx和-XX:MaxDirectMemorySize有關(guān),而后者完全沒有JVM參數(shù)可以影響和控制,這讓我不禁懷疑兩者的直接緩沖區(qū)是否相同,前 者指的是 JAVA進(jìn)程中的 native堆,即涉及底層平臺如 win32的dll 部分,因為 C語言中的 malloc()分配的內(nèi)存就屬于 native堆,不屬于 JVM堆,這也是DirectMemory能在一些場景中顯著提高性能的原因,因為它避免了在 native堆和jvm堆之間數(shù)據(jù)的來回復(fù)制;而后者則是沒有經(jīng)過 native堆,是由 JAVA進(jìn)程直接建立起 某一段虛擬地址空間和文件對象的關(guān)聯(lián)映射關(guān)系,參見 Linux虛擬存儲器圖中的 “Memory mapped region for shared libraries” 區(qū)域,所以內(nèi)存映射文件的區(qū)域并不在JVM GC的回收范圍內(nèi),因為它本身就不屬于堆區(qū),卸載這部分區(qū)域只能通過系統(tǒng)調(diào)用 unmap()來實現(xiàn) (Linux)中,而 JAVA API 只提供了 FileChannel.map 的形式創(chuàng)建內(nèi)存映射區(qū)域,卻沒有提供對應(yīng)的 unmap(),讓人十分費解,導(dǎo)致要卸載這部分區(qū)域比較麻煩。

最后再試試通過 DirectMemory來操作前面 內(nèi)存映射和基本通道操作的例子,來看看直接內(nèi)存操作的話,程序的性能如何:

File file = new File("F:\\liq.pdf");  
FileInputStream in = new FileInputStream(file);  
FileChannel channel = in.getChannel();  
ByteBuffer buff = ByteBuffer.allocateDirect(1024);   
  
long begin = System.currentTimeMillis();  
while (channel.read(buff) != -1) {  
    buff.flip();  
    buff.clear();  
}  
long end = System.currentTimeMillis();  
System.out.println("time is:" + (end - begin));  

程序輸出為 312毫秒,看來比普通的NIO通道操作(468毫秒)來的快,但是比 mmap 內(nèi)存映射的 63秒差距太多了,我想應(yīng)該不至于吧,通過修改;ByteBuffer buff = ByteBuffer.allocateDirect(1024); 為 ByteBuffer buff = ByteBuffer.allocateDirect((int)file.length()),即一次性分配整個文件長度大小的堆外內(nèi)存,最終輸出為 78毫秒,由此可以得出兩個結(jié)論:1.堆外內(nèi)存的分配耗時比較大. 2.還是比mmap內(nèi)存映射來得慢,都不要說通過mmap讀取數(shù)據(jù)的時候還涉及缺頁異常、頁面調(diào)度的系統(tǒng)調(diào)用了,看來內(nèi)存映射文件確實NB啊,這還只是 86M的文件,如果上 G 的大小呢?
最后一點為 DirectMemory的內(nèi)存只有在 JVM執(zhí)行 full gc 的時候才會被回收,那么如果在其上分配過大的內(nèi)存空間,那么也將出現(xiàn) OutofMemoryError,即便 JVM 堆中的很多內(nèi)存處于空閑狀態(tài)。

本來只想寫點內(nèi)存映射部分,但是寫著寫著涉及進(jìn)來的知識多了點,邊界不好把控啊。。。

我想補充下額外的一個知識點,關(guān)于 JVM堆大小的設(shè)置是不受限于物理內(nèi)存,而是受限于虛擬內(nèi)存空間大小,理論上來說是進(jìn)程的虛擬地址空間大小,但是實際上我們的虛擬內(nèi)存空間是有限制的,一 般windows上默認(rèn)在C盤,大小為物理內(nèi)存的2倍左右。我做了個實驗:我機子是 64位的win7,那么理論上說進(jìn)程虛擬空間是幾乎無限大,物理內(nèi)存為4G,而我設(shè)置 -Xms5000M, 即在啟動JAVA程序的時候一次性申請到超過物理內(nèi)存大小的5000M內(nèi)存,程序正常啟動,而當(dāng)我加到 -Xms8000M的時候就報OOM錯誤了,然后我修改增加 win7的虛擬內(nèi)存,程序又正常啟動了,說明 -Xms 受限于虛擬內(nèi)存的大小。我設(shè)置-Xms5000M,即超過了4G物理內(nèi)存,并在一個死循環(huán)中不斷創(chuàng)建對象,并保證不會被GC回收。程序運行一會后整個電腦 幾乎死機狀態(tài),即卡住了,反映很慢很慢,推測是發(fā)生了系統(tǒng)顛簸,即頻繁的頁面調(diào)度置換導(dǎo)致,說明 -Xms -Xmx不是局限于物理內(nèi)存的大小,而是綜合虛擬內(nèi)存了,JVM會根據(jù)電腦虛擬內(nèi)存的設(shè)置來控制。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,627評論 2 380

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