您的位置:首页 > 其它

深入理解JVM之类文件结构

2017-11-05 18:54 429 查看
JVM的第五章是调优案例分析与实战需要大家上机编程进行具体体验,有代码的调优和工具的调优,有机会可以亲自上机试一试,JVM的第六章讲了类文件结构,明白了Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础构成之一。了解Class文件的结构对后面进一步了解虚拟机执行引擎有很重要的意义。第六章详细讲了Class文件结构中的各个组成部分,以及每个部分的定义,数据结构和使用方法。本章通过一个Java代码以及它的Class文件样例,用实战的方式演示了Class的数据是如何存储和访问的。

样例代码:

package com.pl.chap06;

public class TestClass {

private int m;

public int inc(){
return m + 1;
}
}
Class文件:



一:概述

不管从计算机诞生开始还是到现在,计算机都只认识0和1,所以我们编写的程序需要经过编译期翻译成0和1构成的二进制格式才能由计算机执行。但是由于近10年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一选择,越来越多的程序语言选择了与操作系统和机器指令集无关的,平台中立的格式作为程序编译后的存储格式。

二:无关性的基石

Java语言的诞生是基于CPU多指令集和多操作系统的,故Java在最初诞生时的口号就是“一次编写,到处运行(Write once,Run Anywhere)”。各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。注意这里是平台无关性,但是随着技术发展,现在人们也开始关注了虚拟机的另外一种特性——语言无关性。



三、Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是运行程序必要数据,没有空隙存在。到遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据JVM规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。是由多个无符号数或者其他作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由表6-1所示的数据项构成。



无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的格式,这时称这一系列连续的某一类型的数据为某一类型的集合。
Class的结构不像XML等描述语言,由于他没有任何分割符号,所以在表6-1中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。

魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。(魔数可以用来进行身份识别,用魔数而不是用文件扩展名是出于安全的考虑,因为文件扩展名可以随意改变;文件格式的指定者可以自由的选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可)
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。


如上图所示开头的4个字节的十六进制表示是0xCAFEBABE,代表次版本号的第5个和第6个字节值为0x0000,而主版本号为0x0032,也即是十进制的50,该版本号说明这个文件是可以被JDK1.6或以上版本虚拟机执行的Class文件。
注意:例如JDK1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件,而JDK1.2则能支持45.0~46.65535的Class文件。JDK1.7的能支持可生成Class文件主版本号最大值为51.0。目前最新的JDK为1.8。



常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)(它的计数是从1开始而不是0)。



如上图所示常量池容量为十六进制数0x0016,即十进制的22,这就代表常量池有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项空出来是有特殊考虑的,这样做的目的是在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况下就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其它集合类型,包括接口索引集合、字段表达集合、方法表集合等的容量计数一般都从0开始。



常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量:接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值;符号引用:属于编译原理方面的概念,包括三类常量:1,类和接口的全限定名;2,字段的名称和描述符;3,方法的名称和描述符。
Java在进行Javac编译的时候,并不像C、C++那样有“连接”这一步,而是在虚拟机加载Class文件的时候进行动态连接。即Class文件不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存之中。
常量池中的每一项常量都是一个表,在JDK1.7之前有11种结构各不相同的表结构数据,在JDK1.7中为了更好的支持动态语言调用,又增加了3种。具体如下表所示:



因为14种常量类型均有自己的结构故比较繁琐,对照图6-3常量池结构中的常量池第一项常量,它的标志位(偏移地址:0x0000000A)是0x07,查上表得知这个常量属于CONSTANT_Class_Info类型,得知此类型为类或接口的符号引用。CONSTANT_Class_Info结构比较简单,见下表:



tag是标志位区分常量类型;name_index是索引值,它指向常量池中一个CONSTANT_Utf8_Info类型常量,此常量代表了这个接口的全限定名,这里name_index为0x0002,指向了常量池中的第二项常量。继续从图6-3常量池结构中查找第二项常量,它的标志位(地址:0x0000000D)是0x01,查表得知它确实是一个CONSTANT_Utf8_Info类型的常量。它的结构见下表:



length值说明了这个UTF-8编码的字符串长度是多少字节。它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。
注意:UTF-8缩略编码和普通UTF-8编码的区别是:从'/u0001'到'/u007f'之间的字符(相当于1~127的ASCII编码)的缩略编码用一个字节表示,从'/u0080'到'/u07ff'之间所有字符的缩略编码用两个字节表示,从'/u0800'到'/ufffff'之间的所有字符的缩略码用3个字节表示。
在Class文件中方法、字段等都需要引用CONSTANT_Utf8_Info型常量来描述名称,所以CONSTANT_Utf8_Info型常量的最大长度也就是Java中方法、字段名的最大长度。这里的最大长度就是length的最大值为65535。故Java程序中定义了超过64KB英文字符的变量或方法名,将会无法编译。



用javap -verbose TestClass编译上面Java文件输出的结果,有兴趣可以研究研究

C:\Users\Peter\Desktop>javap -verbose TestClass
Compiled from "TestClass.java"
public class TestClass extends java.lang.Object
SourceFile: "TestClass.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method       #4.#15; //  java/lang/Object."<init>":()V
const #2 = Field        #3.#16; //  TestClass.m:I
const #3 = class        #17;    //  TestClass
const #4 = class        #18;    //  java/lang/Object
const #5 = Asciz        m;
const #6 = Asciz        I;
const #7 = Asciz        <init>;
const #8 = Asciz        ()V;
const #9 = Asciz        Code;
const #10 = Asciz       LineNumberTable;
const #11 = Asciz       inc;
const #12 = Asciz       ()I;
const #13 = Asciz       SourceFile;
const #14 = Asciz       TestClass.java;
const #15 = NameAndType #7:#8;//  "<init>":()V
const #16 = NameAndType #5:#6;//  m:I
const #17 = Asciz       TestClass;
const #18 = Asciz       java/lang/Object;

{
public TestClass();
Code:
Stack=1, Locals=1, Args_size=1
0:   aload_0
1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
4:   return
LineNumberTable:
line 1: 0

public int inc();
Code:
Stack=2, Locals=1, Args_size=1
0:   aload_0
1:   getfield        #2; //Field m:I
4:   iconst_1
5:   iadd
6:   ireturn
LineNumberTable:
line 5: 0

}






访问标志

在常量池结束之后,紧接着的两个字符代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话是否被声明为final。标志位一共有16个标志位可以使用。



类索引、父类索引与接口索引集合

类索引与父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类全限定名。由于Java语言不允许有多重继承,所以父类索引只有一个,除了java.lang.Object类外所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合之中。
类索引、父类索引与接口索引集合都按顺序排列在访问标志之后,类索引、父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_Info的类描述符常量,通过CONSTANT_Class_Info类型的常量中的索引值可以找到定义在CONSTANT_Class_Info类型的常量中的全限定名字符串。对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(Interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。





字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。Java中描述一个字段的信息有:字段的作用域(public、protect、private修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatitle修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息各个字段都是boolean值,要么有某个修饰符要么没有,很适合使用标志位来表示。







方法表集合

Class文件存储格式中对于方法的描述与对字段的描述几乎采用了完全一致的方式,方法的表结构如同表字段一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
方法表结构

类型
名称
数量
类型
名称
数量
U2
Access_flags
1
U2
Attributes_count
1
U2
Name_index
1
Attribute_info
attributes
Attributes_count
U2
Descriptor_index
1


属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的信息。







对于每个属性,它的名称需要从常量池引用一个CONSTANT_Utf8_Info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足如下的定义:
属性表结构

类型
名称
数量
U2
Attribute_name_index
1
U4
Attribute_length
1
U1
info
Attribute_length
1:code属性
Java程序方法体中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在code属性,如果方法表有code属性存在,那么它的表结构将如下:



2:Exceptions属性
Exception属性是在方法表中与code属性平级的一项属性。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。



3:LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必须的属性,但默认会生成到Class文件之中,可以在javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。



4:LocalVariableTable属性
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必须的属性,但默认会生成到Class文件之中,以在javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人用这个方法时,所有的参数名称都将丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码的编写带来较大的不便,而且在调试期间无法根据参数名称从上下文中获得参数值。



5:SourceFile属性
SourceFile属性用于记录生成这个Class文件的源码文件名称,这个属性也是可选的,在javac中分别使用-g:none或-g:source选项来取消或要求生成这项信息。在Java中大多数情况下类名和文件名是一致的但内部类除外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。这个属性是一个定长属性。



6:ConstantValue属性
ConstantValue属性就是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。
注意:对非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。目前,Sun Javac编译器的选择是:如果同时使用final和static修饰一个变量(按照习惯称为“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰或者并非基本类型及字符串,则会选择在<clinit>方法中进行初始化。



7:InnerClass属性
InnerClass属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClass属性。该属性的结构见下表:





8:Deprecated及Synthetic属性



9:StackMapTable属性
这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
10:signature属性
可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在JDK1.5之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。
11:BootstrapMethods属性
这个属性用于保存invokedynamic指令引用的引导方法限定符。

四:字节码指令简介

字节码与数据类型

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。

加载和存储指令

加载和存储指令用于将数据在帧栈中的局部变量表和操作数栈之间来回传输。这些指令包括如下内容:
1:将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
2:将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>;
3:将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>;
4:扩充局部变量表的访问索引指令:wide。
存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。
注意:上面类似这样的格式:iload_<n>:代表了iload_0、iload_1、iload_2和iload_3这几条指令

运算指令

运算或算数指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上算数指令可以分为两种:对整型数据进行运算的指令和对浮点型数据进行运算的指令,无论哪种算数指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char和boolean类型的算术指令,对于这类数据的运算,应使用操作int类型的指令代替。整数和浮点数的算数指令在溢出和被除零的时候也有各自不相同的行为表现。所有的算术指令如下:
加法指令:iadd、ladd、fadd、dadd;
减法指令:isub、lsub、fsub、dsub;
乘法指令:imul、lmul、fmul、dmul;
除法指令:idiv、ldiv、fdiv、ddiv;
求余指令:irem、lrem、frem、drem;
取反指令:ineg、lneg、fneg、dneg;
位移指令:ishl、lshl、fshl、dshl;
按位或指令:ior、lor;
按位与指令:iand、land;
按位异或指令:ixor、lxor;
局部变量自增指令:iinc;
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

类型转换指令

类型转换指令可以将两种不同的数值类型的进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
JVM直接支持(即转换时无需显示的转换指令)以下数值类型的宽化类型转换(Widening Numberic Conversions,即小范围类型向大范围类型的安全转换):
int类型到long、float、或者double类型;
long类型到float、double类型;
float类型到double类型。
相对的,处理窄化类型转换(Narrowing Numberic Conversions)时 ,必须显示地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。窄化类型转换可能导致转换结果产生不同的正负号、不同的数量级情况、转换过程很可能导致数值的精确度丢失。
注意:数值类型的窄化处理永远不可能导致虚拟机抛出运行时异常。

对象创建与访问指令

虽然类实例和数组都是对象,但JVM对类实例和数组的创建与操作采用了不同的字节码指令。对象创建后,就可以通过对象访问指令获取对象实例或者数组中的字段或者数组元素,这些指令如下:
1,创建类实例的指令:new;
2,创建数组的指令:newarray、anewarray、multianewarray;
3,访问类字段(static字段,或者成为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield、putfield、getstatic、putstatic;
4,把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload;
5,将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore;
6,取数组长度的指令:arraylength;
7,检查类实例类型的指令:instanceof、checkcast。

操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,JVM直接提供了一些用于直接操作操作数栈的指令,包括:
1,将操作数栈的栈顶一个或两个元素出栈:pop、pop2;
2,复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2;
3,将栈最顶端的两个数值互换:swap。

控制转移指令

控制转移指令可以让JVM有条件或者无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或者无条件地修改PC寄存器的值。控制转移指令如下:
条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne;
复合条件分支:tableswitch、lookupswitch;
无条件分支:goto、goto_w、jsr、jsr_w、ret;
在JVM中有专门的指令集用来处理int(boolean型、byte型、char型和short型的条件分支比较也都用int型,对于float型、double型的条件分支比较操作则会先执行相应类型的比较运算指令,它会返回一个整型值到操作数栈中,然后按int型执行)和reference类型的条件分支比较操作,为了可以无需明显标识一个实体值是否为null,也有专门的指令来检测null值。

方法调用和返回指令

方法调用指令举例:
invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是java语言最常见的方法分派方式;
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用;
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法;
invokestatic指令用于调用类方法(static方法);
invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法并执行该方法,前面4条调用指令的分派逻辑都固话在JVM内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

异常处理指令

在Java程序中显示抛出异常的操作(throw语句)都有athrow指令来实现,除了用throw语句显示抛出异常的情况之外,JVM规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常时自动抛出。而在Java虚拟机之中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。

同步指令

JVM可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步都使用管程(Monitor)来支持的。







五:公有设计和私有实现

JVM规范描绘了JVM应有的共同程序存储格式:Class文件格式以及字节码指令集。
理解公有设计和私有实现之间的分界线是很有必要的,JVM实现必须能够读取Class文件并精确实现包含在其中的JVM代码的语义。按照虚拟机规范一成不变地逐字实现其中的要求内容是一种途径;但是也可以在满足JVM规范要求的情况下对具体实现做出修改和优化也是可以的,并且JVM规范明确鼓励这么做。
虚拟机实现者可以使用这种伸缩性来让虚拟机实现更高的性能、更低的内存消耗或者更好的可移植性,选择哪种特性取决于JVM实现的目标和关注点。虚拟机实现主要有以下两种方式:
1,将输入的虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集;
2,将输入的虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集。
精确定义的虚拟机和目标文件格式不应当对虚拟机实现者创造性产生太多的限制,Java虚拟机应被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的、新的、有趣的解决方案。

六:Class文件结构的发展



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: