【JVM】类文件结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。

使用命令javac将.java 文件编译为.class文件
使用命令javap输出.class文件的字节码内容

Class文件格式采用类似于C语言结构体的伪结构,只有两种数据类型:无符号数和表。

  • 无符号数:属于基本的数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • :由多个无符号数或其他表作为数据项构成的复合数据类型。所有表都以_info结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,由下表中的数据项构成。
名称 类型 数量
magic u4 1
minor_version u2 1
major_version u2 1
constant_pool_count u2 1
constant_pool cp_info constant_pool_count - 1
access_flags u2 1
this_class u2 1
super_class u2 1
interfaces_count u2 1
interfaces u2 interfaces_count
fields_count u2 1
fields field_info fields_count
methods_count u2 1
methods method_info methods_count
attributes_count u2 1
attributes attribute_info attributes_count

学习类文件结构,就是要明白上表中各个数据项的具体含义。


魔数与Class文件版本

  • 魔数

  • 即上表中的magic,4个字节,在Class文件的开头。

  • 作用是确定该文件是否为一个被虚拟机接受的Class文件。使用魔数而不是后缀名的方式是基于安全的考虑:文件扩展名可以任意更改。

  • 正常的Class文件的魔数为0xCAFEBABEcafe babe ,咖啡宝贝。

    Java.png

  • 版本号

  • 即上表中的major_versionminor_version:Class文件的第5、6个字节是次版本号minor_version,第7、8个字节是主版本号major_version

  • 高版本的JDK可以向下兼容,但低版本的JDK不向上兼容,虚拟机会拒绝执行。


常量池

常量池是Class文件中的资源仓库,它是Class文件结构中与其他数据项关联最多的数据类型,也是占用Class文件空间最大的数据项之一。

  • 常量池容器计数值

  • 即上表中的constant_pool_count,2个字节

  • 常量池中的常量的数量是不固定的,所以需要constant_pool_count来表示常量池中常量的数量。

  • 该容器计数从1开始(而不是Java语言习惯中从0开始),比如constant_pool_count的值为16,则说明常量池中有15项常量,索引值范围为1-15。将第0项常量空出来的目的是为了满足后面某些指向常量池的索引值的数据在特定情况下表达“不引用任何一个常量池项”的含义。

  • 常量池

  • 从概念上理解,常量池 = constant_pool[constant_pool_count - 1]

  • 常量池中主要存放两大类常量:字面量和符号引用。
    ①字面量:接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
    ②符号引用:属于编译原理方面的概念,主要包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。Java代码在经过javac命令编译之后,生成的Class文件中并不会保存各个方法、字段的最终内存布局信息,必须等到虚拟机运行时,从常量池中获得对应的符号引用,再通过这些符号引用经过类创建或运行时解析、翻译等才能得到具体的内存地址。

  • 常量池中共有14中类型的常量,每一个常量(项)都有自己的表结构,但这14种表的有一个共同特点:表开始的第一位是一个u1类型的标志位tag,代表这个常量属于哪种常量类型。常量池中的常量类型见下表。

常量类型 表类型 tag取值
UTF-8编码的字符串 CANSTANT_Utf8_info 1
整型字面量 CANSTANT_Integer_info 3
浮点型字面量 CANSTANT_Float_info 4
长整型字面量 CANSTANT_Long_info 5
双精度浮点型字面量 CANSTANT_Double_info 6
类或接口的符号引用 CANSTANT_Class_info 7
字符串类型字面量 CANSTANT_String_info 8
字段的符号引用 CANSTANT_Fieldref_info 9
类中方法的符号引用 CANSTANT_Methodref_info 10
接口中方法的符号引用 CANSTANT_IntefaceMethodref_info 11
字段或方法的部分符号引用 CANSTANT_NameAndType_info 12
方法句柄 CANSTANT_MethodHandle_info 15
方法类型 CANSTANT_MethodType_info 16
动态方法调用点 CANSTANT_InvokeDynamic_info 18

访问标志

即Class文件数据项表中的access_flags,用于识别类或者接口层次的访问信息:该是类还是接口,是否为public,是否定义为abstract;如果是类,是否声明为final;该类是否由用户代码产生,是否为注解,是否为枚举等信息。


类索引、父类索引和接口索引

Class文件根据这三项数据来确定这个类的继承关系。

  • 类索引
    即Class文件数据项表中的this_class:用于确定这个类的全限定名。
  • 父索引
    即Class文件数据项表中的super_class:用于确定这个类的父类的全限定名。由于Java是单继承,只有一个父类。所有类(除了java.lang.Object)都有父类。
  • 接口索引
    即Class文件数据项表中的interfaces_countinterfacesinterfaces_count表示接口索引表的容量,如果该值为0,接口索引表不占用任何字节。

其中,类索引和父索引各自指向一个常量池中的类型为CANSTANT_Class_info的常量,通过该常量的索引值再定位到常量池中的CANSTANT_Utf8_info类型的常量表示的全限定名字符串。


字段表集合

从概念上理解,字段表集合 = fields[fields_count],用于描述该Class文件对应的代码中声明的变量:包括类级变量以及实例变量,但不包括方法内声明的局部变量。
对于一个字段,描述信息主要有:作用域(public、private、protected),是实例变量还是类变量(有无static修饰),可变性(final),并发可见性(volatile),可否被序列化(transient),数据类型(基本类型、对象、数组),名称。这些信息中各个修饰符都是布尔值(要么有,要么没有),用标志位表示,而名称、数据类型则引用常量池中的常量来描述。
每一个字段会对应一个字段表,字段表的最终结构如下。

名称 类型 数量
access_flag u2 1
name_index u2 1
descriptor_index u2 1
attributes_count u2 1
attributes attribute_info attributes_count
  • access_flag
    该字段的修饰符信息:是否public、private、protected、static、final、volatile、transient、enum等。
  • name_index
    该字段的简单名称,是对常量池的常量引用。比如在代码中定义private String name,则name字段的简单名称就是name,但是这个name这个字面量是在常量池中的,name_index存储的是对常量池中该常量项的引用。
  • descriptor_index
    该字段的描述符,描述字段的数据类型。
  • attributes和attributes
    该字段的属性表,见下文。

字段表集合中不会列出从超类或父接口中继承而来的字段,但有可能会列出原本Java代码中不存在的字段,比如在内部类中为了保持对外类的访问性,在编译Class文件的时候会自动添加外部类的实例字段。


方法表集合

从概念上理解,方法表集合 = methods[methods_count]。每一个方法对应一个方法表method_info。方法表与字段表的结构一致,只是具体的信息项不同。

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

其中,访问信息access_flag包括是否public、private、protected、static、final、synchronized、native、abstract、strictfp、是否接受不定参数、是否由编译器自动产生等。
其中,方法描述符descriptor_index中描述了方法的参数列表(数量、类型、顺序)和返回值。

而方法体中的代码,经过编译器编译成字节码指令后,存储在方法属性表attributes[attributes_count]中。

如果子类没有重写父类的方法,则子类的方法表集合中不会出现来自父类的方法信息,但可能会出现编译器自动添加的方法,如类构造器<clinit>方法和实例构造器<init>方法。


属性表集合

在Class文件、字段表、方法表中都可以包含自己的属性表集合,以此描述某些场景专有的信息。

1、Code属性

Java程序方法体中的代码经过javac编译器编译之后,最终变为字节码指令存储在Code属性表中,即Code属性表是方法表的一部分。但并非所有的方法表都存在该属性,比如接口或者抽象类中的抽象方法就不存在Code属性表。Code属性表的结构如下。

名称 类型 数量
attribute_name_index u2 1
attribute_length u4 1
max_stack u2 1
max_locals u2 1
code_length u4 1
code u1 code_length
exception_table_length u2 1
exception_table exception_info exception_length
attributes_count u2 1
attributes attribute_info attributes_count
  • attribute_name_index和attribute_length
    attribute_name_index表示该属性表的名称,即Code,是指向常量池中类型为CANSTANT_Utf8_info的常量的索引。
    attribute_length表示该属性值的长度。

  • max_stack
    代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配帧栈中的操作深度。

  • max_locals
    代表了局部变量表所需要的存储空间。max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用一个Slot,double和long这两种64位的数据类型则需要两个Slot。方法参数(包括实例方法中的隐藏参数this)、显式异常处理器的参数(try-catch中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占用的Slot可以被其他局部变量所使用。Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

  • code_length和code

  • code_length代表字节码长度,即字节码指令的个数,也就是code的长度。虽然是u4类型(2^32),但虚拟机规范中明确规定一个方法不允许超过65535条字节码指令,即它只使用了u2的长度,超过这个长度,Javac编译器会拒绝编译。

  • code中存储的是字节码指令的一系列字节流。对于字节码指令,每个指令都是单字节(u1类型)。当虚拟机读取到code中的一个字节码时,就可以找出对应的这个字节码对应的指令,并且可以知道这个指令后面是否需要跟随参数以及参数应当如何理解。因为字节码指令是用1个字节(8位,2^8=256)来表示,所以一共可以表示256条指令。目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含义。

  • exception_table_length和exception_table
    显式异常处理表集合,对于Code属性来说并不是必须的,表示的是try-catch中的异常信息描述。

Code属性是Class文件中最重要的一个属性,如果把一个Java程序的信息分为代码和元数据两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

2、Exceptions属性

属于方法表中的一部分。作用是列出出方法中可能抛出的受检查异常,也就是方法描述时在throws 关键字后面列出的异常。

3、LineNumberTable属性

属于Code属性的一部分。用于描述Java源代码行号与字节码行号之间的对应关系,它并不是运行时必须的属性,默认生成,可以在javac命令中使用 -g:none-g:lines属性取消生成该信息。不生成该信息对程序运行的影响:当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

4、LocalVariableTable属性

属于Code属性的一部分。用于描述帧栈中局部变量表中的变量与Java源代码定义的变量之间的关系,不是运行时必须的属性,默认生成,可以在javac命令中使用 -g:none-g:vars属性取消生成该信息。不生成该信息的影响:当其他人引用该方法时,所有的参数名称都会丢失,IDE会使用类似arg0、arg1等占位符代替原有的参数名,给代码编写带来不便。

5、ConstantValue属性

属于字段表中的一部分。作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用该属性。虚拟机对类变量和实例变量的赋值方式和时机有所不同。对于实例变量的赋值是在实例构造器<init>方法中进行的。对于类变量,如果是同时有static和final修饰的基本类型数据或String类型数据,则会生成ConstantValue属性来进行初始化,否则在<clinit>方法中进行初始化。


实例分析

定义一个父类Animal,两个接口Eat、Sleep,一个要分析的Rabbit类。

package constructor;
public class Animal {

    protected String weight;

    public String getWeight() {
        return weight;
    }

    public void setWeight(String weight) {
        this.weight = weight;
    }
}
package constructor;
public interface Sleep {
    void sleep();
}
package constructor;
public interface Eat {
    void eat();
}
package constructor;
public class Rabbit extends Animal implements Eat, Sleep{

    private String nickName;

    private int age;

    public static final boolean isCute = true;

    @Override
    public void eat() {
        System.out.println("I eat grass");
    }

    @Override
    public void sleep() {
        System.out.println("I sleep well");
    }

    public String play(int temperature){
        if(temperature > 10){
            return "I want to play outside";
        }else {
            return "I want to stay at home";
        }
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

使用命令javac *.java编译所有源文件,生成class文件。(如果仅仅单独编译Rabbit.java文件会提示找不到类Sleep、Eat和Animal等)。
使用javap -verbose Rabbit.class命令查看Rabbit.class文件的字节码内容。

Classfile /Users/yue/Documents/workspace/idea/datacenter/src/test/constructor/Rabbit.class
  Last modified 2017-7-9; size 1092 bytes
  MD5 checksum 64e6283bb3c70e9c41fb2f72e09aae13
  Compiled from "Rabbit.java"
public class constructor.Rabbit extends constructor.Animal implements constructor.Eat,constructor.Sleep
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #11.#41        // constructor/Animal."<init>":()V
   #2 = Fieldref           #42.#43        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #44            // I eat grass
   #4 = Methodref          #45.#46        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #47            // I sleep well
   #6 = String             #48            // I want to play outside
   #7 = String             #49            // I want to stay at home
   #8 = Fieldref           #10.#50        // constructor/Rabbit.nickName:Ljava/lang/String;
   #9 = Fieldref           #10.#51        // constructor/Rabbit.age:I
  #10 = Class              #52            // constructor/Rabbit
  #11 = Class              #53            // constructor/Animal
  #12 = Class              #54            // constructor/Eat
  #13 = Class              #55            // constructor/Sleep
  #14 = Utf8               nickName
  #15 = Utf8               Ljava/lang/String;
  #16 = Utf8               age
  #17 = Utf8               I
  #18 = Utf8               isCute
  #19 = Utf8               Z
  #20 = Utf8               ConstantValue
  #21 = Integer            1
  #22 = Utf8               <init>
  #23 = Utf8               ()V
  #24 = Utf8               Code
  #25 = Utf8               LineNumberTable
  #26 = Utf8               eat
  #27 = Utf8               sleep
  #28 = Utf8               play
  #29 = Utf8               (I)Ljava/lang/String;
  #30 = Utf8               StackMapTable
  #31 = Utf8               getNickName
  #32 = Utf8               ()Ljava/lang/String;
  #33 = Utf8               setNickName
  #34 = Utf8               (Ljava/lang/String;)V
  #35 = Utf8               getAge
  #36 = Utf8               ()I
  #37 = Utf8               setAge
  #38 = Utf8               (I)V
  #39 = Utf8               SourceFile
  #40 = Utf8               Rabbit.java
  #41 = NameAndType        #22:#23        // "<init>":()V
  #42 = Class              #56            // java/lang/System
  #43 = NameAndType        #57:#58        // out:Ljava/io/PrintStream;
  #44 = Utf8               I eat grass
  #45 = Class              #59            // java/io/PrintStream
  #46 = NameAndType        #60:#34        // println:(Ljava/lang/String;)V
  #47 = Utf8               I sleep well
  #48 = Utf8               I want to play outside
  #49 = Utf8               I want to stay at home
  #50 = NameAndType        #14:#15        // nickName:Ljava/lang/String;
  #51 = NameAndType        #16:#17        // age:I
  #52 = Utf8               constructor/Rabbit
  #53 = Utf8               constructor/Animal
  #54 = Utf8               constructor/Eat
  #55 = Utf8               constructor/Sleep
  #56 = Utf8               java/lang/System
  #57 = Utf8               out
  #58 = Utf8               Ljava/io/PrintStream;
  #59 = Utf8               java/io/PrintStream
  #60 = Utf8               println
{
  public static final boolean isCute;
    descriptor: Z
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 1

  public constructor.Rabbit();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method constructor/Animal."<init>":()V
         4: return
      LineNumberTable:
        line 11: 0

  public void eat();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String I eat grass
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 21: 0
        line 22: 8

  public void sleep();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String I sleep well
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 26: 0
        line 27: 8

  public java.lang.String play(int);
    descriptor: (I)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_1
         1: bipush        10
         3: if_icmple     9
         6: ldc           #6                  // String I want to play outside
         8: areturn
         9: ldc           #7                  // String I want to stay at home
        11: areturn
      LineNumberTable:
        line 30: 0
        line 31: 6
        line 33: 9
      StackMapTable: number_of_entries = 1
        frame_type = 9 /* same */

  public java.lang.String getNickName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #8                  // Field nickName:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 38: 0

  public void setNickName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #8                  // Field nickName:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 42: 0
        line 43: 5

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #9                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 46: 0

  public void setAge(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #9                  // Field age:I
         5: return
      LineNumberTable:
        line 50: 0
        line 51: 5
}
SourceFile: "Rabbit.java"

内容摘抄自《深入理解Java虚拟机》

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

推荐阅读更多精彩内容

  • 简介 Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础构成之一。了解Class文件的结...
    黄俊彬阅读 277评论 0 0
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,871评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,587评论 18 399
  • 看到这篇文章的你是“小蘑菇”么?是的话,跟你握个手;不是的话,请受我一拜。 这周五,我的工作情绪是低落的。我是做后...
    阿历Ali阅读 484评论 1 0
  • “他死了。” “我不回来。” 01 扶苏学会抽烟那年,她刚满二十岁。 扶苏开始戒烟那年,刚过二十五岁。 二十五岁的...
    沈鹿之阅读 922评论 0 0