HotSpot note (paet-2)

part 2

这个comment希望能分析一下GenCollectedHeap::do_collection这个函数的具体执行流程,根据函数名字可以猜测该函数实现的功能就是做垃圾回收,下面是它的方法声明(声明和定义是有区别的,声明仅仅是告诉别人有这样一个函数,而定义则是说这个函数具体实现了什么功能):

  // Helper function for two callbacks below.
  // Considers collection of the first max_level+1 generations.
  void do_collection(bool           full,
                     bool           clear_all_soft_refs,
                     size_t         size,
                     bool           is_tlab,
                     GenerationType max_generation);

参数full代表是否是FullGC,clear_all_soft_refs参数表示是否要回收sort reference,size参数需要多说明一点,在一些情况下,GC发生是因为发送了"Allocate Fail",这个size就代表了申请分配的内存大小;is_tlab表示是否使用 TLAB(线程分配Buffer,可以避免多线程在堆上并发申请内存),max_generation参数表示最大的回收代,只有两种类型,YoungGen或者OldGen;下面来仔细分析一下这个函数。

  • (1)、首先是做一些基本的校验,比如是否在safe_point,是否是GC线程访问该函数,以及是否已经有其他的线程触发了GC,这些条件都需要满足才能执行接下来的代码。

<img width="946" alt="2018-11-11 10 04 16" src="https://user-images.githubusercontent.com/16225796/48308185-65793780-e599-11e8-83d6-607af6a98722.png">

  • (2)、接下来需要做一些GC策略的生成,主要是判断是否回收soft reference对象,是否收集Young或者Old区域等。complete表示是否收集整个堆,old_collects_young表示是否在收集老年代的同时收集新生代,也就是是否有必要收集新生代,JVM参数ScavengeBeforeFullGC控制是否在FullGC前做一次YoungGC,如果设置了该参数,那在收集old区的时候就没有必要再回收young区了;do_young_collection表示是否需要对young区域进行垃圾收集,判断标准就是young区域确实需要回收了,也就是进行YoungGC,

<img width="1223" alt="2018-11-11 10 07 02" src="https://user-images.githubusercontent.com/16225796/48308258-6f039f00-e59b-11e8-8bac-435316592a08.png">

  • (3)、现在,知道该回收哪些区域了,那么接下来就去回收需要回收的区域,如果do_young_collection是true的,那么就执行YoungGC,collect_generation函数是具体的执行某个区域垃圾回收的入口,待会再来分析这个函数的具体流程;接着也判读oldGen是否需要回收,如果需要的话也进行回收。

<img width="1081" alt="2018-11-11 10 22 01" src="https://user-images.githubusercontent.com/16225796/48308286-33b5a000-e59c-11e8-914f-f54bad42683a.png">

<img width="1291" alt="2018-11-11 10 25 21" src="https://user-images.githubusercontent.com/16225796/48308297-868f5780-e59c-11e8-957c-822f4a25e7d6.png">

  • (4)、垃圾收集完成之后,需要计算各个分代的大小因为GC之后堆可能会扩展,所以需要重新计算一下各个分代的大小,重新计算大小通过调用函数compute_new_size实现,该函数需要调整各个分代的各种指针,使得堆扩展后各个分代依然可以正常工作。

下面,来分析上面几个步骤中出现的一些关键函数,首先是should_collect函数,该函数用于判断某一个分代是否需要做垃圾回收下面来看看该方法的细节

  // Returns "true" iff collect() should subsequently be called on this
  // this generation. See comment below.
  // This is a generic implementation which can be overridden.
  //
  // Note: in the current (1.4) implementation, when genCollectedHeap's
  // incremental_collection_will_fail flag is set, all allocations are
  // slow path (the only fast-path place to allocate is DefNew, which
  // will be full if the flag is set).
  // Thus, older generations which collect younger generations should
  // test this flag and collect if it is set.
  virtual bool should_collect(bool   full,
                              size_t word_size,
                              bool   is_tlab) {
    return (full || should_allocate(word_size, is_tlab));
  }

如果是FullGC,那么无论哪个分代都应该被回收,如果不是FullGC,那么就使用should_allocate函数继续判断是否需要在该分代进行收集,比如对于DefNew(Serial GC下新生代)分代来说,其具体实现就如下:

  // Allocation support
  virtual bool should_allocate(size_t word_size, bool is_tlab) {
    assert(UseTLAB || !is_tlab, "Should not allocate tlab");

    size_t overflow_limit    = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);

    const bool non_zero      = word_size > 0;
    const bool overflows     = word_size >= overflow_limit;
    const bool check_too_big = _pretenure_size_threshold_words > 0;
    const bool not_too_big   = word_size < _pretenure_size_threshold_words;
    const bool size_ok       = is_tlab || !check_too_big || not_too_big;

    bool result = !overflows &&
                  non_zero   &&
                  size_ok;

    return result;
  }

接着一个重要的函数就是collect_generation,这个函数将回收给定的分代中的垃圾,主要看下面的这段代码片段:

// Do collection work
  {
    // Note on ref discovery: For what appear to be historical reasons,
    // GCH enables and disabled (by enqueing) refs discovery.
    // In the future this should be moved into the generation's
    // collect method so that ref discovery and enqueueing concerns
    // are local to a generation. The collect method could return
    // an appropriate indication in the case that notification on
    // the ref lock was needed. This will make the treatment of
    // weak refs more uniform (and indeed remove such concerns
    // from GCH). XXX

    HandleMark hm;  // Discard invalid handles created during gc
    save_marks();   // save marks for all gens
    // We want to discover references, but not process them yet.
    // This mode is disabled in process_discovered_references if the
    // generation does some collection work, or in
    // enqueue_discovered_references if the generation returns
    // without doing any work.
    ReferenceProcessor* rp = gen->ref_processor();
    // If the discovery of ("weak") refs in this generation is
    // atomic wrt other collectors in this configuration, we
    // are guaranteed to have empty discovered ref lists.
    if (rp->discovery_is_atomic()) {
      rp->enable_discovery();
      rp->setup_policy(clear_soft_refs);
    } else {
      // collect() below will enable discovery as appropriate
    }
    gen->collect(full, clear_soft_refs, size, is_tlab);
    if (!rp->enqueuing_is_done()) {
      rp->enqueue_discovered_references();
    } else {
      rp->set_enqueuing_is_done(false);
    }
    rp->verify_no_references_recorded();
  }

接着看gen->collect函数调用,这里面就是做具体的垃圾收集工作,比如下面分析在DefNew分代中的gen->collect实现。

  • (1)、DefNew是Serial GC下的新生代,首先它要判断是否有必要让老年代来做这次GC,使用collection_attempt_is_safe函数来做这个判断,也就是判断出收集该区域是否是安全的,所谓安全的,就是DefNew分代收集了之后,old 区域是否可以完整的将这次Minor GC之后晋升的对象安置起来,如果不能的话,那DefNew就举得自己做GC是不安全的,应该让老年代来做GC,这也是最合适的选择,老年代会做一次规模宏大的GC,并且做一些内存规整的工作,避免新生代中晋升上来的大对象无法找到连续的空间放置,当然,老年代GC实现上几乎都包含"整理"的阶段,这也是为什么老年代发生GC耗时是新生代GC的10倍的原因之一,新生代使用copying算法,是一种非常快速的收集算法,当然也得益于新生代中的对象寿命都比较短,不像老年代中的对象寿命较长,当然,这也是分代的意义所在;

<img width="1083" alt="2018-11-11 11 06 01" src="https://user-images.githubusercontent.com/16225796/48308556-ef79ce00-e5a2-11e8-9a81-e3cd4839f8c7.png">

collection_attempt_is_safe函数的实现如下:

bool DefNewGeneration::collection_attempt_is_safe() {
  if (!to()->is_empty()) {
    log_trace(gc)(":: to is not empty ::");
    return false;
  }
  if (_old_gen == NULL) {
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    _old_gen = gch->old_gen();
  }
  return _old_gen->promotion_attempt_is_safe(used());
}

正常来说,用区域内两个survivor中有一个区域总是空闲的,但是在某些情况下也会发生意外,使得两个survivor都不为空,这种情况是有可能发生的,首先DefNew在进行YoungGC之后,会将Eden + From中存活的对象拷贝到To中去,并且将一些符合晋升要求的对象拷贝到old区域中去,然后调换两个survivor的角色,所以按理来说其中某个survivor区域总是空的,但是这是在YoungGC顺利完成的情况,在发生"promotion failed"的时候就不会去清理From和To,这一点在后续会再次说明;但是肯定的是,如果To区域不为空,那么就说明前一次YoungGC并不是很顺利,此时DefNew就举得没必要再冒险去做一次可能没啥用处的Minor GC,因为有可能Minor GC之后需要出发一次Full GC来解决某些难题,所以DefNew基于自己的历史GC告诉Old去做一些较为彻底的GC工作时必要的;如果没有发生"promotion fail"这种不愉快的事情,那么接下来就让old区自己判断是否允许本次Minor GC的发生,也就是_old_gen->promotion_attempt_is_safe的调用,下面来看看该函数的具体实现;

bool TenuredGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
  size_t available = max_contiguous_available();
  size_t av_promo  = (size_t)gc_stats()->avg_promoted()->padded_average();
  bool   res = (available >= av_promo) || (available >= max_promotion_in_bytes);

  log_trace(gc)("Tenured: promo attempt is%s safe: available(" SIZE_FORMAT ") %s av_promo(" SIZE_FORMAT "), max_promo(" SIZE_FORMAT ")",
    res? "":" not", available, res? ">=":"<", av_promo, max_promotion_in_bytes);

  return res;
}

老年代也会看历史数据,如果发现老年代的最大连续空间大小大于新生代历史晋升的平均大小或者新生代中存活的对象,那么老年代就认为本次Minor GC是安全的,没必要做一次Full GC;当然这是有一些冒险的成分的,如果某一次minorGC发生之后符合晋升条件的对象大小远远大小评价晋升大小,而且这个时候老年代连续空间小于这些符合晋升的对象大小的时候,悲剧就发生了,也就是上面说到的"promotion fail",这个时候就要做一次Full GC。

  • (2)、接着关键的一个步骤就是进行对象存活判断,并且将存活的对象转移到正确的位置,比如To区域或者old区域;

<img width="875" alt="2018-11-11 11 53 16" src="https://user-images.githubusercontent.com/16225796/48308898-8a28db80-e5a8-11e8-887e-1d16e41998fd.png">

FastEvacuateFollowersClosure是一个递归的过程,Closure后缀代表 它是一个回调操作,所谓递归,就是在判断对象存活并且copying的工作是递归进行的,首先找到root objects,然后根据root objects去标记存活的对象,并且将它们转移到合适的区域中去;gch->young_process_roots做的工作就是将root objects转移到其他空间去的函数:

void GenCollectedHeap::young_process_roots(StrongRootsScope* scope,
                                           OopsInGenClosure* root_closure,
                                           OopsInGenClosure* old_gen_closure,
                                           CLDClosure* cld_closure) {
  MarkingCodeBlobClosure mark_code_closure(root_closure, CodeBlobToOopClosure::FixRelocations);

  process_roots(scope, SO_ScavengeCodeCache, root_closure, root_closure,
                cld_closure, cld_closure, &mark_code_closure);
  process_string_table_roots(scope, root_closure);

  if (!_process_strong_tasks->is_task_claimed(GCH_PS_younger_gens)) {
    root_closure->reset_generation();
  }

  // When collection is parallel, all threads get to cooperate to do
  // old generation scanning.
  old_gen_closure->set_generation(_old_gen);
  rem_set()->younger_refs_iterate(_old_gen, old_gen_closure, scope->n_threads());
  old_gen_closure->reset_generation();

  _process_strong_tasks->all_tasks_completed(scope->n_threads());
}

这里面关键的函数是process_roots,该函数会对设置的各种Closure进行回调,比如FastScanClosure,具体的回调工作将在Closure的do_oop_work进行:

// NOTE! Any changes made here should also be made
// in ScanClosure::do_oop_work()
template <class T> inline void FastScanClosure::do_oop_work(T* p) {
  T heap_oop = oopDesc::load_heap_oop(p);
  // Should we copy the obj?
  if (!oopDesc::is_null(heap_oop)) {
    oop obj = oopDesc::decode_heap_oop_not_null(heap_oop);
    if ((HeapWord*)obj < _boundary) {
      assert(!_g->to()->is_in_reserved(obj), "Scanning field twice?");
      oop new_obj = obj->is_forwarded() ? obj->forwardee()
                                        : _g->copy_to_survivor_space(obj);
      oopDesc::encode_store_heap_oop_not_null(p, new_obj);
      if (is_scanning_a_klass()) {
        do_klass_barrier();
      } else if (_gc_barrier) {
        // Now call parent closure
        do_barrier(p);
      }
    }
  }
}

如果对象已经被复制过了,那么就不用再复制一次了,否则调用copy_to_survivor_space将该对象复制到to区域中去,下面是copy_to_survivor_space函数的具体实现:

oop DefNewGeneration::copy_to_survivor_space(oop old) {
  assert(is_in_reserved(old) && !old->is_forwarded(),
         "shouldn't be scavenging this oop");
  size_t s = old->size();
  oop obj = NULL;

  // Try allocating obj in to-space (unless too old)
  if (old->age() < tenuring_threshold()) {
    obj = (oop) to()->allocate_aligned(s);
  }

  // Otherwise try allocating obj tenured
  if (obj == NULL) {
    obj = _old_gen->promote(old, s);
    if (obj == NULL) {
      handle_promotion_failure(old);
      return old;
    }
  } else {
    // Prefetch beyond obj
    const intx interval = PrefetchCopyIntervalInBytes;
    Prefetch::write(obj, interval);

    // Copy obj
    Copy::aligned_disjoint_words((HeapWord*)old, (HeapWord*)obj, s);

    // Increment age if obj still in new generation
    obj->incr_age();
    age_table()->add(obj, s);
  }

  // Done, insert forward pointer to obj in this header
  old->forward_to(obj);

  return obj;
}

这个函数的流程大概是这样的:首先判断对象是否达到了晋升到老年代的年龄阈值,如果到了,那么就要将对象拷贝到老年代中去,否则就要将对象拷贝到to区域中去,这里面也包括一个细节,如果对象没有达到晋升老年代的年龄阈值,但是无法拷贝到To区域中去,那么也试图将对象晋升到老年代,也就是将对象提前晋升,晋升是有风险的,可能晋升失败,那么就要通过调用handle_promotion_failure来处理晋升失败的情况,如果对象成功拷贝到了To区域中来,那么就要将对象的年龄更新一下,最后,需要需要标记对象已经被转移,如果可能,那么就把老的对象清空吧;下面来先来看看promote函数,该函数用于将对象晋升到老年代:

// Ignores "ref" and calls allocate().
oop Generation::promote(oop obj, size_t obj_size) {
  assert(obj_size == (size_t)obj->size(), "bad obj_size passed in");

#ifndef PRODUCT
  if (GenCollectedHeap::heap()->promotion_should_fail()) {
    return NULL;
  }
#endif  // #ifndef PRODUCT

  HeapWord* result = allocate(obj_size, false);
  if (result != NULL) {
    Copy::aligned_disjoint_words((HeapWord*)obj, result, obj_size);
    return oop(result);
  } else {
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    return gch->handle_failed_promotion(this, obj, obj_size);
  }
}

这个函数较为简单,首先通过allocate函数试图在老年代申请一块可以容纳对象的内存,如果成功了,那么就将对象复制到里面去,否则通过handle_failed_promotion函数来处理晋升失败的情况,晋升失败的前提下,handle_failed_promotion在handle_promotion_failure前执行,看起来都是处理晋升失败的情况,下面先看看handle_failed_promotion:

oop GenCollectedHeap::handle_failed_promotion(Generation* old_gen,
                                              oop obj,
                                              size_t obj_size) {
  guarantee(old_gen == _old_gen, "We only get here with an old generation");
  assert(obj_size == (size_t)obj->size(), "bad obj_size passed in");
  HeapWord* result = NULL;

  result = old_gen->expand_and_allocate(obj_size, false);

  if (result != NULL) {
    Copy::aligned_disjoint_words((HeapWord*)obj, result, obj_size);
  }
  return oop(result);
}

可以看到,oldGen将试图去扩展自己的堆空间来让更多的新生代对象可以成功晋升,但是很多情况下,堆空间被设置为不可扩展,这种情况下这个方法也就做了无用功,接着会调用handle_promotion_failure,调用handle_promotion_failure代表老年代也就明确告诉新生代无法将本次晋升的这个对象放置到老年代,来看看handle_promotion_failure会有什么对策:

void DefNewGeneration::handle_promotion_failure(oop old) {
  log_debug(gc, promotion)("Promotion failure size = %d) ", old->size());

  _promotion_failed = true;
  _promotion_failed_info.register_copy_failure(old->size());
  _preserved_marks_set.get()->push_if_necessary(old, old->mark());
  // forward to self
  old->forward_to(old);

  _promo_failure_scan_stack.push(old);

  if (!_promo_failure_drain_in_progress) {
    // prevent recursion in copy_to_survivor_space()
    _promo_failure_drain_in_progress = true;
    drain_promo_failure_scan_stack();
    _promo_failure_drain_in_progress = false;
  }
}

看起来DefNew还是比较乐观的,既然老年代容纳不了你,那么这个晋升的对象就还呆在新生代吧,说不定下次老年代发生GC就可以成功把它拷贝过去呢。这个时候_promotion_failed也被标记物为了true,这个标记之后会有用,发生"promotion fail"之后From区域可能存在一些对象没有成功晋升到老年代,但是又不是垃圾,这个时候From和To区域都不为空了,这是个难题。

接着,是时候执行递归标记&复制的过程了,也就是evacuate_followers.do_void(),这个过程是非常复杂的,下面来稍微看看这个函数:

void DefNewGeneration::FastEvacuateFollowersClosure::do_void() {
  do {
    _gch->oop_since_save_marks_iterate(GenCollectedHeap::YoungGen, _scan_cur_or_nonheap, _scan_older);
  } while (!_gch->no_allocs_since_save_marks());
  guarantee(_young_gen->promo_failure_scan_is_complete(), "Failed to finish scan");
}

不断使用oop_since_save_marks_iterate来做递归遍历的工作,结束条件是通过no_allocs_since_save_marks来决定的,下面是no_allocs_since_save_marks函数的具体实现:

bool GenCollectedHeap::no_allocs_since_save_marks() {
  return _young_gen->no_allocs_since_save_marks() &&
         _old_gen->no_allocs_since_save_marks();
}

看名字应该是说没有分配发生了,比如看看DefNew的no_allocs_since_save_marks函数实现:

bool DefNewGeneration::no_allocs_since_save_marks() {
  assert(eden()->saved_mark_at_top(), "Violated spec - alloc in eden");
  assert(from()->saved_mark_at_top(), "Violated spec - alloc in from");
  return to()->saved_mark_at_top();
}

top()指向To区域空闲空间的起点,上面已经说过的一个过程是将root objects先标记并且拷贝到To区域或者老年代,这个时候To区域内已经存在的对象是存活的,需要递归遍历这些对象引用的对象,然后也进行拷贝工作,saved_mark_at_top就是判断是否还在有对象呗拷贝到To区域中来,如果还有对象拷贝进来,那么就说明GC还没有完成,继续循环执行oop_since_save_marks_iterate,否则就可以停止了;下面来看看oop_since_save_marks_iterate函数的实现:

#define ContigSpace_OOP_SINCE_SAVE_MARKS_DEFN(OopClosureType, nv_suffix)  \
                                                                          \
void ContiguousSpace::                                                    \
oop_since_save_marks_iterate##nv_suffix(OopClosureType* blk) {            \
  HeapWord* t;                                                            \
  HeapWord* p = saved_mark_word();                                        \
  assert(p != NULL, "expected saved mark");                               \
                                                                          \
  const intx interval = PrefetchScanIntervalInBytes;                      \
  do {                                                                    \
    t = top();                                                            \
    while (p < t) {                                                       \
      Prefetch::write(p, interval);                                       \
      debug_only(HeapWord* prev = p);                                     \
      oop m = oop(p);                                                     \
      p += m->oop_iterate_size(blk);                                      \
    }                                                                     \
  } while (t < top());                                                    \
                                                                          \
  set_saved_mark_word(p);                                                 \
}

ALL_SINCE_SAVE_MARKS_CLOSURES(ContigSpace_OOP_SINCE_SAVE_MARKS_DEFN)

在深入下去的部分就比较复杂了,不再做分析,但是需要注意的一点是,DefNew在将存活对象复制到To区域的时候,Eden + From区域的对象是否存活不仅仅会看是否被To区域的对象引用,还会看老年代是否存在跨代引用新生代的对象的情况,这种情况也需要将存活的对象转到To或者老年代。

  • (3)、接下来需要对GC过程中发现的引用进行一些处理,比如是否回收soft reference,以及堆weak reference的回收等工作;

<img width="975" alt="2018-11-11 1 03 35" src="https://user-images.githubusercontent.com/16225796/48309323-74b8af00-e5b2-11e8-90d3-14fc28213502.png">

  • (4)、到此GC工作大概已经完成了,接下来需要做一些收尾工作,如果发现在Minor GC的过程中发生了"promotion fail",那么就要做特殊的处理,younger_refs_iterate会将那些晋升失败的对象恢复回来,否则下一次发生Minor GC的时候会误以为这些对象已经被复制过了,但是他们确实没有被转移成功,这样的话,这些对象可能一直留在新生代,无论经历多少次GC都无法发生转移;

<img width="943" alt="2018-11-11 1 05 45" src="https://user-images.githubusercontent.com/16225796/48309362-7cc51e80-e5b3-11e8-9a8c-828a06f700c3.png">

<img width="912" alt="2018-11-11 1 05 59" src="https://user-images.githubusercontent.com/16225796/48309367-851d5980-e5b3-11e8-97bf-2f09a09e2b15.png">

无论如何,新生代发生了GC,经过这次GC,需要转换From和To两个survivor的角色,swap_spaces函数实现了这个功能:

void DefNewGeneration::swap_spaces() {
  ContiguousSpace* s = from();
  _from_space        = to();
  _to_space          = s;
  eden()->set_next_compaction_space(from());
  // The to-space is normally empty before a compaction so need
  // not be considered.  The exception is during promotion
  // failure handling when to-space can contain live objects.
  from()->set_next_compaction_space(NULL);

  if (UsePerfData) {
    CSpaceCounters* c = _from_counters;
    _from_counters = _to_counters;
    _to_counters = c;
  }
}

这个函数较为简单,只是swap了一下From和To;再说一句,如果没有发生"Promotion Fail",那么在Minor GC之后,需要将From和Eden清空,因为没有发生晋升失败事件,就说明所以在新生代(Eden + From)存活的对象都安全的转移到了To或者老年代,所以可以清空,但是发生晋升失败意味着有部分存活的对象依然还留在原地等待,所以不能clear掉。

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