Reference counted Objects (引用计数对象) - 文章翻译

原文地址:http://netty.io/wiki/reference-counted-objects.html

从Netty4开始,某些对象的饿生命周期由其引用计数来管理,因此,一旦不再使用,Netty就可以将它们(或其共享资源)返回给对象池(或对象分配器)。垃圾收集和引用队列并没有提供不可达的高效实时保证,而引用计数则提供了一种可替代的机制,代价是有轻微的不方便。

ByteBuf是最值得注意的一种,它利用了引用计数来提高分配和回收的性能,本节将解释哈在Netty中使用ByteBuf的引用计数。

-引用计数的基础
新引用计数对象的引用计数为1:
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

当你释放引用计数对象时,它的引用计数减少1.如果引用计数达到0,则引用计数对象将被重新分配或者将其返回它来自的对象池。
assert buf.refCnt() == 1;
// release() returns true only if the reference count becomes 0.
boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;

--Dangling引用
尝试访问引用计数为0的引用计数对象将触发IllegalReferenceCountException.
assert buf.refCnt() == 0;
try {
buf.writeLong(0xdeadbeef);
throw new Error("should not reach here");
} catch (IllegalReferenceCountExeception e) {
// Expected
}

--增加引用计数
引用计数也可以通过retain()操作来增加在其尚未被销毁之前。
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

buf.retain();
assert buf.refCnt() == 2;

boolean destroyed = buf.release();
assert !destroyed;
assert buf.refCnt() == 1;

--谁销毁它?
一般的经验规则是,最后访问引用计数的对象负责对引用计数对象的销毁。更具体的来说:
如果【发送】组件将引用计数对象传递给另一个【接收】组件,则发送组件通常不需要销毁它,而是将其推迟到接收组建中再决定。
如果一个组件使用了引用计数对象,并且知道其他任何内容都无法访问这个对象(即不传递给另一个组件),则此组件应该销毁它。

这里有一个简单的实例:
public ByteBuf a(ByteBuf input) {
input.writeByte(42);
return input;
}

public ByteBuf b(ByteBuf input) {
try {
output = input.alloc().directBuffer(input.readableBytes() + 1);
output.writeBytes(input);
output.writeByte(42);
return output;
} finally {
input.release();
}
}

public void c(ByteBuf input) {
System.out.println(input);
input.release();
}

public void main() {
...
ByteBuf buf = ...;
// This will print buf to System.out and destroy it.
c(b(a(buf)));
assert buf.refCnt() == 0;
}

--派生的缓冲区
ByteBuf.duplicate(), ByteBuf.slice() and ByteBuf.order(ByteOrder) 创建一个派生的缓冲区,它共享父缓冲区的内存区域。派生的缓冲区不具有自己的引用计数,但共享父缓冲区的引用计数。
ByteBuf parent = ctx.alloc().directBuffer();
ByteBuf derived = parent.duplicate();

// Creating a derived buffer does not increase the reference count.
assert parent.refCnt() == 1;
assert derived.refCnt() == 1;

相比之下,ByteBuf.copy() and ByteBuf.readBytes(int)不是派生的缓冲区。所分配的ByteBuf需要被释放。
请注意,父缓冲区及其派生的缓冲区共享相同的引用计数,并且在创建派生缓冲区时,引用计数不会增加。因此,如果要将派生缓冲区传递给应用程序的其他组件时,则必须先调用retain()。
ByteBuf parent = ctx.alloc().directBuffer(512);
parent.writeBytes(...);

try {
while (parent.isReadable(16)) {
ByteBuf derived = parent.readSlice(16);
derived.retain();
process(derived);
}
} finally {
parent.release();
}
...

public void process(ByteBuf buf) {
...
buf.release();
}

--ByteBufHolder接口:
有时,ByteBuf被包含在一个缓冲区中,例如DatagramPacket、HttpContent和WebSocketframe。这些类型继承了一个名为ByteBufHolder的公共接口。
一个buffer holder共享它所包含的缓冲区的引用计数,就像派生的缓冲区一样。

--CHannelHandler中的引用计数
-进站消息
当一个事件循环将数据读入ByteBuf并触发一个ChannelRead()事件时,相应pipline中的ChannelHandle负责释放buffer。因此,处理接收到的数据的handler应该在它的channelRead()中调用buffer的release()。
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
...
} finally {
buf.release();
}
}
正如本文档中“谁来销毁”一节所描述的,如果处理器将缓冲区(或者任何引用计数对象)传递给下一个处理程序,则不需要释放它。

public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
...
ctx.fireChannelRead(buf);
}
请注意,ByteBuf不是Netty中唯一的引用计数类型。如果你正在处理由解码器生成的消息,则很可能该消息也是引用计数的。
// Assuming your handler is placed next to HttpRequestDecoder
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpRequest) {
HttpRequest req = (HttpRequest) msg;
...
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
try {
...
} finally {
content.release();
}
}
}

如果你有疑问,或者你想要简化消息的释放,可以使用ReferenceCountUtil.release()。
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
...
} finally {
ReferenceCountUtil.release(msg);
}
}
或者,你可以考虑继承SimpleChannelHandler,它为你收到的所有消息调用ReferenceCountUtil.release(msg)。

--出站消息
与进站消息不同的是,出站消息是由你的应用程序创建的,将这些消息释放在将其写入到线路后是Netty的责任。但是,拦截你的写入请求的处理器应确保正确释放任何中间对象。(比如编码器)

// Simple-pass through
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
System.err.println("Writing: " + message);
ctx.write(message, promise);
}

// Transformation
public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) {
if (message instanceof HttpContent) {
// Transform HttpContent to ByteBuf.
HttpContent content = (HttpContent) message;
try {
ByteBuf transformed = ctx.alloc().buffer();
....
ctx.write(transformed, promise);
} finally {
content.release();
}
} else {
// Pass non-HttpContent through.
ctx.write(message, promise);
}
}

-缓冲区泄漏问题解决
引用计数的缺点是容易泄漏引用计数的对象。由于JVM不知道引用计数的Netty实现,因此,即使它们的引用计数不为0,它也会在它们变得不可访问时自动对其进行GC。一旦回收的垃圾无法恢复,也就无法返回它所来自的池,从而产生内存泄漏。
幸运的是,尽管很难找到内存泄漏,但Netty将在默认的情况下抽取大约1%的缓冲区来检查应用中是否存在内存泄漏。如果发生泄漏,你可以找到如下日志信息:
LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

使用上面提到的JVM选项重新启动你的应用,然后你将看到你的应用可以访问泄漏的缓冲区的最近位置。下面的例子来自单元测试显示了一个泄漏:
Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 1

1:

io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
...

Created at:
io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155)
io.netty.buffer.UnpooledUnsafeDirectByteBuf.copy(UnpooledUnsafeDirectByteBuf.java:465)
io.netty.buffer.WrappedByteBuf.copy(WrappedByteBuf.java:697)
io.netty.buffer.AdvancedLeakAwareByteBuf.copy(AdvancedLeakAwareByteBuf.java:656)
io.netty.handler.codec.xml.XmlFrameDecoder.extractFrame(XmlFrameDecoder.java:198)
io.netty.handler.codec.xml.XmlFrameDecoder.decode(XmlFrameDecoder.java:174)
io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:227)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:140)
io.netty.channel.ChannelHandlerInvokerUtil.invokeChannelReadNow(ChannelHandlerInvokerUtil.java:74)
io.netty.channel.embedded.EmbeddedEventLoop.invokeChannelRead(EmbeddedEventLoop.java:142)
io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:317)
io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846)
io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:176)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:147)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133)
...

-泄漏检测级别
目前有4种泄漏检测:
DISABLED - 禁用泄漏检测。不推荐
SIMPLE - 取样的1%是否发生了泄漏。默认情况
ADVANCED - 取样的1%发生泄漏的地方
PARANOID - 与ADVANCED类似,但是检查所有的缓冲区,而不只是取样的1%。此选项在自动测试的阶段很有用。如果构建输出包含了LEAK,可以认为构建失败。

你可以使用JVM的Y JVM option -Dio.netty.leakDetection.level来制定泄漏检测级别。
java -Dio.netty.leakDetectionLevel=advanced ...

-避免泄漏的最佳实践
在PARANOID和SIMPLE泄漏检测级别运行你的单元测试和集成测试。
在一个足够长的时间内,使用SIMPLE级别推出到整个级别的应用,看是否有泄漏
如果有泄漏,再使用ADVANCED级别来cannary以获得一些关于泄漏的提示。
不要部署存在泄漏的程序到整个集群。

--在单元测试中修复泄漏
在单元测试中,很容易忘记释放缓冲区或者消息。它将生成泄漏警告,但并不意味着你的程序有泄漏。你可以使用ReferenceCountUtil. releaseLater ()方法,而不是使用try-catch块来包装单元测试以释放所有的缓冲区。
import static io.netty.util.ReferenceCountUtil.*;

@Test
public void testSomething() throws Exception {
// ReferenceCountUtil.releaseLater() will keep the reference of buf,
// and then release it when the test thread is terminated.
ByteBuf buf = releaseLater(Unpooled.directBuffer(512));
...
}

原文地址:

http://netty.io/wiki/reference-counted-objects.html

从Netty4开始,某些对象的饿生命周期由其引用计数来管理,因此,一旦不再使用,Netty就可以将它们(或其共享资源)返回给对象池(或对象分配器)。垃圾收集和引用队列并没有提供不可达的高效实时保证,而引用计数则提供了一种可替代的机制,代价是有轻微的不方便。

ByteBuf是最值得注意的一种,它利用了引用计数来提高分配和回收的性能,本节将解释哈在Netty中使用ByteBuf的引用计数。

-引用计数的基础

新引用计数对象的引用计数为1:

ByteBuf buf = ctx.alloc().directBuffer(); assert buf.refCnt() == 1;

当你释放引用计数对象时,它的引用计数减少1.如果引用计数达到0,则引用计数对象将被重新分配或者将其返回它来自的对象池。

assert buf.refCnt() == 1; // release() returns true only if the reference count becomes 0. boolean destroyed = buf.release(); assert destroyed; assert buf.refCnt() == 0;

--

Dangling

引用

尝试访问引用计数为0的引用计数对象将触发IllegalReferenceCountException.

assert buf.refCnt() == 0; try { buf.writeLong(0xdeadbeef); throw new Error("should not reach here"); } catch (IllegalReferenceCountExeception e) { // Expected }

--增加引用计数

引用计数也可以通过retain()操作来增加在其尚未被销毁之前。

ByteBuf buf = ctx.alloc().directBuffer(); assert buf.refCnt() == 1; buf.retain(); assert buf.refCnt() == 2; boolean destroyed = buf.release(); assert !destroyed; assert buf.refCnt() == 1;

--谁销毁它?

一般的经验规则是,最后访问引用计数的对象负责对引用计数对象的销毁。更具体的来说:

如果【发送】组件将引用计数对象传递给另一个【接收】组件,则发送组件通常不需要销毁它,而是将其推迟到接收组建中再决定。

如果一个组件使用了引用计数对象,并且知道其他任何内容都无法访问这个对象(即不传递给另一个组件),则此组件应该销毁它。

这里有一个简单的实例:

public ByteBuf a(ByteBuf input) { input.writeByte(42); return input; } public ByteBuf b(ByteBuf input) { try { output = input.alloc().directBuffer(input.readableBytes() + 1); output.writeBytes(input); output.writeByte(42); return output; } finally { input.release(); } } public void c(ByteBuf input) { System.out.println(input); input.release(); } public void main() { ... ByteBuf buf = ...; // This will print buf to System.out and destroy it. c(b(a(buf))); assert buf.refCnt() == 0; }

[图片上传失败...(image-8c3af0-1512467876661)]

--派生的缓冲区

ByteBuf.duplicate(), ByteBuf.slice() and ByteBuf.order(ByteOrder) 创建一个派生的缓冲区,它共享父缓冲区的内存区域。派生的缓冲区不具有自己的引用计数,但共享父缓冲区的引用计数。

ByteBuf parent = ctx.alloc().directBuffer(); ByteBuf derived = parent.duplicate(); // Creating a derived buffer does not increase the reference count. assert parent.refCnt() == 1; assert derived.refCnt() == 1;

相比之下,ByteBuf.copy() and ByteBuf.readBytes(int)不是派生的缓冲区。所分配的ByteBuf需要被释放。

请注意,父缓冲区及其派生的缓冲区共享相同的引用计数,并且在创建派生缓冲区时,引用计数不会增加。因此,如果要将派生缓冲区传递给应用程序的其他组件时,则必须先调用retain()。

ByteBuf parent = ctx.alloc().directBuffer(512); parent.writeBytes(...); try { while (parent.isReadable(16)) { ByteBuf derived = parent.readSlice(16); derived.retain(); process(derived); } } finally { parent.release(); } ... public void process(ByteBuf buf) { ... buf.release(); }

--ByteBufHolder接口:

有时,ByteBuf被包含在一个缓冲区中,例如DatagramPacket、HttpContent和WebSocketframe。这些类型继承了一个名为ByteBufHolder的公共接口。

一个buffer holder共享它所包含的缓冲区的引用计数,就像派生的缓冲区一样。

--CHannelHandler中的引用计数

-进站消息

当一个事件循环将数据读入ByteBuf并触发一个ChannelRead()事件时,相应pipline中的ChannelHandle负责释放buffer。因此,处理接收到的数据的handler应该在它的channelRead()中调用buffer的release()。

public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; try { ... } finally { buf.release(); } }

正如本文档中“谁来销毁”一节所描述的,如果处理器将缓冲区(或者任何引用计数对象)传递给下一个处理程序,则不需要释放它。

public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; ... ctx.fireChannelRead(buf); }

请注意,ByteBuf不是Netty中唯一的引用计数类型。如果你正在处理由解码器生成的消息,则很可能该消息也是引用计数的。

// Assuming your handler is placed next to HttpRequestDecoder public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; ... } if (msg instanceof HttpContent) { HttpContent content = (HttpContent) msg; try { ... } finally { content.release(); } } }

如果你有疑问,或者你想要简化消息的释放,可以使用ReferenceCountUtil.release()。

public void channelRead(ChannelHandlerContext ctx, Object msg) { try { ... } finally { ReferenceCountUtil.release(msg); } }

或者,你可以考虑继承SimpleChannelHandler,它为你收到的所有消息调用ReferenceCountUtil.release(msg)。

--出站消息

与进站消息不同的是,出站消息是由你的应用程序创建的,将这些消息释放在将其写入到线路后是Netty的责任。但是,拦截你的写入请求的处理器应确保正确释放任何中间对象。(比如编码器)

// Simple-pass through public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) { System.err.println("Writing: " + message); ctx.write(message, promise); } // Transformation public void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise) { if (message instanceof HttpContent) { // Transform HttpContent to ByteBuf. HttpContent content = (HttpContent) message; try { ByteBuf transformed = ctx.alloc().buffer(); .... ctx.write(transformed, promise); } finally { content.release(); } } else { // Pass non-HttpContent through. ctx.write(message, promise); } }

-缓冲区泄漏问题解决

引用计数的缺点是容易泄漏引用计数的对象。由于JVM不知道引用计数的Netty实现,因此,即使它们的引用计数不为0,它也会在它们变得不可访问时自动对其进行GC。一旦回收的垃圾无法恢复,也就无法返回它所来自的池,从而产生内存泄漏。

幸运的是,尽管很难找到内存泄漏,但Netty将在默认的情况下抽取大约1%的缓冲区来检查应用中是否存在内存泄漏。如果发生泄漏,你可以找到如下日志信息:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

使用上面提到的JVM选项重新启动你的应用,然后你将看到你的应用可以访问泄漏的缓冲区的最近位置。下面的例子来自单元测试显示了一个泄漏:

Running io.netty.handler.codec.xml.XmlFrameDecoderTest 15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected. Recent access records: 1 #1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString(AdvancedLeakAwareByteBuf.java:697) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:157) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133) ... Created at: io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:55) io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:155) io.netty.buffer.UnpooledUnsafeDirectByteBuf.copy(UnpooledUnsafeDirectByteBuf.java:465) io.netty.buffer.WrappedByteBuf.copy(WrappedByteBuf.java:697) io.netty.buffer.AdvancedLeakAwareByteBuf.copy(AdvancedLeakAwareByteBuf.java:656) io.netty.handler.codec.xml.XmlFrameDecoder.extractFrame(XmlFrameDecoder.java:198) io.netty.handler.codec.xml.XmlFrameDecoder.decode(XmlFrameDecoder.java:174) io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:227) io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:140) io.netty.channel.ChannelHandlerInvokerUtil.invokeChannelReadNow(ChannelHandlerInvokerUtil.java:74) io.netty.channel.embedded.EmbeddedEventLoop.invokeChannelRead(EmbeddedEventLoop.java:142) io.netty.channel.DefaultChannelHandlerContext.fireChannelRead(DefaultChannelHandlerContext.java:317) io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:846) io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:176) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(XmlFrameDecoderTest.java:147) io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(XmlFrameDecoderTest.java:133) ...

-泄漏检测级别

目前有4种泄漏检测:

DISABLED - 禁用泄漏检测。不推荐

SIMPLE - 取样的1%是否发生了泄漏。默认情况

ADVANCED - 取样的1%发生泄漏的地方

PARANOID - 与ADVANCED类似,但是检查所有的缓冲区,而不只是取样的1%。此选项在自动测试的阶段很有用。如果构建输出包含了LEAK,可以认为构建失败。

你可以使用JVM的Y JVM option

-Dio.netty.leakDetection.level来制定泄漏检测级别。

java -Dio.netty.leakDetectionLevel=advanced ...

-避免泄漏的最佳实践

在PARANOID和SIMPLE泄漏检测级别运行你的单元测试和集成测试。

在一个足够长的时间内,使用SIMPLE级别推出到整个级别的应用,看是否有泄漏

如果有泄漏,再使用ADVANCED级别来cannary以获得一些关于泄漏的提示。

不要部署存在泄漏的程序到整个集群。

--在单元测试中修复泄漏

在单元测试中,很容易忘记释放缓冲区或者消息。它将生成泄漏警告,但并不意味着你的程序有泄漏。你可以使用ReferenceCountUtil. releaseLater ()方法,而不是使用try-catch块来包装单元测试以释放所有的缓冲区。

import static io.netty.util.ReferenceCountUtil.*; @Test public void testSomething() throws Exception { // ReferenceCountUtil.releaseLater() will keep the reference of buf, // and then release it when the test thread is terminated. ByteBuf buf = releaseLater(Unpooled.directBuffer(512)); ... }

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容