用Java實(shí)現(xiàn)JVM(一):剛好夠運(yùn)行 HelloWorld

1. 前言

沒錯(cuò)這又是一篇介紹 JVM 的文章,這類文章網(wǎng)上已經(jīng)很多叛拷,不同角度舌厨、不同深度、不同廣度忿薇,也都不乏優(yōu)秀的裙椭。為什么還要來一篇?首先對于我來說煌恢,我正在學(xué)習(xí) Java骇陈,了解JVM的實(shí)現(xiàn)對學(xué)習(xí)Java當(dāng)然很有必要,但我已經(jīng)做了多年C++開發(fā)瑰抵,就算我用C++實(shí)現(xiàn)一個(gè)JVM你雌,我還是個(gè)C++碼農(nóng),而用 Java實(shí)現(xiàn)二汛,即能學(xué)習(xí) Java 語法婿崭,又能理解 JVM,一舉兩得肴颊。其次氓栈,作為讀者,hotspot或者其他成熟JVM實(shí)現(xiàn)的源碼讀起來并不輕松婿着,特別是對沒有C/C++經(jīng)驗(yàn)的人來說授瘦,如果只是想快速了解JVM的工作原理醋界,并且希望運(yùn)行和調(diào)試一下JVM的代碼來加深理解,那么這篇文章可能更合適提完。

我將用Java實(shí)現(xiàn)一個(gè)JAVA虛擬機(jī)(源碼在這下載形纺,加 Star 亦可),一開始它會(huì)非常簡單徒欣,實(shí)際上簡單得只夠運(yùn)行HelloWorld逐样。雖然簡單,但是我盡量讓其符合 JVM 標(biāo)準(zhǔn)打肝,目前主要參考依據(jù)是《Java虛擬機(jī)規(guī)范 (Java SE 7 中文版)》脂新。

2. 準(zhǔn)備

先寫一個(gè)HelloWorld,代碼如下:

package org.caoym;

public class HelloWorld {
    public static void main(String[] args){
        System.out.println("Hello World");
    }
}

我期望所實(shí)現(xiàn)的虛擬機(jī)(姑且命名為JJvm吧)粗梭,可以通過以下命令運(yùn)行:

$ java org.caoym.jjvm.JJvm org.caoym.HelloWorld
Hello World

接下來我們開始實(shí)現(xiàn)JJvm争便,下面是其入口代碼,后面將逐步介紹:

public void run(String[] args) throws Exception {
    Env env = new Env(this);
    //加載初始類
    JvmClass clazz = findClass(initialClass);
    //找到入口方法
    JvmMethod method = clazz.getMethod(
            "main",
            "([Ljava/lang/String;)V",
            (int)(AccessFlags.JVM_ACC_STATIC|AccessFlags.JVM_ACC_PUBLIC));
    //執(zhí)行入口方法
    method.call(env, clazz, (Object[]) args);
}

3. 加載初始類

我們將包含 main 入口的類稱為初始類楼吃,JJvm 首先需要根據(jù)org.caoym.HelloWorld類名始花,找到.class 文件,然后加載并解析孩锡、校驗(yàn)字節(jié)碼枫慷,這些步驟正是 ClassLoader(類加載器)做的事情疼约。HelloWorld.class內(nèi)容大致如下:

cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 164c 6f72 672f 6361
6f79 6d2f 4865 6c6c 6f57 6f72 6c64 3b01
0004 6d61 696e 0100 1628 5b4c 6a61 7661
...

沒錯(cuò)是緊湊的二進(jìn)制格式,需要按規(guī)范解析蹦锋,不過我并不打算自己寫解析程序炕置,可以直接用com.sun.tools.classfile.ClassFile荣挨,這也是用JAVA寫好處。下面是HelloWorld.class解析后的內(nèi)容(通過javap -v HelloWorld.class輸出):

public class org.caoym.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // org/caoym/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/caoym/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               org/caoym/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public org.caoym.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/caoym/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

可以看到HelloWorld.class 文件中主要包含幾部分:

  1. 常量池(Constant pool)

    常量池中記錄了當(dāng)前類中用到的常量朴摊,包括方法名默垄、類名、字符串常量等甚纲,如:#3 = String #23, #3為此常量的索引口锭,字節(jié)碼執(zhí)行時(shí)通過此索引獲取此常量,String為常量類型, 還可以是Methodref (方法引用)介杆、Fieldref(屬性引用)等鹃操。

  2. 方法定義

    此處定義了方法的訪問方式(如 PUBLIC、STATIC)春哨、字節(jié)碼等荆隘,關(guān)于字節(jié)碼的執(zhí)行方式將在后面介紹。

以下為類加載器的部分代碼實(shí)現(xiàn):

/**
 * 虛擬機(jī)的引導(dǎo)類加載器
 */
public class JvmClassLoader {
    // ... 此處省略部分代碼    
    public JvmClass loadClass(String className) throws ClassNotFoundException{
        String fileName = classPath + "/"+className.replace(".", "/")+".class";
        Path path = Paths.get(fileName);
        //如果文件存在赴背,加載文件字節(jié)碼
        //否則嘗試通過虛擬機(jī)宿主加載指定類椰拒,并將加載后的類當(dāng)做 native 類
        if(Files.exists(path)){
             return JvmOpcodeClass.read(path);
        }else{
            return new JvmNativeClass(Class.forName(className.replace("/",".")));
        }
    }
}

類加載器可以加載兩種形式的類:JvmOpcodeClassJvmNativeClass晶渠,均繼承自JvmClass。其中JvmOpcodeClass 表示用戶定義的類燃观,通過字節(jié)碼執(zhí)行乱陡,也就是這個(gè)例子中的HelloWorldJvmNativeClass表示JVM 提供的原生類仪壮,可直接調(diào)用原生類執(zhí)行憨颠,比如 java.lang.System。這里把所有非項(xiàng)目內(nèi)的類积锅,都當(dāng)做原始類處理爽彤,以便簡化虛擬機(jī)的實(shí)現(xiàn)。

4. 找到入口方法

JVM規(guī)定入口是static public void main(String[])缚陷,為了能夠查找指定類的方法适篙,JvmOpcodeClassJvmNativeClass都需要提供getMethod方法, 當(dāng)然 main 方法肯定存在JvmOpcodeClass中:

public class JvmOpcodeClass implements JvmClass{

    private JvmOpcodeClass(ClassFile classFile) throws ConstantPoolException {
        this.classFile = classFile;
        for (Method method : classFile.methods) {
            String name = method.getName(classFile.constant_pool);
            String desc = method.descriptor.getValue(classFile.constant_pool);
            methods.put(name+":"+desc, new JvmOpcodeMethod(classFile, method));
        }
    }

    @Override
    public JvmMethod getMethod(String name, String desc, int flags) throws NoSuchMethodException {
        JvmOpcodeMethod method = methods.get(name+":"+desc);
        //... check method != null
        return method;
    }
}

5. 執(zhí)行非 Native(字節(jié)碼定義的)方法

下圖為以HelloWorldmain()方法的執(zhí)行過程:

下面將詳細(xì)說明。

5.1. 虛擬機(jī)棧

每一個(gè)虛擬機(jī)線程都有自己私有的虛擬機(jī)棧(Java Virtual Machine Stack)箫爷,用于存儲(chǔ)棧幀嚷节。每一次方法調(diào)用,即產(chǎn)生一個(gè)新的棧幀硫痰,并推入棧頂,函數(shù)返回后窜护,此棧幀從棧頂推出。以下為 JJvm中虛擬機(jī)棧的部分代碼:

public class Stack {
    //創(chuàng)建新棧并推入棧頂缓屠,用于 native 方法調(diào)用
    public StackFrame newFrame() {
        StackFrame frame = new StackFrame(null, null, 0, 0);
        frames.push(frame, 1);
        return frame;
    }
        //創(chuàng)建新棧并推入棧頂,用于 opcode 方法調(diào)用
    public StackFrame newFrame(ConstantPool constantPool,
                               Opcode[] opcodes,
                               int variables,
                               int stackSize) {
        StackFrame frame = new StackFrame(constantPool, opcodes, variables, stackSize);
        frames.push(frame, 1);
        return frame;
    }
    public StackFrame currentFrame(){...} //獲取當(dāng)前正在執(zhí)行的棧幀
    public StackFrame popFrame(){...} //從棧頂退出一個(gè)棧幀
}

5.2. 棧幀

棧幀用于保存當(dāng)前函數(shù)調(diào)用的上下文信息,以下為 JJvm 中棧幀的部分代碼:

public class StackFrame {  
    private int pc=0;  //程序計(jì)數(shù)器
    public StackFrame(ConstantPool constantPool,
                      Opcode[] opcodes,
                      int variables,
                      int stackSize) {
        this.constantPool = constantPool;               //常量池
        this.opcodes = opcodes;                         //當(dāng)前方法的字節(jié)碼
        this.operandStack = new SlotsStack(stackSize);  //操作數(shù)棧
        this.localVariables = new Slots(variables);     //局部變量表
    }
    public Slots<Object> getLocalVariables() {...}      //局部變量表
    public SlotsStack<Object> getOperandStack() {...}   //操作數(shù)棧
    public ConstantPool getConstantPool() {...}         //常量池
    public void setPC(int pc) {...}                     //設(shè)置程序計(jì)數(shù)器
    //設(shè)置方法返回值业踏,一旦設(shè)置,此幀需要被退出棧頂涧卵,并將返回值推入上一個(gè)棧幀的操作數(shù)棧
    public void setReturn(Object returnVal, String returnType) {...}  
    public Object getReturn() {...}                     //獲取當(dāng)前方法返回值
    public String getReturnType() {...}                 //獲取當(dāng)前方法返回值類型
    public boolean isReturned() {...}                   //判斷當(dāng)前方法是否已經(jīng)返回
    public int getPC() {...}                            //獲取程序計(jì)數(shù)器
    public int increasePC() {...}                       //遞增程序計(jì)數(shù)器
    public Opcode[] getOpcodes() {...}                  //當(dāng)前方法的字節(jié)碼
}

說明:

  • 局部變量表

    保存當(dāng)前方法的局部變量勤家、實(shí)例的this指針和方法的實(shí)參。函數(shù)執(zhí)行過程中柳恐,部分字節(jié)碼會(huì)操作或讀取局部變量表伐脖。局部變量表的長度由編譯期決定热幔。

  • 常量池

    引用當(dāng)前類的常量池。

  • 字節(jié)碼內(nèi)容

    以數(shù)組形式保存的當(dāng)期方法的字節(jié)碼讼庇。

  • 程序計(jì)數(shù)器

    記錄當(dāng)前真在執(zhí)行的字節(jié)碼的位置绎巨。

  • 操作數(shù)棧

    操作數(shù)棧用來準(zhǔn)備字節(jié)碼調(diào)用時(shí)的參數(shù)并接收其返回結(jié)果,操作數(shù)棧的長度由編譯期決定蠕啄。

5.3. 方法調(diào)用

方法調(diào)用的過程大致如下:

  1. 新建棧幀场勤,并推入虛擬機(jī)棧。
  2. 將實(shí)例的this和當(dāng)前方法的實(shí)參設(shè)置到棧幀的局部變量表中歼跟。
  3. 解釋執(zhí)行方法的字節(jié)碼和媳。

以下為 JJvm 中的部分代碼:

public class JvmOpcodeMethod implements JvmMethod {
    public void call(Env env, Object thiz, Object ...args) throws Exception {
        // 每次方法調(diào)用都產(chǎn)生一個(gè)新的棧幀,當(dāng)前方法返回后哈街,將其棧幀設(shè)置為已返回留瞳,BytecodeInterpreter.run會(huì)在檢查到返回后,將棧幀推
        // 出棧骚秦,并將返回值(如果有)推入上一個(gè)棧幀的操作數(shù)棧
        StackFrame frame = env.getStack().newFrame(
                classFile.constant_pool,
                opcodes,
                codeAttribute.max_locals,
                codeAttribute.max_stack);

        // Java 虛擬機(jī)使用局部變量表來完成方法調(diào)用時(shí)的參數(shù)傳遞她倘,當(dāng)一個(gè)方法被調(diào)用的時(shí)候,它的 參數(shù)將會(huì)傳遞至從 0 開始的連續(xù)的局部變量表位置
        // 上作箍。特別地硬梁,當(dāng)一個(gè)實(shí)例方法被調(diào)用的時(shí)候, 第 0 個(gè)局部變量一定是用來存儲(chǔ)被調(diào)用的實(shí)例方法所在的對象的引用(即 Java 語言中的“this”
        // 關(guān)鍵字)蒙揣。后續(xù)的其他參數(shù)將會(huì)傳遞至從 1 開始的連續(xù)的局部變量表位置上靶溜。
        Slots<Object> locals = frame.getLocalVariables();
        int pos = 0;
        if(!method.access_flags.is(AccessFlags.ACC_STATIC)){
            locals.set(0, thiz, 1);
            pos++;
        }
        for (Object arg : args) {
            locals.set(pos++, arg, 1);
        }
        //解釋執(zhí)行字節(jié)碼
        BytecodeInterpreter.run(env);
    }
}

5.4. 解釋執(zhí)行字節(jié)碼

字節(jié)碼的執(zhí)行過程如下:

  1. 獲取棧頂?shù)牡谝粋€(gè)棧幀。
  2. 獲取當(dāng)前棧的程序計(jì)數(shù)器(PC懒震,其默認(rèn)值為0)指向的字節(jié)碼,程序計(jì)數(shù)器+1嗤详。
  3. 執(zhí)行上一步獲取的字節(jié)碼个扰,推出操作數(shù)棧的元素,作為其參數(shù)葱色,執(zhí)行字節(jié)碼递宅。
  4. 字節(jié)碼返回的值(如果有),重新推入操作數(shù)棧苍狰。
  5. 如果操作數(shù)為return等办龄,則設(shè)置棧幀為已返回狀態(tài)。
  6. 如果操作數(shù)為invokevirtual等嵌套調(diào)用其他方法淋昭,則創(chuàng)建新的棧幀俐填,并回到第一步。
  7. 如果棧幀已設(shè)置為返回翔忽,則將返回值推入上一個(gè)棧幀的操作數(shù)棧英融,并推出當(dāng)前棧盏檐。
  8. 重復(fù)執(zhí)行1~7,直到虛擬機(jī)棧為空驶悟。

以下為JJvm中解釋執(zhí)行字節(jié)碼的部分代碼:

public class BytecodeInterpreter {
    
    //執(zhí)行字節(jié)碼
    public static void run(Env env) throws Exception {
        //只需要最外層調(diào)用執(zhí)行棧上操作
        if(env.getStack().isRunning()) return;
        
        StackFrame frame;
        Stack stack = env.getStack();
        stack.setRunning(true);

        while ((frame = stack.currentFrame()) != null){
            //如果棧幀被設(shè)置為返回胡野,則將其返回值推入上一個(gè)棧幀的操作數(shù)棧
            if(frame.isReturned()){
                //原先此處有 bug,多謝 @樂浩beyond 指出
                StackFrame oldFrame = frame;
                stack.popFrame();
                frame = stack.currentFrame();
                //如果有返回值痕鳍,則將返回值推入上一個(gè)棧幀的操作數(shù)棧硫豆。
                if(frame != null && !"void".equals(oldFrame.getReturnType())){
                    frame.getOperandStack().push(oldFrame.getReturn());
                }
                continue;
            }
            Opcode[] codes = frame.getOpcodes();
            int pc = frame.increasePC();
            codes[pc].call(env, frame);
        }
    }
    // opcode 的實(shí)現(xiàn)
    static {
        //return: 從當(dāng)前方法返回 void。
        OPCODES[Constants.RETURN] = (Env env, StackFrame frame, byte[] operands)->{
            frame.setReturn(null, "void");
        };

        //getstatic: 獲取對象的靜態(tài)字段值
        OPCODES[Constants.GETSTATIC] = (Env env, StackFrame frame, byte[] operands)->{
            int arg = (operands[0]<<4)|operands[1];
            ConstantPool.CONSTANT_Fieldref_info info
                    = (ConstantPool.CONSTANT_Fieldref_info)frame.getConstantPool().get(arg);
            //靜態(tài)字段所在的類
            JvmClass clazz = env.getVm().findClass(info.getClassName());
            //靜態(tài)字段的值
            Object value = clazz.getField(
                    info.getNameAndTypeInfo().getName(),
                    info.getNameAndTypeInfo().getType(),
                    AccessFlags.ACC_STATIC
                    );

            frame.getOperandStack().push(value, 1);
        };

        //ldc: 將 int笼呆,float 或 String 型常量值從常量池中推送至棧頂
        OPCODES[Constants.LDC] = (Env env, StackFrame frame, byte[] operands)->{
            int arg = operands[0];
            ConstantPool.CPInfo info = frame.getConstantPool().get(arg);
            frame.getOperandStack().push(asObject(info), 1);
        };

        //invokevirtual: 調(diào)用實(shí)例方法
        OPCODES[Constants.INVOKEVIRTUAL] = (Env env, StackFrame frame, byte[] operands)->{
            int arg = (operands[0]<<4)|operands[1];

            ConstantPool.CONSTANT_Methodref_info info
                    = (ConstantPool.CONSTANT_Methodref_info)frame.getConstantPool().get(arg);

            String className = info.getClassName();
            String name = info.getNameAndTypeInfo().getName();
            String type = info.getNameAndTypeInfo().getType();

            JvmClass clazz  = env.getVm().findClass(className);
            JvmMethod method = clazz.getMethod(name, type, 0);

            //從操作數(shù)棧中推出方法的參數(shù)
            Object args[] = frame.getOperandStack().dumpAll();
            method.call(env, args[0], Arrays.copyOfRange(args,1, args.length));
        };
        // ... 以下省略
    }
}

6. 執(zhí)行 Native 方法

Native方法的調(diào)用要更簡單一些熊响,只需調(diào)用已存在的實(shí)現(xiàn)即可,代碼如下:

public class JvmNativeMethod implements JvmMethod {

    private Method method;
    @Override
    public void call(Env env, Object thiz, Object... args) throws Exception {
        StackFrame frame = env.getStack().newFrame();
        Object res = method.invoke(thiz, args);
        //設(shè)置為已返回
        frame.setReturn(res, method.getReturnType().getName());
    }
}

7. 結(jié)束

到目前為止抄邀,我們的“剛好夠運(yùn)行 HelloWorld”的 JVM 已經(jīng)完成耘眨,完整代碼可在這里下載。當(dāng)然這個(gè)JVM 并不完整境肾,缺少很多內(nèi)容剔难,如類和實(shí)例的初始化、多線程問題奥喻、反射偶宫、GC 等等。我爭取逐步完善JJvm环鲤,并奉上更多文章纯趋。

下一篇:用Java實(shí)現(xiàn)JVM(二):支持接口、類和對象

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冷离,一起剝皮案震驚了整個(gè)濱河市吵冒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌西剥,老刑警劉巖痹栖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瞭空,居然都是意外死亡揪阿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門咆畏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來南捂,“玉大人,你說我怎么就攤上這事旧找∧缃。” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵钦讳,是天一觀的道長矿瘦。 經(jīng)常有香客問我枕面,道長,這世上最難降的妖魔是什么缚去? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任潮秘,我火速辦了婚禮,結(jié)果婚禮上易结,老公的妹妹穿的比我還像新娘枕荞。我一直安慰自己,他們只是感情好搞动,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布躏精。 她就那樣靜靜地躺著,像睡著了一般鹦肿。 火紅的嫁衣襯著肌膚如雪矗烛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天箩溃,我揣著相機(jī)與錄音瞭吃,去河邊找鬼。 笑死涣旨,一個(gè)胖子當(dāng)著我的面吹牛歪架,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播霹陡,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼和蚪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烹棉?” 一聲冷哼從身側(cè)響起攒霹,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浆洗,沒想到半個(gè)月后剔蹋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辅髓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了少梁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洛口。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖凯沪,靈堂內(nèi)的尸體忽然破棺而出第焰,到底是詐尸還是另有隱情,我是刑警寧澤妨马,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布挺举,位于F島的核電站杀赢,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏湘纵。R本人自食惡果不足惜脂崔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望梧喷。 院中可真熱鬧砌左,春花似錦、人聲如沸铺敌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽偿凭。三九已至产弹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弯囊,已是汗流浹背痰哨。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留常挚,地道東北人作谭。 一個(gè)月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像奄毡,于是被迫代替她去往敵國和親折欠。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容