Java反射: 从JDK到JVM全链路详解

该篇文章,将从我们熟悉的Java反射API出发,一直到JVM源码对Java反射的支持。本文分析用的是JDK1.7。

首先看使用的示例代码:

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

        Class<String> stringClass = String.class;

        Method hashCodeMethod = stringClass.getDeclaredMethod("hashCode");

        String str= "hello, world";
        Object hashCode= hashCodeMethod.invoke(str);
        System.out.println( hashCode);

    }

这段代码的逻辑十分简单,就是利用反射来获取一个字符串的哈希值。该示例毫无实际利用的价值,但是拿来举例子是足够的了。

这篇文章我们要弄明白的问题是:

  1. getDeclaredMethod方法是如何正确查找到方法的;
  2. invoke方法是如何被执行的;
  3. 这两个方法的调用,在虚拟机的层面上究竟发生了什么;

JDK分析

反射的源码在JDK层面上是比较简单的,getDeclaredMethod方法的源码是:

    @CallerSensitive
    public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
        Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(getName() + "." + name + argumentTypesToString(parameterTypes));
        }
        return method;
    }

其中最重要的就是

//name -- 方法名字, parameterTypes -- 方法参数
Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);

searchMethods是依据传入的方法名字和方法参数,从privateGetDeclaredMethods返回的Method[]中找到对应的方法,返回该方法的副本
所以重要的逻辑是在privateGetDeclaredMethods中,该方法的源码是:

    private Method[] privateGetDeclaredMethods(boolean publicOnly) {
        checkInitted();
        Method[] res;
        ReflectionData<T> rd = reflectionData();
        if (rd != null) {
            res = publicOnly ? rd.declaredPublicMethods : rd.declaredMethods;
            if (res != null) return res;
        }
        // 没有缓存,从JVM中获取
        res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
        if (rd != null) {
            if (publicOnly) {
                rd.declaredPublicMethods = res;
            } else {
                rd.declaredMethods = res;
            }
        }
        return res;
    }

该方法大体上可以分成两个部分:

  1. 从本地缓存中获取Method[];
  2. 如果本地没有缓存,则从虚拟机中获取;

本地缓存

先考察从本地缓存获取,其中关键的一句话是:

        ReflectionData<T> rd = reflectionData();

这一句话就是访问本地缓存。ReflectionData是Class的一个内部类,它里面存储了一个Class所含有的各色信息,包括字段、方法,并且进行了一定的分类,源码是:

    private static class ReflectionData<T> {
        //...其余字段
        volatile Method[] declaredMethods;
        volatile Method[] publicMethods;
        
        // Value of classRedefinedCount when we created this ReflectionData instance
        final int redefinedCount;

        ReflectionData(int redefinedCount) {
            this.redefinedCount = redefinedCount;
        }
    }

这个源码要注意的是其构造函数,在这个构造函数中,只设置了redefinedCount,其余字段都会是Null。
reflectionData()方法源码是:

    private ReflectionData<T> reflectionData() {
        SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
        int classRedefinedCount = this.classRedefinedCount;
        ReflectionData<T> rd;
        if (useCaches &&
            reflectionData != null &&
            (rd = reflectionData.get()) != null &&
            rd.redefinedCount == classRedefinedCount) {
            return rd;
        }
        // else no SoftReference or cleared SoftReference or stale ReflectionData
        // -> create and replace new instance
        return newReflectionData(reflectionData, classRedefinedCount);
    }

这段代码有一个很关键的点:本地缓存的ReflectionData是使用SoftReference的,这意味着,在内存紧张的时候会被回收掉。
如果在回收掉之后再次请求获得Method[],那么就会新建一个SoftReference,作为缓存。也就是其中newReflectionData的逻辑:

    private ReflectionData<T> newReflectionData(SoftReference<ReflectionData<T>> oldReflectionData,
                                                int classRedefinedCount) {
        if (!useCaches) return null;

        while (true) {
            ReflectionData<T> rd = new ReflectionData<>(classRedefinedCount);
            // try to CAS it...
            if (Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))) {
                return rd;
            }
            // ...其余代码
        }
    }

这段逻辑看上去很简单,但是其中有一些很容易遗漏的点。首先要注意的是,当走到

ReflectionData<T> rd = new ReflectionData<>(classRedefinedCount);

这一句的时候,rd里面只有一个redefinedCount字段被设置,其余字段都还是null。该方法的核心是:

Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))

该方法的底层是使用Unsafe的compareAndSwapObject方法来实现的:

        static <T> boolean casReflectionData(Class<?> clazz,
                                             SoftReference<ReflectionData<T>> oldData,
                                             SoftReference<ReflectionData<T>> newData) {
            return unsafe.compareAndSwapObject(clazz, reflectionDataOffset, oldData, newData);
        }

这个方法的含义就是:如果clazz内存分布中,在reflectionDataOffset位置的地方,如果期望的值是oldData,那么就会使用newData来替换掉oldData。而在clazz内存的reflectionDataOffset的地方,恰好就是Class类中reflectionData域引用的地方。所以这个方法的真实含义,借助CAS原子操作,将老的reflectionData替换为新的reflectionData。

但是,这里要注意的一点是,此时的newData,也就是

new SoftReference<>(rd)

它是一个新建的ReflectionData的实例的soft reference。而新建的ReflectionData,也就是rd,前面已经提到了,它只有redefinedCount字段是被设置好了的,其余字段都还是null。所以如果没有本地缓存,在方法返回了之后,一直到privateGetDeclaredMethods方法的

ReflectionData<T> rd = reflectionData();

此刻的rd就是方才新建的ReflectionData,只有redefinedCount字段是设置的。所以判断:

if (res != null) return res;

是必然不成立的,因此最终会走到从JVM中获取Method[]的代码处。我们总结一下,在这种情况下,Class的实例类似于:


Class实例

从JVM获取

前面已经提到,在本地缓存失效,或者被回收了之后,需要从JVM当中获得Method[]:

        // No cached value available; request value from VM
        res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));

所以最后是通过调用native方法

    private native Method[]      getDeclaredMethods0(boolean publicOnly);

从JVM中取到了Method[]。
该方法在JVM中的头文件声明是:

// src/share/vm/prims/jvm.h
//JNIEnv: java本地方法调用的上下文,
//ofClas: 在java中的Class实例
// publicOnly: 是否是public,对应于java中native方法中的publicOnly参数
JNIEXPORT jobjectArray JNICALL
JVM_GetClassDeclaredMethods(JNIEnv *env, jclass ofClass, jboolean publicOnly);

该方法的实现是:

// src/share/vm/prims/jvm.cpp

JVM_ENTRY(jobjectArray, JVM_GetClassDeclaredMethods(JNIEnv *env, jclass ofClass, jboolean publicOnly))
{
  //省略一部分不重要的代码...
  // Exclude primitive types and array types -- 对基本类型和数组类型进行特殊处理
  if (java_lang_Class::is_primitive(JNIHandles::resolve_non_null(ofClass))
      || Klass::cast(java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(ofClass)))->oop_is_javaArray()) {
    // Return empty array
    oop res = oopFactory::new_objArray(SystemDictionary::reflect_Method_klass(), 0, CHECK_NULL);
    return (jobjectArray) JNIHandles::make_local(env, res);
  }
  // 取得了ofClass所对应的instanceKlassHandle
  instanceKlassHandle k(THREAD, java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(ofClass)));

  // Ensure class is linked ---确保类已经完成了链接
  k->link_class(CHECK_NULL);

  objArrayHandle methods (THREAD, k->methods());
  int methods_length = methods->length();
  int num_methods = 0;

  //统计有多少方法符合要求,而后为结果分配内存
  int i;
  for (i = 0; i < methods_length; i++) {
    methodHandle method(THREAD, (methodOop) methods->obj_at(i));
    if (!method->is_initializer()) {
      if (!publicOnly || method->is_public()) {
        ++num_methods;
      }
    }
  }

  // Allocate result
  objArrayOop r = oopFactory::new_objArray(SystemDictionary::reflect_Method_klass(), num_methods, CHECK_NULL);
  objArrayHandle result (THREAD, r);

  int out_idx = 0;
  for (i = 0; i < methods_length; i++) {
    methodHandle method(THREAD, (methodOop) methods->obj_at(i));
    if (!method->is_initializer()) {
      if (!publicOnly || method->is_public()) {
        oop m = Reflection::new_method(method, UseNewReflection, false, CHECK_NULL);
        result->obj_at_put(out_idx, m);
        ++out_idx;
      }
    }
  }
  assert(out_idx == num_methods, "just checking");
  return (jobjectArray) JNIHandles::make_local(env, result());
}
JVM_END

如我在其中注释的一样,整个过程可以分成几步:

  1. 先处理基本类型和数组类型;
  2. 确保类已经完成链接,在此处就是确保String已经完成了链接
  3. 统计符合要求的方法,并根据方法个数为结果分配内存
  4. 创建Method[]数组

下面我们将对2和4进行详细的分析。
在这之前先对JVM的oop-klass做一个简单的介绍:

  • oop: ordinary object pointer,普通对象指针,用于描述对象的实例信息
  • klass:Java类在JVM中的表示,是对Java类的描述
    对于我们日常说的一个对象来说,它们的oop-klass模型如图:


    oop-klass

JVM就是用这种方式,将一个对象的数据和对象模型进行分离。普遍意义上来说,我们说持有一个对象的引用,指的是图中的handle,它是oop的一个封装。

处理基本类型和数组类型

  if (java_lang_Class::is_primitive(JNIHandles::resolve_non_null(ofClass))
      || Klass::cast(java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(ofClass)))->oop_is_javaArray()) {
    // Return empty array
    oop res = oopFactory::new_objArray(SystemDictionary::reflect_Method_klass(), 0, CHECK_NULL);
    return (jobjectArray) JNIHandles::make_local(env, res);
  }

JNIHandles::resolve_non_null方法将ofClass转化为JVM内部的一个oop,因为JVM只会直接操作oop实例。
顾名思义,is_primitive是判断是否属于基本类型,其源码实现是:

//src/share/vm/classfile/javaClasses.cpp
bool java_lang_Class::is_primitive(oop java_class) {
  // should assert:
  //assert(java_lang_Class::is_instance(java_class), "must be a Class object");
  klassOop k = klassOop(java_class->obj_field(_klass_offset));
  return k == NULL;
}

这一段的逻辑十分简单,取到java_class这个oop中在klass字段上的值,如果是Null则被认为是基本类型。因为对于任何一个非基本类型的对象来说,oop中必然包含着一个指向其klass实例的指针。


基本类型判断

另外一个判断条件:

Klass::cast(java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(ofClass)))->oop_is_javaArray()

java_lang_Class::as_klassOop方法将一个klass包装成一个oop,逻辑和前面的is_primitive十分相像:

//src/share/vm/classfile/javaClasses.cpp
klassOop java_lang_Class::as_klassOop(oop java_class) {
  assert(java_lang_Class::is_instance(java_class), "must be a Class object");
  klassOop k = klassOop(java_class->obj_field(_klass_offset));
  assert(k == NULL || k->is_klass(), "type check");
  return k;
}

这里额外讨论一点“将一个klass包装成一个oop”是什么意思。在JVM中,对象在内存中的基本存在形式就是oop,正如前面的图所描述的那样。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。在这种设计下,JVM对内存的分配和回收,都可以采用统一的方式来管理。oop-klass-klassKlass关系如图:


oop-klass-klassKlass

我们接下来看Klass::cast将klassOop转化为一个klass,而后调用oop_is_javaArray判断该klass是不是对java数组的描述。
在if判断为真的情况下, 会执行

oop res = oopFactory::new_objArray(SystemDictionary::reflect_Method_klass(), 0, CHECK_NULL);

其中:

 SystemDictionary::reflect_Method_klass()

是取得了Java类java.lang.reflect.Method类在JVM中对应的klass实例。确切说,这个klass是java.lang.reflect.Method在JVM中的镜像类。(注:它和JVM运行时刻真正用的java.lang.reflect.Method的klass是不一样的,本篇博客将不关注这其中的区别,但是读者应该知道这是镜像类。
new_objArray方法的源码是:

// src/share/vm/memory/oopFactory.cpp
//length--数组长度,也就是要为多少个对象分配空间
//klass -- 类的描述,它自身知道应该为每一个java对象分配多大的空间
objArrayOop oopFactory::new_objArray(klassOop klass, int length, TRAPS) {
  assert(klass->is_klass(), "must be instance class");
  if (klass->klass_part()->oop_is_array()) {
    return ((arrayKlass*)klass->klass_part())->allocate_arrayArray(1, length, THREAD);
  } else {
    assert (klass->klass_part()->oop_is_instance(), "new object array with klass not an instanceKlass");
    return ((instanceKlass*)klass->klass_part())->allocate_objArray(1, length, THREAD);
  }
}

很显然关键的部分就是:

allocate_arrayArray(1, length, THREAD)

前面提到过,一个klass是对java类的描述,因此在分配内存的时候,klass必然知道应该给该java类的实例分配多大的内存空间。前面应该知道,此处传入的length是0。但是这并不意味着不分配内存空间。从我们前面对oop的描述来看,即便是没有任何数据——也就是数组不包含任何元素,但是它依然占据一些空间,用于存放mark,metadata等数据。这与

Method[] methods =null;

是全然不同的。
让我们暂时忽略后面跟着的

(jobjectArray) JNIHandles::make_local(env, res)

后面会对这个方法进行深入分析。

创建method[]

这个部分,将在前面分配的内存的基础上,创建Method[]数组。我们应该知道,在JVM里面,是没有Method这个类的,因此,创建Method[]数组,其实质是创建一个methodOop数组。不过我们现在也还没看到,JVM是如何获得一个Class所具有的方法。答案就隐藏在:

instanceKlassHandle k(THREAD, java_lang_Class::as_klassOop(JNIHandles::resolve_non_null(ofClass)));

前面我们已经解释过as_klassOop和resolve_non_null的含义了。所以这一句的关键就是创建了一个instanceKlassHandle的实例k。我们可以先来看看这个instanceKlassHandle的定义,它是由一个宏定义的:

//  src/share/vm/runtime/handles.hpp

#define DEF_KLASS_HANDLE(type, is_a)             \
  class type##Handle : public KlassHandle {      \
   public:                                       \
    /* Constructors */                           \
    type##Handle ()                              : KlassHandle()           {} \
    type##Handle (klassOop obj) : KlassHandle(obj) {                          \
      assert(SharedSkipVerify || is_null() || obj->klass_part()->is_a(),      \
             "illegal type");                                                 \
    }                                                                         \
    type##Handle (Thread* thread, klassOop obj) : KlassHandle(thread, obj) {  \
      assert(SharedSkipVerify || is_null() || obj->klass_part()->is_a(),      \
             "illegal type");                                                 \
    }                                                                         \
                                                 \
    /* Access to klass part */                   \
    type*        operator -> () const            { return (type*)obj()->klass_part(); } \
                                                 \
    static type##Handle cast(KlassHandle h)      { return type##Handle(h()); } \
                                                 \
  };

DEF_KLASS_HANDLE(instanceKlass         , oop_is_instance_slow )

这段宏在编译的时候会被翻译为:

  class instanceKlassHandle : public KlassHandle {     
   public:                                       
    /* Constructors */                           
    instanceKlassHandle () : KlassHandle()  {} 
    instanceKlassHandle (klassOop obj) : KlassHandle(obj) {                          
      assert(SharedSkipVerify || is_null() || obj->klass_part()->oop_is_instance_slow(),      
             "illegal type");                                                 
    }                                                                         
    instanceKlassHandle (Thread* thread, klassOop obj) : KlassHandle(thread, obj) {  
      assert(SharedSkipVerify || is_null() || obj->klass_part()->oop_is_instance_slow(),      
             "illegal type");                                                 
    }                                                                         
                                                 
    /* Access to klass part */                   
    instanceKlass* operator -> () const { return (instanceKlass*)obj()->klass_part(); } 
                                                 
    static instanceKlassHandle cast(KlassHandle h) { return instanceKlassHandle(h()); }                                   
  }

这里调用的构造函数最终将会调用基类Handle的构造函数。现在我们将注意力转回:

k->methods()

如果直接去找instanceKlassHandle里面的methods()方法,那么找到吐血都找不到。因为在宏里面,instanceKlassHandle已经重载了操作符->,所以这个调用实际上调用的是:

// src/share/vm/oops/instanceKlass.hpp
objArrayOop methods() const              { return _methods; }

所以这个部分,直接返回的就是_methods属性。给属性的值,会在String类加载的时候,被赋予值(该部分,本篇文章将不会讨论,实际上,类的加载,创建对应的JVM表示,是一个很复杂的过程)。在获取到了这个代表类方法的objArrayOop,会把它包装成对应的objArrayHandle,同样的,objArrayHandle也是由宏定义的。
现在只剩下最后一个步骤,真正创建Method[]数组。首先来看for循环:

  for (i = 0; i < methods_length; i++) {
    methodHandle method(THREAD, (methodOop) methods->obj_at(i));
    if (!method->is_initializer()) {
      if (!publicOnly || method->is_public()) {
        oop m = Reflection::new_method(method, UseNewReflection, false, CHECK_NULL);
        result->obj_at_put(out_idx, m);
        ++out_idx;
      }
    }
  }

根据前面的解析,读者大概都能够理解methodHandle和methods->obj_at(i)是什么作用。我们将注意力放在关键的:

oop m = Reflection::new_method(method, UseNewReflection, false, CHECK_NULL);

这个调用的返回值就是oop,我们知道,所有的对象,在JVM的存在形式就是一个oop。所以,到这一步基本上就可以认为已经创建了一个Method的对象。
Reflection::new_method是一个十分长的方法,它的源码是:

// src/share/vm/runtime/reflection.cpp
oop Reflection::new_method(methodHandle method, bool intern_name, bool for_constant_pool_access, TRAPS) {
  // ...一些断言
  instanceKlassHandle holder (THREAD, method->method_holder());
  int slot = method->method_idnum();

  Symbol*  signature  = method->signature();
  int parameter_count = ArgumentCount(signature).size();
  oop return_type_oop = NULL;
  objArrayHandle parameter_types = get_parameter_types(method, parameter_count, &return_type_oop, CHECK_NULL);
  if (parameter_types.is_null() || return_type_oop == NULL) return NULL;

  Handle return_type(THREAD, return_type_oop);

  objArrayHandle exception_types = get_exception_types(method, CHECK_NULL);

  if (exception_types.is_null()) return NULL;

  Symbol*  method_name = method->name();
  Handle name;
  if (intern_name) {
    // intern_name is only true with UseNewReflection
    oop name_oop = StringTable::intern(method_name, CHECK_NULL);
    name = Handle(THREAD, name_oop);
  } else {
    name = java_lang_String::create_from_symbol(method_name, CHECK_NULL);
  }
  if (name == NULL) return NULL;

  int modifiers = method->access_flags().as_int() & JVM_RECOGNIZED_METHOD_MODIFIERS;

  Handle mh = java_lang_reflect_Method::create(CHECK_NULL);

//设置oop的各个属性 -------第二部分--------------------------
  java_lang_reflect_Method::set_clazz(mh(), holder->java_mirror());
  java_lang_reflect_Method::set_slot(mh(), slot);
  java_lang_reflect_Method::set_name(mh(), name());
  java_lang_reflect_Method::set_return_type(mh(), return_type());
  java_lang_reflect_Method::set_parameter_types(mh(), parameter_types());
  java_lang_reflect_Method::set_exception_types(mh(), exception_types());
  java_lang_reflect_Method::set_modifiers(mh(), modifiers);
  java_lang_reflect_Method::set_override(mh(), false);
  if (java_lang_reflect_Method::has_signature_field() &&
      method->generic_signature() != NULL) {
    Symbol*  gs = method->generic_signature();
    Handle sig = java_lang_String::create_from_symbol(gs, CHECK_NULL);
    java_lang_reflect_Method::set_signature(mh(), sig());
  }
  if (java_lang_reflect_Method::has_annotations_field()) {
    java_lang_reflect_Method::set_annotations(mh(), method->annotations());
  }
  if (java_lang_reflect_Method::has_parameter_annotations_field()) {
    java_lang_reflect_Method::set_parameter_annotations(mh(), method->parameter_annotations());
  }
  if (java_lang_reflect_Method::has_annotation_default_field()) {
    java_lang_reflect_Method::set_annotation_default(mh(), method->annotation_default());
  }
  return mh();
}

整个方法可以看成两个部分:

  1. 取得oop属性对应的handle
  2. 设置oop属性
    在一个Method实例里面,它有很多的属性,比如说参数类型。那么很显然的是,这些属性在JVM的存在也是oop的形式。所以一个Method的oop形如:


    Method的oop

这里还要提醒一下读者的是,parameter types oop是一个数组类型的oop,其元素,每一个都是klassOop,因为每一个parameter type在Java语言中,就是Class的一个实例,因而正好是一个klassOop。

到了这一步,我们已经完成了大部分的工作,只剩下最后一步了:

JNIHandles::make_local(env, result())

该方法的源码是:

jobject JNIHandles::make_local(JNIEnv* env, oop obj) {
  if (obj == NULL) {
    return NULL;                // ignore null handles
  } else {
    JavaThread* thread = JavaThread::thread_from_jni_environment(env);
    assert(Universe::heap()->is_in_reserved(obj), "sanity check");
    return thread->active_handles()->allocate_handle(obj);
  }
}

这段代码的含义,十分简单,就是获得当前执行反射代码的线程,然后为引用分配内存,最后返回一个已经装配好了的,能够被java代码所访问的jobject。
现在我们从内存的分配角度来一下整个过程。整个过程,实际上只涉及两次内存分配,一次是为oop分配,这个一次的分配是根据要返回的结果来决定;第二次分配是将oop包装成jobject的时候,也就是allocate_handle方法的调用。前面一次分配很好理解,那么最后一次分配是什么意思呢?我的个人理解是这一次是为Java引用所指向的handle分配内存,它的大小就是一个handle指针的大小,在32位上,是4个字节,在64位上是8个字节。简单粗暴的说,就是在栈上分配一个引用。

最后

到这里,我们已经分析完了整个流程。但是因为涉及了一大堆的代码,不容易对流程有个整体的把握。整个过程可以看成是:


整体流程

实际上,如果理解oop-klass模型,那么很容易就明白这一段的源码。不过这篇文章其实忽略了很多的细节,比如说内存分配的分配,就没有深入探究调用链,有兴趣的读者可以自己去看看。或者等待我以后的博客,

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,550评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,337评论 25 707
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,165评论 11 349
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,056评论 0 62
  • 一枝粉色的月季花。
    林逸葵阅读 252评论 0 2