深入理解 JVM 类文件结构

1.流程

  • 创建 HelloWorld.java
    代码如下
    package jvm;

      /**
       * @author lidiqing
       * @since 2017/3/4.
       */
      public class HelloWorld {
      
          private final String text = "Hello World!";
      
          public HelloWorld() {
          }
      
          public String getText() {
              return text;
          }
      }
    
  • 编译成 HelloWorld.class

  • 使用 javap -verbose HelloWorld.class输出字节信息

    javap输出

  • 使用 winhex 查看二进制数据
    winhex输出

2.结构详解

2.1 Class 文件格式

2.1.1 概述

结构:Class 文件 == 8位字节为单位的二进制流 == 无符号数 + 表

  • u2, u4 => 无符号数,代表2个字节,u4 代表4个字节
  • xxx_info => 表,无符号数或其他表构成,整个 class 就是一张表
    • cp_info 常量表
    • field_info 字段表
    • method_info 方法表
    • attribute_info 属性表

特点:

  • 不依赖于任何操作系统
  • 数据存储使用大端字节序

文件格式:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool-1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

2.2 魔数和版本

2.2.1 概述

  • 魔数
    唯一作用:确认该文件能被虚拟机接受
    类加载过程中的验证阶段,会判断该 Magic

  • 版本号和编译器 JDK 有关

2.2.2 数据分析

魔术和版本对应数据
  • magic
    固定值:0xCAFEBABY

  • minor_version
    次版本号:0x0000

  • major_version
    主版本号:0x0034

2.2.3 表

Class 文件的版本表

编译器版本 -target参数 十六进制版本号 十进制版本号(主版本号.次版本号)
JDK 1.7.0 不带(默认 -target 1.7) 00 00 00 33 51.0
JDK 1.7.0 -target 1.6 00 00 00 32 50.0
JDK 1.8.0 不带(默认 -target 1.8) 00 00 00 34 52.0

2.3 常量池(常量表集合)

2.3.1 概述

特点:

  • Class 文件的资源仓库
  • Class 文件中和其他项目关联最多
  • Class 文件中最大的数据项目之一

主要类型:

  • 字面量,语言层面的常量,如文本字符串,final常量(如字符串 'HelloWorld!')
  • 符号引用,编译层面的常量
    • 类和接口的全限定名(如 jvm/HelloWorld
    • 字段的名称和描述符 (如 jvm/Helloworld/text:Ljava/lang/String;)
    • 方法的名称和描述符(如 java/lang/Object."<init>":()V

作用:

无论是字段表、方法表还是属性表,只要涉及到一些常量或者描述,都会有个索引指向该表的某个位置

2.3.2 数据分析

在 class 中的二进制数据

常量池的二进制数据
  • constant_pool_count => 0x19
    容量计数,常量表从 1 开始计数,所以 0x19 只有 0x18 个常量,十进制 24 个常量
    这样的话,如果因为其他地方用了索引为 0,常量为空,比如 java.lang.Object 对象是所有类的基类,父索引为 0

  • constant_pool
    对照 2.3.3 的常量池结果总表来进行分析,直接用 javap 输出 24 个常量如下

      Constant pool:
         #1 = Methodref          #5.#20         // java/lang/Object."<init>":()V
         #2 = String             #21            // Hello World!
         #3 = Fieldref           #4.#22         // jvm/HelloWorld.text:Ljava/lang/String;
         #4 = Class              #23            // jvm/HelloWorld
         #5 = Class              #24            // java/lang/Object
         #6 = Utf8               text
         #7 = Utf8               Ljava/lang/String;
         #8 = Utf8               ConstantValue
         #9 = Utf8               <init>
        #10 = Utf8               ()V
        #11 = Utf8               Code
        #12 = Utf8               LineNumberTable
        #13 = Utf8               LocalVariableTable
        #14 = Utf8               this
        #15 = Utf8               Ljvm/HelloWorld;
        #16 = Utf8               getText
        #17 = Utf8               ()Ljava/lang/String;
        #18 = Utf8               SourceFile
        #19 = Utf8               HelloWorld.java
        #20 = NameAndType        #9:#10         // "<init>":()V
        #21 = Utf8               Hello World!
        #22 = NameAndType        #6:#7          // text:Ljava/lang/String;
        #23 = Utf8               jvm/HelloWorld
        #24 = Utf8               java/lang/Object    
    

Methodref,是方法的符号引用,比如虚拟机调用 invokespecial 指令会用到,比如该类的初始化方法字节码指令中有这么一条

    1: invokespecial #1

这样会实例化 #1 的方法,而 #1 是 java.lang.Object 的初始化方法,所以该指令执行了 Object 的初始化
Fieldref,是字段的符号引用,比如虚拟机调用 putfield 指令会去解析该符号引用

    7: putfield #3    

这样的话会把该符号引用对应的变量,设置为栈帧中的值
NameAndType,字段或者方法的描述,有名称和描述符组合起来
Class,表示一个类的全限定名
String,表示一个字符串常量
Utf8,使用 utf-8 格式编码的字符串,最后其他类型的常量都会指向这些值,这是最基本的常量值

Class 文件加载后,常量池的字面量和符号引用会被存入方法区,多个线程共享。方法区又称为永久带,GC 基本不在方法区进行垃圾回收。但也会有,比如一些废弃的常量和无用的类

2.4 访问标志

2.4.1 概述

识别类或接口层次的访问信息

u2类型=2个字节=16位可用,目前只定义了8个,可见后面的访问标志表

2.4.2 数据分析

在 class 中的二进制信息

访问标志二进制数据
  • access_flags

该值为 0x21 = 0x01| 0x20

0x01 => ACC_PUBLIC
0x20 => ACC_SUPER

对照访问标志表,可知是一个普通的 public 类
使用 JDK 1.0.2 之后编译出来的类的 ACC_SUPER 均为真

2.4.3 访问标志表

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否public类型
ACC_FINAL 0x0010 是否final类型,只有类可用
ACC_SUPER 0x0020 JDK1.2 之后必须为真
ACC_INTERFACE 0x0200 标记是接口
ACC_ABSTRACT 0x400 是否abstract类型,接口或抽象类为真,其他类为假
ACC_SYNTHETIC 0x1000 标记类不是用户代码产生
ACC_ANNOTATION 0x2000 标记是注解
ACC_ENUM 0x4000 标记是枚举

2.5 索引集合

2.5.1 概述

分为三种类型用来确定整个类的继承关系,属于类的元数据

  • 类索引(this_class)
  • 父类索引(super_class)
  • 接口索引集合(interfaces)

根据规则,每个类只能有一个父类,但可以有多个接口
而更特殊的 java.lang.Object 是没有父类的,毕竟是所有类的基类,所以有且仅有 Object 类的父类索引为 0

2.5.2 数据分析

在 class 中的二进制信息

索引集合二进制数据
  • this_class => 0x0004
    这里指向常量池第4个常量,为 Class,全限定名为 jvm/HelloWorld
    使用 javap 解析到的数据

      Constant pool
          #4 = Class      #23
          #23 = Utf8      jvm/HelloWorld
    

这个索引用来表示该类的全限定名

  • super_class => 0x0005
    常量池第5个常量,为 Class,全限定名为 java/lang/Object
    使用 javap 解析到的数据

      Constant pool
          #5 = Class      #24
          #24 = Utf8      java/lang/Object    
    

这个索引用来表示该类父类的全限定名,所以该类的父类就是 java/lang/Object

  • interfaces_count => 0x0000
    接口数量为 0
    因为没有实现任何接口

  • interfaces
    因为接口数量为 0,所以后面没有接口数据

2.6 字段表集合

2.6.1 概述

范围:包含类级、实例级变量,不包括方法内部声明的局部变量

信息:

使用标记表示

  • 作用域
    • public
    • private
    • protect
  • static 实例变量还是类变量
  • final 可变性
  • volatile 并发可见性
  • transient 可被序列化

用常量表示

  • 字段数据类型(基本类型、对象、数组)
  • 字段名称

2.6.2 数据分析

字段表集合二进制数据
  • fields_count => 0x0001
    有一个字段

  • fields
    从 fields_count 知道只有一个字段,查表可以得到 field_info 各个部分为:
    access_flags => 0x0012=0x0010|0x0002
    根据字段访问标志可知,这是个 private 和 final 型属性
    name_index => 0x0006
    索引对应常量池第6个常量,名为 “text”

      Constant pool
          #6 = Utf8       text    
    

descriptor_index => 0x0007
索引对应常量池第7个常量,所以描述符为 “Ljava/lang/String;”

    Constant pool
        #6 = Utf8       Ljava/lang/String;  

attribute_count => 0x0001
所以该字段有一个属性,接下来就是详细的 attribute_info 数据,表示该字段的属性
attribute_name_index => 0x0008
索引对应常量池第8个常量,属性名称为 “constantvalue”

    Constant pool
        #8 = Utf8       constantvalue    

该属性用来通知虚拟机自动为静态变量赋值。这个发生在类的加载过程中的初始化阶段。关于类变量,初始化的时候会给默认值,但如果有 Constantvalue 属性,就会用这个属性的值来对类变量进行初始化
attribute_length => 0x00000002
constantvalue 的该值固定为 2,因为后面需要 u2 类型的数据来指向常量池,表示该 constantvalue 的值
constantvalue_index => 0x0002,索引对应常量池第2个常量,值为 “Hello World!”

    Constant pool
        #2 = String     #21
        #21 = Utf8      Hello World!    

实际上,用代码进行比较的话,上面字段对应 Java 源码为

    private final String text = "Hello World!";    

在编译成 class 文件后,我们可以解析到这样的结果

    flags: ACC_PRIVATE ACC_FINAL
    name: text
    descriptor: Ljava/lang/String;
    attributes:
        constantvalue:HelloWorld!

在 Java 中用 static 和 final 修饰的字段,即该类的常量字段。在 Java 编译阶段的常量传播优化中,如果 B 只引用了 A 的常量,编译后 A 的常量会被转化为 B 的常量,存入 B 类的常量池里。所以,常量传播优化后,使用者 B 就会拥有 A 的常量,对 A 常量的引用在 Class 文件中成为对自己的常量的引用

2.6.3 表

字段表(field_info):

类型 名称 数量
u2 access_flag 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

字段访问标志(access_flag):

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否 public
ACC_PRIVATE 0x0002 是否 private
ACC_PROTECTED 0x0004 是否 protected
ACC_STATIC 0x0008 是否 static
ACC_FINAL 0x0010 是否 final
ACC_VOLATILE 0x0040 是否 volatile
ACC_TRANSIENT 0x0080 是否 transient
ACC_SYNTHETIC 0x1000 是否 synthetic
ACC_ENUM 0x4000 是否 enum

描述符(descriptor):

标识字符 含义
B 基本类型 byte
C 基本类型 char
D 基本类型 double
F 基本类型 float
I 基本类型 int
J 基本类型 long
S 基本类型 short
Z 基本类型 boolean
V 特殊类型 void
L 对象类型,如 Ljava/lang/Object

数组每一维度用 “[” 来描述

比如

  • int[] => [I
  • string[][] => [[java/lang/String;
  • void inc => ()V
  • String toString => ()Ljava/lang/String;
  • int indexof(char[] a, int b, char[] c, int d) => ([CI[CI)I

类的字段和方法都由两部分来表示,即名称和描述符(NameAndType)

2.7 方法表集合

2.7.1 概述

特点:

  • 方法中的代码,被编译成字节码后,放在了 “Code” 属性中
  • 如果父类的方法没有被子类重写,不会出现父类方法的信息
  • 会出现编译器添加的方法,比如类构造器“<clinit>”和实例构造器“<init>
  • Java 文件中同一个类中的方法的特征签名不能一致;Class 文件特征签名可以一致,但返回值要不同

特征签名 => 一个方法中各个参数在常量池中的字段符号的引用集合,不包含返回值

2.7.2 数据分析

方法表集合二进制数据
  • methods_count => 0x0002
    表示该类有两个方法

  • methods
    分别有两个方法,都是 method_info 的结构。methods_count 紧接着就是第一个方法表
    现在只分析第一个方法表
    access_flag => 0x0001
    方法为 Public 方法
    name_index => 0x0009
    方法名称为常量表第9个常量,是 Utf8 类型,为 “<init>”,所以这个方法是类的初始化方法

      Constant pool
          #9 = Utf8       <init>  
    

descriptor_index => 0x000A
方法描述符为常量表第10个常量,也是 Utf8 类型,为 “()V”

    Constant pool
        #10 = Utf8      ()V    

attribute_count => 0x0001
属性数量为 1。从这个可知,该方法有一个属性。
基本上所有方法都至少有一个属性 “Code” 用来记录方法中编译后的字节码指令。也可以说,方法中的代码编译成字节码指令后都被存入了 “Code” 属性中了
attribute_name_index => 0x000B
常量表第11个常量,也是 Utf8 类型,就是 “Code” 属性

    Constant pool
        #11 = Utf8      Code    

attribute_length => 0x0000003D
表示接下来的 61 个字节就是 “Code” 属性表的内容了,关于这个属性表的解析在后面
使用 javap 可以得出详细的解析结果如下:

javap的解析结果

2.7.3 表

方法表(method_info):

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

方法访问标志(access_flag):

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为 public
ACC_PRIVATE 0x0002 是否为 private
ACC_PROTECTED 0x0004 是否为 protect
ACC_STATIC 0x0008 是否为 static
ACC_FINAL 0x0010 是否为 final
ACC_SYNCHRONIZED 0x0020 是否为 synchronized
ACC_BRIDGE 0x0040 是否为编译器产生的桥接方法
ACC_VARARGS 0x0080 是否接受不定参数
ACC_NATIVE 0x0100 是否为 native
ACC_ABSTRACT 0x0400 是否为 abstract
ACC_STRICTFP 0x0800 是否为 strictfp
ACC_SYNTHETIC 0x1000 是否为编译器自动产生的

2.8 属性表集合

2.8.1 概述

上面已经有用到两种属性,字段用到 constantvalue 用来表示是一个常量,方法用到了 code 来记录方法的字节码指令集合

特点:

  • 属性不要求严格按顺序排列,只要不重名
  • 属性值的结构可完全自定义,但虚拟机只取自己认识的

2.8.2 基本结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

每个属性的名称都要从常量池中拿一个 Utf8 类型的常量。属性的内容为 attribute_length 个 info 组成,每个 info 由一个字节组成。所以属性的组成是很灵活的,如何解释这些 info,有虚拟机的标准,也可以完全自定义。但虚拟机在解析的时候,只取自己标准的那部分

2.8.3 几个主要属性

2.8.3.1 Code 属性

使用在方法表中,用来表示代码编译成的字节码指令

具体的结构为

类型 名称 数量
u2 attibute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_length
u2 attributes_count 1
attribute_info attributes attributes_count

现在拿类第一个方法表的 Code 属性来进行分析

第一个方法表的 code 属性二进制数据

  • attribute_name_indexattribute_length 上面已经解析过了,为 “Code”,长度为 29。即后面的 29 个字节均为该属性的内容

  • max_stack => 0x0002
    操作数栈的最大深度,方法执行的任意时刻,操作数栈不会超过这个深度,虚拟机用这个值来分配栈帧(Stack Frame)中的操作数栈深度。这里表示该方法的操作数栈最深为 2。如果虚拟机执行的时候,有出现超出了这个深度,会抛出 Stack Overflow 的异常

  • max_locals => 0x0001
    局部变量表所需的存储空间。该值的单位为 Slot,长度不超过 32 位的数据类型,比如 byte,int 等用一个 Slot,而 double 和 long 用两个 Slot。这里表示方法局部变量的存储空间为 1 个Slot

  • code_length => 0x0000000B
    方法体内的字节码长度,这里的长度为 11,所以接下来 11 位就是字节码指令了

  • code
    方法体的字节码指令,每个指令用一个 u1 表示,即 8 位,一共可以表达 256 条指令,目前虚拟机已经定义 200 多条了。code 指令可以分成以下几种类型。其中异常指令只记录显示抛出(throw)的异常,而像 try...catch 的异常则有属性表之异常表来处理

    • 加载和存储
    • 运算
    • 类型转换
    • 对象的创建和访问
    • 操作数栈管理
    • 控制转移
    • 方法调用和返回
    • 异常
    • 同步指令
  • attributes_count => 0x00000002
    表示后面还有两个属性,对照表可知为 LineNumberTable 和 LocalVariableTable。这两个属性都不是必须的,主要用来描述源码和字节码之间的一些关系

因为该方法比较简单,没有异常等。如果有异常,Code 属性中还会有异常表 Exception table

2.8.3.2 LineNumberTable 属性

用处

  • 描述 Java 源码和字节码行号的对应关系
  • 可以使用 javac -g:none 或 -g:lines 选项来取消或显示。默认显示
  • 用来实现断点调试(如果没有的话无法断点)

LineNumberTable 具体的结构为

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

line_number_info 由 start_pc 和 line_number 组成,分别表示字节码行号和源码行号

现在拿上面的方法的 LineNumberTable 属性数据来进行分析

第一个方法的 Code 属性的 LineNumberTable 的二进制数据
  • attribute_name_index => 0x000C
    属性名称,指向常量池第 12 个常量,可以得知就是 “LineNumberTable”

      Constant pool
          #12 = Utf8      LineNumberTable 
    
  • attribute_length => 0x0000000E
    属性的长度为 15,所以接下来 15 个字节都是 LineNumberTable 属性的值

  • line_number_table_length => 0x0003
    表示接下来有 3 个的 line_number_info 类型,用来表示字节码和源码的对应关系。有可能多个字节指令才会对应一条源码

  • line_number_table
    现在就分析第一个 line_number_info 的信息
    start_pc => 0x0000,字节码第0行
    PC 的全称为 Program Counter,即程序计数,对应程序计数器的值,表示在字节码指令中的偏移量。因为要保证线程切换后能回到正确的位置,所以每个线程都会有一个程序计数器
    line_number => 0x000B,源码第11行
    结合 start_pc 可知,所以 0:aload_0 指令对应的源码为 public HelloWorld() {

使用 javap 解析到方法 “<init>” 的完整 LineNumberTable 表如下

javap解析到的结果

有了这些class文件中有了这些信息,我们就可以进行断点调试了。所以断点调试是依赖于方法表中是否有设置 LineNumberTable 属性

2.8.3.3 LocalVariableTable 属性

用处

  • 描述栈帧局部变量表中的变量和 Java 源码中对应的变量之间的关系
  • 可使用 -g:none 或 -g:vars 来取消或者生产这项信息
  • 没有该属性,当其他人引用这个方法,所有的参数名称丢失,IDE 会使用 arg0、arg1 等占位符代替

LocalVariableTable 具体的结构为

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

local_variable_info 的结构为

类型 名称 数量
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1

用第一个方法表的 code 属性的 LocalVariableTable 属性数据来分析

第一个方法表的 code 属性的 LocalVariableTable 属性的二进制数据
  • attribute_name_index => 0x000D
    指向常量池第 13 个常量,可以得知就是 “LocalVariableTable”

  • attribute_length => 0x0000000C
    属性的长度为 12,所以接下来 12 个字节都是 LocalVariableTable 属性的值

  • local_variable_table_length => 0x0001
    表示接下来有一个 local_variable_info 用来表示栈帧内局部变量表和源码的关系

  • local_varibale_info
    start_pc => 0x0000
    该局部变量的生命周期开始的字节码偏移量
    length => 0x000B
    该局部变量的生命周期作用范围的长度
    所以该局部变量作用在 0 -> 11 条字节码之间,可知为整个方法的字节码指令集范围内
    name_index => 0x000E
    指向常量池第 14 个常量,为 “this”,所以这个局部变量是 this 指针,指向使用该方法的当前实例
    descriptor_index => 0x000F
    指向常量池第 15 个常量,为 “Ljvm/HelloWorld”,所以这个 this 指针代表的就是类 HelloWorld 的实例
    index => 0x0000
    表示该局部变量在栈帧局部变量表中 Slot 的位置,可知为第一个 Slot

2.8.3.4 ConstantValue 属性

该属性在 2.6.2 对字段 “text” 的解析中已经提到,具体的作用是通知虚拟机初始化静态变量

目前类变量的初始化有两种方式

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

推荐阅读更多精彩内容