Java 引用对象

这篇文章整理自一位外国大神的英文博客,我在保存文章的结构下,增加了一些自己的见解,并做了一个文章的脑图。原文链接为 http://www.kdgregory.com/index.php?page=java.refobj

脑图如下


基础

Java堆和对象生命周期

在 Java 中,随着函数的调用,局部变量和函数会被压入栈帧,而 new 操作符生成出来的实际对象保存在堆中,当然,如果这时堆中没有合适足够的空间生成新对象,在报出 OutOfMemoryError之前,就会尝试进行一次垃圾收集来获取空间。

垃圾收集

Java 语言给我们提供了 new 操作符来在堆中分配一块内存,但是却没有给我们提供一个delete操作符来释放这些空间,如果仅仅是这样,那么我们的堆内存空间很快就会占满,程序久无法继续执行了。

幸运的是 Java 给我们提供了垃圾收集器。在我们 new 一个对象时,如果堆内存空间不足,调用 new 操作符的线程就会被挂起,等待垃圾收集器扫描一遍堆内存并释放空间,如果收集后仍然没有足够的空间,就会报出 OOM 了。

标记-清除算法

标记清除算法可以概括为:所有不可达的对象都是垃圾,并且可以被收集清除。

标记清除算法有如下步骤:

步骤一:标记

垃圾收集器从根引用开始,遍历对象关系图并把遍历过的对象标记为可达对象。

步骤二:清除

所有在步骤一中没有被标记到的对象,如果有定义 finalizer,则会被加入到 finalization queue 中执行 finalize 方法,否则会被清除。

步骤三:压缩(可选)

有些垃圾收集器会有第三个步骤:压缩整理堆内存,即把第二步执行结束后,零碎的堆内存重新对齐,整理出大片的连续堆内存空间。

比如,在1.6 和 1.7 server 模式下的 Hotspot JVM,就会将年轻代的空间压缩整理,但是不会压缩整理老年代的空间。

Finalizer

虽然 Java 替我们提供了垃圾收集机制去释放堆内存,但是内存不是我们唯一要清理的资源。比如,FileOutputStream 在不可达之后,被垃圾收集器收集之前,应该释放它关联的文件和系统的连接、把缓冲区的数据刷入文件等。为此,Java 为我们提供了 Finalizer 机制,我们只要实现 finalize() 方法即可。

虽然 Finalizer 看起来简单好用,但是我们不应该依赖它,因为如果垃圾收集一直不执行,那么它将一直不被调用。如果有过多的 Finalizer 拖住了内存空间的清理,那么也可能导致不能及时释放出足够的空间而出现 OOM。

Java 对象生命周期(没有 Reference 的情况下)

创建---->构造---->使用---->不可达---->Finalizer

引用关系

Reference 对象

三个生命周期中的新状态

从 JDK 1.2 起,Java 提供了三个新的状态在 Java 生命周期中:分别是软可达(softly-reachable)、弱可达(weakly-reachable)、虚幻可达(phantom-reachable)。这些状态都是应用在对象满足垃圾收集状态时,换句话说,就是这个对象已经没有任何强引用了。

  • 软可达: 当对象被一个 SoftReference 关联,并且没有强引用时,这个对象就进入了软可达状态。在这个状态下,垃圾收集器会尝试不去收集这个对象,直到如果不收集这个对象就会引发 OOM 才会尝试收集它。
  • 弱可达: 当对象被一个 WeakReference 关联,并且没有强或者软引用时,这个对象就进入了弱可达状态。在这个状态下,垃圾收集器可以自由收集这个对象而不受约束。不过实际上,只有在 full collect 时会收集清除弱可达对象,小收集是不会清除的。
  • 虚幻可达: 当对象被一个 PhantomReference 关联时,并且对象已经被垃圾收集器盯上且 finalize() 方法已经执行时,这个对象就进入了虚幻可达状态。换句话说,已经没有任何方式可以挽回这个对象了~

这里需要注意的两个点是:

  • 对象可以跳过其中某些生命周期。比如没有软引用,只有弱引用。那么对象可以直接从强可达进入弱可达状态。
  • 只有极少对象需要用到这些引用关系。

引用关系和被引用对象

Reference 引用关系是我们程序和具体对象之间的一个中间层,其中被引用的对象是在 Reference 构造时指定的,并且不可修改。下面是一个例子:

SoftReference<List<Foo>> ref = new SoftRerence<>(new LinkedList<Foo>);

List<Foo> list = ref.get();
if (list != null){
    list.add(foo);
}else {
    // somthing else
}

其中要注意的点是:

  1. 每次使用对象前必须确认对象是否已经被清理(null)
  2. 必须先拿到对象的强引用再使用对象。不然直接使用 ref.get().add(foo),如果这时在执行到 ref.get() 时触发了一次垃圾收集,将会报 NPE。
  3. 比如给这个 Reference 指定一个强引用。如果这个引用关系被垃圾收集清理了。那我们讲这么多都没用了……

软引用

在 JDK 文档中讲了,软引用关系适合用于内存敏感的缓存:每个被缓存的对象通过一个 SoftReference 连接,然后 JVM 会在不需要这部分被引用对象的空间时,不去清理它,在内存空间不足时再清理。也因此,对于正在使用的缓存对象,我们应该加上一个强引用指向被引用对象,防止它被清理。当然,如果要使用的对象已经被清理了,我们就刷新一下缓存再加它进去即可。

需要注意的是,不建议缓存很小的对象,应该缓存大文件、大对象、层层嵌套的对象图的根对象之类的。因为,如果缓存小文件,那么需要清理很多很多对象才能释放出看起来有起色的内存空间,并且这个引用关系也会占用很多空间。

使用软引用来触发循环的终止

这时软引用的一种典型的用途,可以在循环继续运行时会触发 OOM 的情况下,终止循环,避免 OOM 的出现。

来看下面一段代码:

public List<Object> getBigObjectListByIdList(List<String> ids){
    List<Object> list = new LinkedList<>();
    for (String id : ids){
        list.add(getBigObjectFromDisk(id));
    }
    return list;
}

显然如果这时内存空间不足,经过垃圾收集后仍然不够的话。程序将会发出 OOM 然后崩溃。如果这时我们给 list 对象套上一层软引用,并判断 list 对象的状态是否为 null 来决定是否终止循环。那么当内存不足时,list 将会清理,循环将终止,OOM 就可以被避免,程序的鲁棒性就能得到增强。当然,依旧提供代码示例:

public List<Object> getBigObjectListByIdList(List<String> ids){
    SoftReference<List<Object>> ref =  new SoftReference<>(new LinkedList<>());
    for (String id : ids){
        List<Object> list = ref.get();
        if (list == null) 
            return null;
        else
            list.add(getBigObjectFromDisk(id));
        list = null;
    }
    return list;
}

需要注意的是,我在循环末尾把 list 显式声明为 null,因为这里避免了一种特殊情况,虽然我们在循环结束时失去 list 这个对象,但是垃圾收集器可能还没发现它已经是不可达状态,因为 list 的引用还存在 JVM 的栈中,是处于一种不明显、不易被察觉的强引用状态。

软引用不是银弹

虽然软引用可以帮我们避免很多内存溢出的情况,但是却不能避免所有情况。问题在于:当我们实际使用一个软引用来连接对象时,比如上面的 getBigObjectListByIdList(List<String> ids) 函数,当我们要添加一行新数据到结果里,我们必须先拿到被引用对象 list 的强引用。在我们拿到 list 强引用的这段时间,我们就处在 OOM 的风险中。

这样看来,使用软引用作为循环的终止,只是最小化了我们触发 OOM 的风险,并没有完全解决了 OOM 的问题。

弱引用

弱引用,如同它的名字一样,在 gc 时它不会做任何反抗,只要被引用对象没有存在强引用关系,即使保留了弱引用关系,仍会被清理。

弱引用关系,存在肯定不会一无是处啦。它也有适合的应用场景:

  • 连接那些没有天生存在关联的对象
  • 通过一个调度 map,来减少重复数据。(缓存)

连接那些没有天生存在关联的对象

比如 ObjectOutputStream 使用了一个 WeakClassKey 来保存最近输出的对象的 ObjectStreamClass。避免反复对同一个Class创建ObjectStreamClass对象。

从被序列化的对象的角度来看,它跟 ObjectOutputStream 没有天生的关联,从 ObjectOutputStream 的角度来看,它跟被序列化的对象的 ObjectStreamClass 只是存在使用时要用到的关系,也不是天然有关联的。

假设我们写了一个程序,这个程序直接强引用 ObjectStreamClass 作为 socket 中发送消息的协议,那么这里就存在一个问题:每个消息一瞬间就发送完了,但是消息对象的 ObjectStreamClass 仍然存在内存中一直占有这部分资源,那么这部分内存就废了,慢慢程序的内存也会被耗尽。(除非我们显式释放掉这部分内存)

这样看来,弱引用提供了这样一种方式去维持对象的引用关系:当对象正在使用被引用对象时,就显式持有一个被引用对象的强引用,当使用完被引用对象后,就释放掉强引用关系,只留下弱引用关系。这个弱引用关系会维持住跟被引用对象的连接,以期待下次程序再次调用到被引用对象时,将其取出,或者直到被引用对象被垃圾收集器清理。

通过一个调度 map 来减少重复数据

这个功能跟 String.intern() 极其相似,假设我们手动实现一个 String.intern() 方法,就可以通过一个 WeakHashMap 和 WeakReference 配合实现:

private Map<String,WeakReference<String>> _map
    = new WeakHashMap<String,WeakReference<String>>();

public synchronized String intern(String str)
{
    WeakReference<String> ref = _map.get(str);
    String s2 = (ref != null) ? ref.get() : null;
    if (s2 != null)
        return s2;

    _map.put(str, new WeakReference(str));
    return str;
}

当存在大量的相同的 String 对象时,这个做法就可以节省大量的内存,使它们都引用到同一个 String 对象的地址;当一个 String 不再被使用时,就可以被垃圾收集器自由清理掉,不再占用空间。推广到其他对象,也可以用这种方法来减少重复对象。这其实也是一种缓存。

引用队列 Reference Quences

当我们在创建一个引用关系时,把这个引用关系关联到一个队列,并且这个引用在对象被清理时被入队。当我们们要寻找哪个对象被清理掉时,就来队列中寻找。那这就是引用队列的作用了。

下面提供一个使用 Reference Queue 的例子

public static void main(String[] argv) throws Exception
{
    Set<WeakReference<byte[]>> refs = new HashSet<WeakReference<byte[]>>();
    ReferenceQueue<byte[]> queue = new ReferenceQueue<byte[]>();
    
    for (int ii = 0 ; ii < 1000 ; ii++)
    {
        WeakReference<byte[]> ref = new WeakReference<byte[]>(new byte[1000000], queue);
        System.err.println(ii + ": created " + ref);
        refs.add(ref);
        
        Reference<? extends byte[]> r2;
        while ((r2 = queue.poll()) != null)
        {
            System.err.println("cleared " + r2);
            refs.remove(r2);
        }
    }
}

通过这个例子我们可以看出引用队列的两个要点:

  1. 一旦入队了,那么这个对象就已经被清理了,回不来了。
  2. 引用关系知道引用队列的存在,引用队列不知道引用关系的存在。所以我们必须持有一个引用关系的强引用。同时我们又要在做完我们对入队对象的操作后,清理掉这个引用关系的强引用,否则,这就会触发内存泄漏了。

虚幻引用

虚幻引用不同于软引用和弱引用的是,不能通过虚幻引用关系获得被引用对象(它的 get() 方法始终返回 null)。所以,虚幻引用唯一的作用应该就是告诉程序被引用对象被垃圾回收了(通过 ReferenceQueue)。

虽然虚幻引用表面上看起来没什么用,但是它可以在资源回收方面做得比 Finalizer 好一些。(但并没有完全解决 Finalizer 的问题)

Finalizer 存在的问题

  • finalize() 方法可能会一直没有被调用

如果我们一直没有耗尽可用的内存,那么垃圾回收可能会一直不被执行,finalize() 也就会一直不被调用。

虽然有办法在程序退出之前通知 JVM 调用 Finalizer,但这个方法不太可靠,而且还可能和其他 JVM 退出时的 hook 冲突。

  • Finalizer 机制制造了另一个强引用

在垃圾回收时,如果一个对象即将被清理,但是它实现了 FInalizer,那它就暂时不会被立刻清理,而是加入到另个独立于垃圾收集的线程去执行 Finalizer。假如我们所有对象都实现了 Finalizer,那那么垃圾收集将没有任何成果,OOM 也将出现。

要多说一点的是,不建议使用 finalizer 去释放资源,但也不建议使用虚幻引用去清理资源。最好还是手动在 try/catch/finally 或 try-resources 去释放资源。

关于虚幻引用不得不知道的知识

虚幻引用允许程序去清理那些不再被使用的对象,因此程序可以借此清理已经不在内存中的资源。不像 finalizers,我们使用虚幻引用来清理对象时,对象已经不再内存了。

虚幻引用还有一点跟 Finalizer 不一样的是,清理是在程序调用的时候进行的,而不是在垃圾收集的时候触发的。我们可以根据我们需要,开一个或者多个线程来清理对象。一个可选的方式就是,我们通过一个对象工厂来生产我们需要的资源,然后工厂在生产一个新的资源出来之前,先进行一次清理,把已经被垃圾收集的资源做一次清理。

理解虚幻引用最关键的点就是:我们不能通过这个引用关系 reference 去访问对象: get() 一直返回 null,即使是这个被引用的对象是强可达的。这也意味着我们这个虚幻引用不能帮我们拿到被引用对象,我们也无法通过虚幻引用知道对象是否被清理。所以我们必须自己另外对被引用对象做一个强引用保存起来,并用一个引用队列 ReferenceQueue 来标记那些已经被垃圾收集的对象。

下图是虚幻引用典型的使用方式,看不懂的可以配合后面的虚幻引用实现连接池的例子来理解。

使用虚幻引用实现一个连接池

数据库连接是应用中最宝贵的资源之一:它需要花一定的时间来建立连接,并且数据库服务器会严格限制并发连接的数量。也因此,程序员们应该非常谨慎地使用数据库连接。但还是有时会有为了查询打开连接,然后忘记手动清理或者忘记在 finally 块中清理。

比起在应用中直接使用数据库连接,大多数应用还是会选择使用数据库连接池来管理连接:这个连接池会维持一定地数据库连接,并且在程序需要使用到数据库连接的使用从提供可用的连接。可靠的连接池会提供几种功能来防止连接泄漏,包括超时(连接查询太长时间),还有从垃圾回收中恢复可用的连接。

后面这个功能,就可以用虚幻引用来实现了。为了达到目的,连接池提供的连接 Connection 必须在真实的数据库连接上做一层包装。这样做的好处是,被包装的连接对象可以被系统垃圾回收,但是底层真实的数据库连接仍会保留下来继续被后续使用。这样看来,数据库连接池通过虚幻引用来关联包装的连接,并且在虚幻引用进入引用队列时,回收真实的连接到连接池中。

这个池还有一个点要关注,那就是 PooledConnection 类,代码在下面。如同上面说的,这是一个包装过的类,它将请求委派给真正的连接。其中,我用了动态代理来实现这个类。每个 Java 版本的 JDBC 接口都在改进,也因此,如果是根据某个 JDK 写出来的代码,那么前一个版本的 JDK 或者后面版本的 JDK 都可能跑不动下面的连接池代码。这里使用了动态代理就解决了这个问题,而且也使得代码简洁了一些。

public class PooledConnection
implements InvocationHandler
{
    private ConnectionPool _pool;
    private Connection _cxt;

    public PooledConnection(ConnectionPool pool, Connection cxt)
    {
        _pool = pool;
        _cxt = cxt;
    }

    private Connection getConnection()
    {
        try
        {
            if ((_cxt == null) || _cxt.isClosed())
                throw new RuntimeException("Connection is closed");
        }
        catch (SQLException ex)
        {
            throw new RuntimeException("unable to determine if underlying connection is open", ex);
        }

        return _cxt;
    }

    public static Connection newInstance(ConnectionPool pool, Connection cxt)
    {
        return (Connection)Proxy.newProxyInstance(
                   PooledConnection.class.getClassLoader(),
                   new Class[] { Connection.class },
                   new PooledConnection(pool, cxt));
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable
    {
        // if calling close() or isClosed(), invoke our implementation
        // otherwise, invoke the passed method on the delegate
    }

    private void close() throws SQLException
    {
        if (_cxt != null)
        {
            _pool.releaseConnection(_cxt);
            _cxt = null;
        }
    }

    private boolean isClosed() throws SQLException
    {
        return (_cxt == null) || (_cxt.isClosed());
    }
}

要关注的最重要的地方是,PooledConnection 关联了底层的数据库连接还有我们的连接池。后一个点用来让程序关闭包装的连接:我们通知连接池我们已经用完了连接,然后连接池就可以回收底层真正的连接来重用。

还要提及一下 getConnection() 方法,它检查了一种特殊情况:程序是否尝试使用一个已经关闭的连接。如果没有这个检查,然后直接使用一个已经重新分配给其他地方使用的连接,那么会造成相当恶劣的结果。总结起来就是, close() 显示地关闭包装的连接,getConnection() 检查连接是否被关闭的特殊情况,然后动态代理委派请求给真实的底层连接。

接下来看看连接池的代码

private Queue<Connection> _pool = new LinkedList<Connection>();

private ReferenceQueue<Object> _refQueue = new ReferenceQueue<Object>();

private IdentityHashMap<Object,Connection> _ref2Cxt = new IdentityHashMap<Object,Connection>();
private IdentityHashMap<Connection,Object> _cxt2Ref = new IdentityHashMap<Connection,Object>();

我们构建完底层可用的连接后将它存储在 _pool,然后使用一个引用队列来标记那些已经被关闭的包装连接。最后,我们使用两个 Map 来构成底层连接和包装连接的虚幻引用的双向 Map,用来释放已经用完的连接。

如同我们上面说的,真实的底层数据库连接会被包装起来,这里我们用了 wrapConnection() 方法来做这件事,在这个方法里我们还创建了虚幻引用,并做了连接-引用双向映射。

private synchronized Connection wrapConnection(Connection cxt)
{
    Connection wrapped = PooledConnection.newInstance(this, cxt);
    PhantomReference<Connection> ref = new PhantomReference<Connection>(wrapped, _refQueue);
    _cxt2Ref.put(cxt, ref);
    _ref2Cxt.put(ref, cxt);
    System.err.println("Acquired connection " + cxt );
    return wrapped;
}

wrapConnection 相反的是 releaseConnection(),这个方法有两种处理情况:一种是连接被显式关闭释放。

synchronized void releaseConnection(Connection cxt)
{
    Object ref = _cxt2Ref.remove(cxt);
    _ref2Cxt.remove(ref);
    _pool.offer(cxt);
    System.err.println("Released connection " + cxt);
}

另一种是连接没有被手动释放,而是被垃圾回收后,我们通过相应的虚幻引用,来解放底层连接。

private synchronized void releaseConnection(Reference<?> ref)
{
    Connection cxt = _ref2Cxt.remove(ref);
    if (cxt != null)
        releaseConnection(cxt);
}

另外,有一种边缘情况我们要考虑的是:如果我们程序并发调用了 getConncetion()close() 会怎么样?这也是为什么我在上面的 releaseConnection() 中添加了一个 synchronized 关键字,接下来我们再改造下 getConnection() 方法,加上 synchronized,就避免了这种边缘情况。

public Connection getConnection() throws SQLException
{
    while (true)
    {
        synchronized (this) 
        {
            if (_pool.size() > 0)
                return wrapConnection(_pool.remove());
        }    

        tryWaitingForGarbageCollector();
    }
}

可以想到,理想的情况是我们每次请求 getConnection() 都会返回一个可用的连接,但是我们必须考虑没有现成的可用连接的情况,这里我们就用了 tryWaitingForGarbageCollector() 方法来检查有没有废弃的连接没有被显式清理掉,并解放底层的连接。

private void tryWaitingForGarbageCollector()
{
    try
    {
        Reference<?> ref = _refQueue.remove(100);
        if (ref != null)
            releaseConnection(ref);
    }
    catch (InterruptedException ignored)
    {
        // we have to catch this exception, but it provides no information here
        // a production-quality pool might use it as part of an orderly shutdown
    }
}

相关代码我已经整理到了 github 上:https://github.com/wean2016/ConnectionPool

虚幻引用存在的问题

如同 Finalizer,虚幻引用也存在如果垃圾回收一直不执行,那么它相关的代码就一直不会运行的问题。如果在上面的例子中,我们初始化了 5 个连接,并且一直向连接池申请连接,那么可用连接很快就会耗尽,垃圾回收不会执行,我们将一直陷入等待。

解决这个问题最简单的方法是,在 tryWaitingForGarbageCollector 手动调用 System.gc()。这个解决方案也同样适用于 Finalizer。

但这不意味着我们可以只关注 Finalizer 而忽视虚幻引用。实际上,如果这个连接池用 Finalizer 来处理,我们需要关闭连接池的话,在 Fianlizer 中我们要显式手动关闭连接池和相关连接,代码相当长。而使用虚幻引用来做这件事,那就很简洁了,只要关联一下虚幻引用就可以在合适的时候清理掉了。

一个最后的思考:有时候我们也许只是需要更大的内存

有时候引用对象确实是我们管理内存相当有用的工具,但是它们并不是万能的。如果我们要维持一个超大的对象连接图,但是我们只有极少内存,那么我们再怎么秀,也秀不起来是吧。

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