反射代理類加載器的潛在內存使用問題


  • tags:反射
  • categories: problems
  • date: 2017-05-28 14:50:04

使用反射代理類加載器的潛在內存使用問題

大量的類加載器 “sun/reflect/DelegatingClassLoader”,用來加載“sun/reflect/GeneratedMethodAccessor”類,可能導致潛在的占用大量本機內存空間問題,應用服務器進程占用的內存會顯著增大。您還有可能遇到拋出的內存溢出錯誤。案例:假笨說-從一起GC血案談到反射原理

先把結論說明了:

  • 在上述案例中,得到的結論是:反射類加載器導致Perm溢出。系統使用jdk1.7,且GC收集器是G1。這個版本的G1的特性是只有在Full GC的情況下才會對perm(永久區)里的類進行卸載,正常的G1的gc過程是不會對Perm中的類進行卸載的,所以,當Perm內存被類堆積滿的時候,就會進行一次Full Gc將無用的類卸載掉。那么為什么會產生那么多類在Perm區域中呢,通過案例中GC日志的分析可以知道,是因為產生了大量的"sun.reflect.DelegatingClassLoader",那么為什么會有那么多的代理委托類加載器,用于加載什么類的呢?
    從分析可以知道,是因為使用三方的Xfire協議,該協議過程中產生大量的RPC,將得到結果進行反序列化時候,是通過Method.invoke反射原理來實現目的的,在Xfire實現中,內部還有Methodref等包含軟索引SoftReference的引用,很容易就會被G1給回收了,一旦回收了,程序內部就會Copy來創建一個新的Method,在調用其invoke通過MethodAccessor來實際調用,多次操作大于指定閾值(15)就會創建一個“sun/reflect/GeneratedMethodAccessor”類字節碼,然后在通過DelegatingClassLoader來加載該字節碼,就會將這些類都保存到Perm中,如此復返,就會Perm溢出。

  • 當使用Java反射時,Java虛擬機有兩種方法獲取被反射的類的信息。它可以使用一個JNI存取器。如果使用Java字節碼存取器,則需要擁有它自己的Java類和類加載器(sun/reflect/GeneratedMethodAccessor類和sun/reflect/DelegatingClassLoader)。這些類和類加載器使用本機內存。字節碼存取器也可以被JIT編譯,這樣會增加本機內存的使用。如果Java反射被頻繁使用,會顯著地增加本機內存的使用。
    Java虛擬機會首先使用JNI存取器,然后在訪問了同一個類若干次后,會改為使用Java字節碼存取器。這種當Java虛擬機從JNI存取器改為字節碼存取器的行為被稱為膨脹。幸運的是,我們可以通過一個Java屬性控制這種行為。屬性sun.reflect.inflationThreshold會告訴Java虛擬機使用JNI存取器多少次。如果設為0,則總是使用JNI存取器。由于字節碼存取器比JNI存取器使用更多本機內存,當我們看到大量Java反射時,最好使用JNI存取器。我們只需要設置inflationThreshold屬性值為0即可。

    image

Reflection反射原理

下面通過一個簡單的反射例子來捋一捋java內部反射機制過程。

public class ReflectDemo {

    
    public static void main(String[] args) throws Exception{
        Proxy target = new ReflectDemo.Proxy();
        Method method = Proxy.class.getDeclaredMethod("pmethod", null);
        //MethodAccessor.invoke
        method.invoke(target, null);
        
    }
    
    static class Proxy{
        public void pmethod(){
            System.out.println("Proxy.pmethod");
        }
    }
}

上述可以看到通過Class.getDeclaredMethod反射方法獲取Proxy類中pmethod方法,最后成功調用。先從下圖中看看從方法調用到最后方法執行的流程圖:

image

獲取得到Method對象

根據上面的例子,當通過Class.getDeclaredMethod(MethodName)進行反射,獲取指定類的指定的方法的時候,具體是如何進行的?結合上面的流程圖,分步說明:

// Class.java
  @CallerSensitive
    public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
        throws NoSuchMethodException, SecurityException {
        // be very careful not to change the stack depth of this
        // checkMemberAccess call for security reasons
        // see java.lang.SecurityManager.checkMemberAccess
        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;
    }

可以看到,先調用privateGetDeclaredMethods方法,在調用searchMethods方法最后得到一個Method對象。
其中,在看到privateGetDeclaredMethods方法之前,需要知道Class類中有個很重要的屬性:ReflectionData,這個屬性類中就是保存著每次從JVM中獲取指定類時候類中的屬性,比如方法,屬性字段等:

static class ReflectionData<T>{
  volatile Field[] declaredFields;
  volatile Field[] publicFields;
  volatile Method[] declaredMethods;
  volatile Method[] publicMethods;
  volatile Constructor<T>[] declaredConstructors;
  volatile Constructor<T>[] publicConstructors;
  //Intermediate results for getFields and getMethods
  volatile Field[] declaredPublicFields;
  volatile Method[] declaredPublicMethods;
  //value of classRedefineCount when we create this reflectionData instance
  final int redefinedCount;
  
  ReflectionData(int redefinedCount){
    this.redefinedCount = redefinedCount;
  }

}

這個屬性是軟引用SoftReference的,也就是在某些內存比較緊張的情況下,是會被GC回收的,可以通過JVM參數:-XX:SoftRefLRUPolicyMSPerMB來控制回收時機。所以,一旦某個類的ReflectionData屬性被回收了的話,意味著當第二次再想通過這個屬性來獲取反射信息的時候,就會發現,緩存中沒有了,就會重新創建一個新的ReflectionData對象,將從JVM中獲取到類的信息封裝到屬性中,那么這個類的屬性類中的關聯所有Method,Field等屬性對象都是重新創建的。重新創建對象必然會消耗內存資源和一些時間,有一些副作用也是肯定的。

然后,現在新的JDK版本,將ReflactionData對象給取代了,取代方式是在Class.java類中通過聲明反射的軟引用集合屬性:

//java.lang.Class
    /**
     * Reflection support.
     */

    // Caches for certain reflective results
    private static boolean useCaches = true;
    private volatile transient SoftReference<Field[]> declaredFields;
    private volatile transient SoftReference<Field[]> publicFields;
    private volatile transient SoftReference<Method[]> declaredMethods;
    private volatile transient SoftReference<Method[]> publicMethods;
    private volatile transient SoftReference<Constructor<T>[]> declaredConstructors;
    private volatile transient SoftReference<Constructor<T>[]> publicConstructors;
    // Intermediate results for getFields and getMethods
    private volatile transient SoftReference<Field[]> declaredPublicFields;
    private volatile transient SoftReference<Method[]> declaredPublicMethods;

    // Incremented by the VM on each call to JVM TI RedefineClasses()
    // that redefines this class or a superclass.
    private volatile transient int classRedefinedCount = 0;

    // Value of classRedefinedCount when we last cleared the cached values
    // that are sensitive to class redefinition.
    private volatile transient int lastRedefinedCount = 0;

(1) privateGetDeclaredMethods方法:
從緩存或者JVM中獲取該Class中,符合反射方法調用傳遞過出來方法名稱,方法參數類型的Method對象列表。

    private Method[] privateGetDeclaredMethods(boolean publicOnly) {
        checkInitted();
        Method[] res = null;
        if (useCaches) {
            clearCachesOnClassRedefinition();
            if (publicOnly) {
                if (declaredPublicMethods != null) {
                    res = declaredPublicMethods.get();
                }
            } else {
                if (declaredMethods != null) {
                    res = declaredMethods.get();
                }
            }
            if (res != null) return res;
        }
        //若是在當前反射緩沖中,所有的Method,Field軟引用數組中都沒有找到,
        //則調用Reflection.filterMethods方法從JVM內部獲取,將得到的數據
        //封裝到Class的反射引用字段數組中,緩存起來
        // No cached value available; request value from VM
        res = Reflection.filterMethods(this, getDeclaredMethods0(publicOnly));
        if (useCaches) {
            if (publicOnly) {
                
                declaredPublicMethods = new SoftReference<>(res);
            } else {
                //重新緩存
                declaredMethods = new SoftReference<>(res);
            }
        }
        return res;
    }

(2) searchMethods方法調用:
searchMethods將從privateGetDeclaredMethods返回的方法列表里找到一個同名的匹配的方法,然后復制一個新的Method方法對象出來。

    private static Method searchMethods(Method[] methods,
                                        String name,
                                        Class<?>[] parameterTypes)
    {
        Method res = null;
        String internedName = name.intern();
        for (int i = 0; i < methods.length; i++) {
            Method m = methods[i];
            if (m.getName() == internedName
                && arrayContentsEq(parameterTypes, m.getParameterTypes())
                && (res == null
                    || res.getReturnType().isAssignableFrom(m.getReturnType())))
                res = m;
        }
        //拷貝一個Method對象
        return (res == null ? res : getReflectionFactory().copyMethod(res));
    }

(3) ReflectionFactory.copyMethod()方法:
當在第二步中,在方法列表中匹配到同名同參數的Method對象的時候,這時候就調用ReflectionFactory.copyMehthod方法,拷貝一個一樣方法。在ReflectionFactory中有個LangReflectAccess字段,用于解決反射訪問其他包中,私有的,共有的方法或者字段屬性的權限問題。

//ReflectionFactory.java
// Provides access to package-private mechanisms in java.lang.reflect
private static volatile LangReflectAccess langReflectAccess;

  
/** Makes a copy of the passed method. The returned method is a
        "child" of the passed one; see the comments in Method.java for
        details. */
public Method copyMethod(Method arg) {
        return langReflectAccess().copyMethod(arg);
 }



(4) ReflectAccess.copyMethod方法:
上述第三步驟中ReflectionFactory.copyMethod中的langReflectionAccess()方法就是返回一個LangReflectAccess接口類,而ReflectAccess則是該接口的實現類,所以就會調用該類的copyMethod()方法:

   //
    // Copying routines, needed to quickly fabricate new Field,
    // Method, and Constructor objects from templates
    //
    public Method      copyMethod(Method arg) {
        return arg.copy();
    }

可以,最后實質是調用傳入的參數Method對象的copy方法。
(5) Method.copy()方法:
Method對象的copy方法,主要是通過將傳入的Method對象的方法名,參數,等等其他屬性都拷貝一份,但是Method對象的methodAccessor字段卻是共享的。通過Method類中root字段屬性來實現。

//Method.java
    // For sharing of MethodAccessors. This branching structure is
    // currently only two levels deep (i.e., one root Method and
    // potentially many Method objects pointing to it.)
    private Method              root;

    //共享的methodAccessor
    private volatile MethodAccessor methodAccessor;


    Method copy() {
        // This routine enables sharing of MethodAccessor objects
        // among Method objects which refer to the same underlying
        // method in the VM. (All of this contortion is only necessary
        // because of the "accessibility" bit in AccessibleObject,
        // which implicitly requires that new java.lang.reflect
        // objects be fabricated for each reflective call on Class
        // objects.)
        Method res = new Method(clazz, name, parameterTypes, returnType,
                                exceptionTypes, modifiers, slot, signature,
                                annotations, parameterAnnotations, annotationDefault);
        res.root = this;
        // Might as well eagerly propagate this if already present
        res.methodAccessor = methodAccessor;
        return res;//返回拷貝的Method對象
    }

調用Method.invoke方法

通過上面的步驟,就能將目標類的指定反射方法的拷貝對象給獲取到了。就如例子中Method method = Proxy.class.getDeclaredMethod("pmethod", null)這句執行完成了。下面就是調用方法的過程了。

整個Method.invoke方法內部實際調用是MethodAccessor接口實現類的invoke方法調用。Method.invoke只是表面的殼而已,也可以說是代理調用。下面就說說MethodAccessor接口實現類和實際調用過程。

MethodAccessor接口的聲明:

public interface MethodAccessor {
    /** Matches specification in {@link java.lang.reflect.Method} */
    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException;
}

該接口的實現類有以下幾種:

  • NativeMethodAccessorImpl
  • DelegatingMethodAccessorImpl
  • GeneratedMethodAccessorXXX

其中的第一個NativeMethodAccessorImpl對應的就是通過JNI(java本地接口)存取器來獲取反射類字節碼信息。第三個GeneratedMethodAccessor<Num> 類就是通過代理類加載器DelegatingClassLoader來加載反射類字節碼的。中間的代理MethodAccessorImpl則是可以將傳入的MethodAccessor參數對象,注入給Method對象的methodAccessor屬性,實現代理調用:也就是說明,通常情況下,我們調用某個Method對象的invoke方法,內部都是通過這個代理類來實現調用的。

class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
    private MethodAccessorImpl delegate; //代理模式,實際調用的接口實現

    DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
        setDelegate(delegate);
    }

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        return delegate.invoke(obj, args);//代理調用
    }

    void setDelegate(MethodAccessorImpl delegate) {
        this.delegate = delegate;
    }
}

(1) Method.invoke方法調用:
我們代碼顯示調用的Method對象的invoke對象,雖然已經知道實際上是MethodAccessor實現類底層調用,但是也會是可以看看該方法內部源代碼:

   @CallerSensitive
    public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                // Until there is hotspot @CallerSensitive support
                // can't call Reflection.getCallerClass() here
                // Workaround for now: add a frame getCallerClass to
                // make the caller at stack depth 2
                Class<?> caller = getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        //Method類共享methodAccessor對象,判斷是否為null,若是null,則調用指定方法獲取
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            //調用以下方法來獲取MethodAccessor實現類
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

(2) acquireMethodAccessor方法獲取MethodAccessor:
當某個反射方法的methodAccessor屬性字段為null的時候,需要顯示的調用acquireMethodAccessor方法來獲取方法訪問實現對象:

    private MethodAccessor acquireMethodAccessor() {
        // First check to see if one has been created yet, and take it
        // if so
        MethodAccessor tmp = null;
        if (root != null) tmp = root.getMethodAccessor();
        if (tmp != null) {
            methodAccessor = tmp;
        } else {
            // Otherwise fabricate one and propagate it up to the root
            //從反射工廠類中,顯示創建一個MethodAccessor實現對象
            tmp = reflectionFactory.newMethodAccessor(this);
            setMethodAccessor(tmp);
        }

        return tmp;
    }

因為在通過反射獲取Method對象,在copy的時候,有個語句是res.root = this,因為MethodAccessor是共享的,所以,可以先從父方法訪問器中獲取父類Method對象的methodAccessor屬性對象,判讀是否為null,不為null的話,直接返回。若是methodAccessor還是為空,則是需要顯示的調用ReflectionFactory.newMethodAccessor()方法。

(3)ReflectionFactory.newMethodAccessor()調用:
通過newMethodAccessor()方法,創建一個新的MethodAccessor對象。ReflectionFactory類中有幾個與MethodAccessor相關的屬性字段:noInflation,inflationThreshold。可以先看看創建新方法訪問對象的源代碼:

    public MethodAccessor newMethodAccessor(Method method) {
        checkInitted();
        //若是noInflation設置為true,則直接調用MethodAccessorGenerator創建新對象訪問對象
        if (noInflation) {
            return new MethodAccessorGenerator().
                generateMethod(method.getDeclaringClass(),
                               method.getName(),
                               method.getParameterTypes(),
                               method.getReturnType(),
                               method.getExceptionTypes(),
                               method.getModifiers());
        } else {
            NativeMethodAccessorImpl acc =
                new NativeMethodAccessorImpl(method);
            DelegatingMethodAccessorImpl res =
                new DelegatingMethodAccessorImpl(acc);
            acc.setParent(res);
            return res;
        }
    }

從源代碼中可以看到有兩個分支:一個當設置noInflation屬性為true的時候,會直接調用MethodAccessorGenerator類直接創建新的MethodAccessor,內部的generatedMethod方法實現在稍后說明;另外一個分支中,這個分支也是常用的分支,因為noInflation默認設置為false。就來看看這個分支中是如何創建新的MethodAccessor對象的。

從源代碼中可以看到,主要的實現就是通過JNI存取器調用本地代碼庫NativeMethodAccessorImpl來創建對象訪問對象,當創建好對象后就通過DelegatingMethodAccessorImpl對象進行注入代理。

(4) NativeMethodAccessorImpl.invoke方法:
NativeMethodAccessorImpl類中會有一個字段numInvocations,用于計算使用JNI本地代碼來生成MethodAccessor對象的次數,當調用次數大于15次的時候,就會調用MethodAccessorGenerator.generateMethod生成類名字形如GeneratedMethodAccessorXXX的字節碼類文件。該類字節碼是通過DelegatingClassLoader來加載的。
為什么要設計這種機制呢?
java的反射機制運行效率是比較低的,執行Method.invoke()或Constructor.newInstance()都是通過調用native方法完成的,JDK為了提高反射運行效率,引入了一個機制叫“Inflation”,它首先通過DelegatingClassLoader去加載字節碼,再執行相關的邏輯,字節碼會緩存起來,所以第一次有加載的成本比正常執行慢3-4倍,但是后面的執行會有20倍以上的性能提升,這樣整體性能會有很大的提升。當然,這種機制也會有弊端,放在后面說。

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {   //大于15次創建GeneratedMethodAccessorXXX字節碼
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
            MethodAccessorImpl acc = (MethodAccessorImpl)
                new MethodAccessorGenerator().
                    generateMethod(method.getDeclaringClass(),
                                   method.getName(),
                                   method.getParameterTypes(),
                                   method.getReturnType(),
                                   method.getExceptionTypes(),
                                   method.getModifiers());
            parent.setDelegate(acc);
        }
        //默認調用JNI存取器來返回一個MethodAccessor對象
        return invoke0(method, obj, args);
    }

(5) MethodAccessorGenerator.generateMethod方法調用:
當noInflation設置為true,即不使用Inflation機制,那么當Method對象的methodAccessor屬性為null的時候,就會直接調用MethodAccessorGenerator.generateMethod生成代理字節類GeneratedMethodAccessorXXX,并在內存中緩存起來;還要一種就是調用JNI的NativeMethodAccessorImpl.invoke次數大于15這個閾值的時候,也會調用這個方法生成代理字節類。

// MethodAccessorGenerator.java
  private MagicAccessorImpl generate(final Class declaringClass,
                                       String name,
                                       Class[] parameterTypes,
                                       Class   returnType,
                                       Class[] checkedExceptions,
                                       int modifiers,
                                       boolean isConstructor,
                                       boolean forSerialization,
                                       Class serializationTargetClass)
    {
        ByteVector vec = ByteVectorFactory.create();
        asm = new ClassFileAssembler(vec);
        this.declaringClass = declaringClass;
        this.parameterTypes = parameterTypes;
        this.returnType = returnType;
        this.modifiers = modifiers;
        this.isConstructor = isConstructor;
        this.forSerialization = forSerialization;

        //通過ClassFileAssembler類型的asm對象,設置Class字節碼內容,
        //也就是通過代碼來構建一個符合JVM規范的Class字節碼文件
         asm.emitMagicAndVersion();//設置魔數和版本.....
        ....
        //這里有個重點,就是生成的字節碼文件的名字規則
        //“GeneratedMethodAccessor+Num” NUm為調用次數
        final String generatedName = generateName(isConstructor, forSerialization);
        
        //通過ClassDefiner.defineClass方法,指定代理類加載器來加載這個類字節碼文件
        //從而返回MethodAccessorImpl對象
        return AccessController.doPrivileged(
            new PrivilegedAction<MagicAccessorImpl>() {
                public MagicAccessorImpl run() {
                        try {
                        return (MagicAccessorImpl)
                        //bytes是構建好的class字節碼文件
                        ClassDefiner.defineClass
                                (generatedName,
                                 bytes,
                                 0,
                                 bytes.length,
                                 declaringClass.getClassLoader()).newInstance();
                        } catch (InstantiationException e) {
                            throw (InternalError)
                                new InternalError().initCause(e);
                        } catch (IllegalAccessException e) {
                            throw (InternalError)
                                new InternalError().initCause(e);
                        }
                    }
                });
    }

    //生成的class類字節碼文件的命名規則
    private static synchronized String generateName(boolean isConstructor,
                                                    boolean forSerialization)
    {
        if (isConstructor) {
            if (forSerialization) {
                int num = ++serializationConstructorSymnum;
                return "sun/reflect/GeneratedSerializationConstructorAccessor" + num;
            } else {
                int num = ++constructorSymnum;
                return "sun/reflect/GeneratedConstructorAccessor" + num;
            }
        } else {
            int num = ++methodSymnum;
            return "sun/reflect/GeneratedMethodAccessor" + num;
        }
    }

可以看到這個generate方法主要做了一下幾件事情:

  • 通過ClassFileAssembler類創建了符合JVM規范的MethodAccessor接口實現類的字節碼數組bytes。
  • 通過generateName方法,生成class類字節碼文件:若是通過構造器反射調用,字節類文件名形如:"GeneratedConstructorAccessorXXXX",若是直接調用反射方法,不通過構造器,則如:"GeneratedMethodAccessorXXX"。
  • 通過ClassDefiner.defineClass創建DelegatingClassLoader代理類加載器,用于加載上面生成的class字節碼,生成GeneratedMethodAccessorXXX類對象到內存中,以供Method.invoke方法調用。

當使用Method.invoke多次調用,生成GeneratedMethodAccessorXXX,并且使用DelegatingClassLoader加載該類的時候,通過DelegatedMethodAccessorImpl將GeneratedMethodAccessorXXX注入到目標方法的methodAccessor,所以實際也就是調用GeneratedMethodAccessorXXX.invoke()方法,通過上圖代碼可知,也就是調用目標對象的方法,和正常的方法調用一樣。

(6) DelegatingClassLoader類加載器:
當通過MethodAccessorGenerator.generateMethod生成形如"GeneratedMethodAccessorXXX"的類字節對象,要加載到內存中使用,必須要通過類加載器來操作。那么,這些類字節文件就是被DelegatingClassLoader這個類加載器加載到內存中并進行裝配使用的。

//ClassDefiner.class
    static Class defineClass(String name, byte[] bytes, int off, int len,
                             final ClassLoader parentClassLoader)
    {
        ClassLoader newLoader = AccessController.doPrivileged(
            new PrivilegedAction<ClassLoader>() {
                public ClassLoader run() {
                        return new DelegatingClassLoader(parentClassLoader);
                    }
                });
        return unsafe.defineClass(name, bytes, off, len, newLoader, null);
    }
}

加載到內存中后,就被緩存到java.lang.Class.class中的與反射有關的軟引用SoftReference給緩存起來,如:private volatile transient SoftReference<Method[]> declaredMethods。那么這個Inflation機制有哪些弊端呢?
Inflation機制提高了反射的性能,但是對于重度使用反射的項目可能存在隱患,它帶來了兩個問題:
(1)初次加載的性能損失;
(2)動態加載的字節碼導致PermGen持續增長;

當然了,解決方法也會有的,參照一下幾篇文章:
假笨說-從一起GC血案談到反射原理
使用反射代理類加載器的潛在內存使用問題
理解 JVM 如何使用 AIX 上的本機內存
sun.reflect.DelegatingClassLoader

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容