首页 > 其他分享 >JVM指令集

JVM指令集

时间:2023-06-29 13:00:32浏览次数:47  
标签:操作数 指令集 压入 JVM 操作码 value2 value1 类型

目录

JVM指令集

本文内容基于JVM规范中的第六章和第七章部分, 介绍了JVM中的字节码指令的含义和执行的过程.

一条JVM指令构成:

  • 一个操作操作码(opcode), 指定要被处理的操作.
  • 零或多个操作数(operand), 体现的是要被操作的值.

无论是操作码还是操作数, 大小都是一个字节.

本文粗略介绍了JVM指令的格式和它对应的操作处理.

值得注意的是, 操作码本身是一个字节的数字, 为了人类可读, 会使用助记符(mnemonic)[其实就是名称]来指代操作码, 后续用助记符来指代操作码的场合将使用斜体.

开始之前简单说一下前置知识:

JVM中的数据类型有 boolean, byte, char, short, int, float, reference, returnAddress, long, double.

对应的运算类型和分类如下表.

表-JVM中的实际类型和计算类型
实际类型 计算类型 分类
boolean int 1
byte int 1
char int 1
short int 1
int int 1
float float 1
reference reference 1
returnAddress returnAddress 1
long long 2
double double 2

除了数据类型外, 另一个比较重要的概念是栈帧.

方法被调用时产生一个栈帧, 字节码正是在栈帧内被执行的. 栈帧内包含操作数栈(Operand Stack), 局部变量表(Local Variables), 动态链接(Dynamic Linking), 方法出口(Method Invocation Completion), 前两者在本文中比较重要.

局部变量表 是一个数组, 其长度在编译时指定, 虚拟机会使用局部变量表传递参数. 表-JVM中的实际类型和计算类型中分类为1的类型都使用单个局部变量来储存, long, double 需要一对局部变量来储存. 实例方法调用中, 局部变量表中的0位置永远都是this, 方法参数列表在局部变量表中的索引从1开始, 类方法调用中, 方法参数列表在局部变量表中的索引从0开始.

操作数栈 是一个先进后出的栈, 最大栈深度也是在编译时确定的, 操作数栈是字节码执行的临时数据区. 操作数栈 在栈帧被创建时为空, 在执行字节码的过程中, 可以将一些局部变量或常量加载到操作数栈, 或是弹出操作数栈的数据保存到局部变量表等等, 同样, long, double 将占用两个单位的深度, 而其它类型为一个.

常量操作码(Constants)

常量操作码的范围是0-20. 除了0 nop是什么都不做外, 常量操作码都是将常量压入到操作数栈中. 常量操作码不读取操作数栈的内容.

0 nop

什么也不做.

1 aconst_null

null压入操作数栈栈顶. (JVM规范未指定null的具体值).

2-8 iconst_<i>

int类型常量<i>压入操作数栈. 有 iconst_ml, iconst_0, iconst_1, iconst_2, iconst_3, iconst_4, iconst_5, 分别代表压入值-15.

9-10 lconst_<i>

long类型常量<i>压入操作数栈. 有 lconst_0, lconst_1, 分别代表压入值01.

11-13 fconst_<i>

float类型常量<i>压入操作数栈. 有 fconst_0, fconst_1, fcont_2, 分别代表压入值0, 12.

14-15 dconst_<i>

double类型常量<i>压入操作数栈. 有 dconst_0, dconst-1, 分别代表压入值01.

16 bipush

压入8位整数.

有一个操作数, 将操作数扩展为int压入操作数栈.

17 sipush

压入16位整数.

有两个操作数, 这两个操作数都是无符号的byte, 等价于一个short, 这个short被拓展为int压入操作数栈.

18 ldc

加载常量.

有一个操作数, 这个操作数代表常量池中一个可加载常量的索引, 将该可加载的常量压入到操作数栈.

但不能加载 double, long 型常量, 以及字段描述符是 J (表示long)或 D (表示 double)的符号引用.

19 ldc_w

加载常量.

ldc 的宽码版本, 有两个操作数, 这两个操作数合并构成一个无符号16位索引, 同样代表常量池中一个可加载的常量, 限制也和 ldc 相同.

20 ldc2_w

加载 double, long 常量.

有两个操作数, 这两个操作数合并构成一个无符号16位索引, 代表常量池中一个可加载的常量, 但与 ldcldc_w 的区别是其加载的常量刚好相反, 只加载 double, long 型常量, 以及字段描述符是 J (表示long)或 D (表示 double)的符号引用.

加载操作码(Loads)

与常量操作码类似, 加载操作码的结果也是最终在操作数栈压入数据, 不同之处在于, 压入的数据来源通常是局部变量表或数组中的数据而非常量.

21-25 <t>load

这几个指令都有一个操作数, 该操作数代表局部变量表中一个变量的索引, 将该变量压入到操作数栈.

这几个指令是: iload, lload, fload, dload, aload, 分别表示压入一个 int, long, float, double, reference类型的变量到操作数栈.

26-45 <t>load_<i>

将局部变量表中索引位置<i><t>类型的变量压入到操作数栈.

<t> 分别是 i (表示 int), l (表示 long), f (表示 float), d (表示 double), a (表示 reference)

<i> 分别是 0, 1, 2, 3, 代表局部变量表中一个变量的索引.

46-53 <t>aload

从操作数栈栈顶弹出 <t> 类型的数组的引用, 再弹出要加载的索引, 将数组中索引位置的数据压入到操作数栈.

<t> 分别是 i (表示 int), l(表示 long), f (表示 float), d (表示 double), a (表示 reference), b (表示 byte), c (表示 char), s(表示 short).

如果弹出的数组的引用是 null, 将抛出 NullPointerException.

如果索引范围越界, 将抛出 ArrayIndexOutOfBoundsException.

储存字节码(Stores)

储存字节码一般是从操作数栈中读取数组, 并将数据储存到局部变量表或数组中.

54-58 <t>store

有一个操作数, 表示局部变量的索引, 从操作数栈栈顶读取一个对应 <t> 类型的变量储存到局部变量表中, 操作数索引所在位置.

<t> 分别是 i (表示 int), l(表示 long), f (表示 float), d (表示 double), a (表示 reference), b (表示 byte), c (表示 char), s(表示 short).

59-78 <t>store_<i>

从操作数栈栈顶, 读取一个 <t> 类型的变量, 储存到局部变量表中的 <i> 索引的位置.

<t> 分别是 i (表示 int), l(表示 long), f (表示 float), d (表示 double), a (表示 reference), b (表示 byte), c (表示 char), s(表示 short).

<i> 分别是 0, 1, 2, 3.

79-86 <t>astore

从操作数栈弹出<t>类型的数组的引用, 再从操作数栈弹出 int 类型索引, 最后弹出要进行存储操作的 <t> 类型的值, 将值储存到数组中索引位置.

<t> 分别是 i (表示 int), l(表示 long), f (表示 float), d (表示 double), a (表示 reference), b (表示 byte), c (表示 char), s(表示 short).

如果数组的引用是null, 将抛出 NullPointerException, 如果索引越界, 将抛出 ArrayIndexOutOfBoundsException.

栈操作码(Stack)

栈操作码, 用于对操作数栈内数据进行操作.

87-88 pop*

弹出操作数栈中的数据. 在弹出一个单位的数据时, 用 pop (87 0x57), 当弹出两个单位的数据时, 使用 pop2 (88 0x58).

如果要弹出的是long, double 类型, 必须使用 pop2.

89 dup

复制栈顶的数据, 压入栈顶.

\[\dots, value \rightarrow \dots, value, value \]

90 dup_x1

复制栈顶的数据, 插入到栈顶的两个单位的操作数的下方.

\[\dots, value2, value1 \rightarrow \dots, value1, value2, value1 \]

91 dup_x2

复制栈顶的数据, 插入到栈顶的三个单位的操作数的下方.

\[\dots, value3, value2, value1 \rightarrow \dots, value1, value3, value2, value1 \]

value3value2 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(longdouble).

92 dup2

复制操作数栈栈顶的两个数据, 压入栈顶.

\[\dots, value2, value1 \rightarrow \dots, value2, value1, value2, value1 \]

value2value1 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(longdouble).

93 dup2_x1

复制操作数栈栈顶的两个数据, 插入栈顶三个单位操作数之下.

\[\dots, value3, value2, value1 \rightarrow \dots, value2, value1, value3, value2, value1 \]

value2value1 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(longdouble).

value3 只能是占用一个单位空间的类型.

94 dup2_x2

复制操作数栈栈顶的两个操作数, 插入栈顶四个单位操作数之下.

\[\dots, value4, value3, value2, value1 \rightarrow \dots, value2, value1, value4, value3, value2, value1 \]

value2value1 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(longdouble).

value4value3 可以是两个占用一个单位空间的类型的数据, 也可以是一个占用两个单位空间的类型(longdouble).

95 swap

交换栈顶两个操作数.

\[\dots, value2, value1 \rightarrow \ ..., value1, value2 \]

value2value1 的类型必须是分类为1的类型.

JVM不提供分类为2的类型的交换指令.

数学操作码(Math)

数学运算的操作码.

96-99 <t>add

加.

<t> 可以是 i, l, f, d.

将操作数数栈栈顶的两个操作数相加, 结果压入操作数栈.

参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.

100-103 <t>sub

减.

<t> 可以是 i, l, f, d.

将操作数数栈栈顶的两个操作数相减, 结果压入操作数栈. 栈顶的是被减的数.

参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.

104-107 <t>mul

乘.

<t> 可以是 i, l, f, d.

将操作数数栈栈顶的两个操作数乘, 结果压入操作数栈.

参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.

108-111 <t>div

除.

<t> 可以是 i, l, f, d.

将操作数数栈栈顶的两个操作数相除, 结果压入操作数栈. 栈顶的是除数.

参与运算的类型要匹配, 并且运算要符合对应类型的运算规则.

112-115 <t>rem

取余.

<t> 可以是 i, l, f, d.

将操作数数栈栈顶的两个操作数取余, 结果压入操作数栈.

当为intlong时, 栈顶的两个值 value2value1 (value2 先弹出) 的运算规则是 result = value1 - (value1 / value2) * value2.

当为floatdobule时, 运算规则为IEEE754标准.

116-119 <t>neg

取反.

<t> 可以是 i, l, f, d.

弹出操作数栈栈顶的值 value , 取反后压入操作数栈.

对于 intlong, 设 xvalue, -x 等价于 (~x) + 1 (按位取反 + 1), 并且因为负数比正数多, 负数最小值取负数得到的是自身.

对于 floatdouble, 运算规则为IEEE754标准:

  • 如果 valueNaN, 结果是 NaN
  • 如果 value 是无穷, 结果是符号位相反的无穷
  • 如果 value0, 结果是符号位相反的 0.

120-121 <t>shl

左移.

<t> 可以是 il.

弹出操作数栈栈顶的 value2value1 (value2 先弹出), 将 value1 左移 s 位作为结果压入操作数栈, 其中 svalue2 低5位的值.

相当于是 value1 乘以 2 的 s 次方(就算会溢出), s 的范围是 0 - 31 之间, 等同于 value0x1f 按位与的结果.

122-123 <t>shr

右移.

<t> 可以是 il.

弹出操作数栈栈顶的 value2value1 (value2 先弹出), 将 value1 右移 s 位作为结果压入操作数栈, 其中 svalue2 低5位的值.

右移时, 在高位补充符号位.

相当于是 value1 除以 2 的 s 次方, s 的范围是 0 - 31 之间, 等同于 value0x1f 按位与的结果.

124-125 <t>ushr

无符号右移.

<t> 可以是 il.

弹出操作数栈栈顶的 value2value1 (value2 先弹出), 将 value1 右移 s 位作为结果压入操作数栈, 其中 svalue2 低5位的值, 即svalue1 & 0x1f, .

右移时, 在高位补充0.

如果 value1 为正数, 结果与 value1 >> s 相同; 如果 value1 为负数, 结果等于 (value1 >> s) + (2 << ~s), (2 << ~s) 抵消了增加的符号位.

s 的范围是 0 - 31 之间.

126-127 <t>and

按位与.

<t> 可以是 il.

弹出操作数栈栈顶的 value2value1 (value2 先弹出), 进行按位与运算后将结果压入操作数栈.

128-129 <t>or

按位或.

<t> 可以是 il.

弹出操作数栈栈顶的 value2value1 (value2 先弹出), 进行按位或运算后将结果压入操作数栈.

130-131 <t>xor

异或.

<t> 可以是 il.

弹出操作数栈栈顶的 value2value1 (value2 先弹出), 进行按位异或运算后将结果压入操作数栈.

132 iinc

自增.

操作码后是一个字节的无符号整数 index 指向局部变量表中 int 类型的变量, 以及一个字节的有符号整数 const. 将局部变量表中索引位置的变量增加 const.

iinc 可以通过 wide 字节码拓展, 以访问局部变量表中两字节索引位置的变量, 或增加两字节的有符号立即数.

转换操作码(Conversions)

133-147 <t1>2<t2>

弹出操作数栈栈顶的<t1>类型的操作数, 强转为<t2>类型, 压入操作数栈, 在向下转型的时候, 可能会发生丢失精度.

操作数列表:

i2l = 133 (0x85)
i2f = 134 (0x86)
i2d = 135 (0x87)
l2i = 136 (0x88)
l2f = 137 (0x89)
l2d = 138 (0x8a)
f2i = 139 (0x8b)
f2l = 140 (0x8c)
f2d = 141 (0x8d)
d2i = 142 (0x8e)
d2l = 143 (0x8f)
d2f = 144 (0x90)
i2b = 145 (0x91)
i2c = 146 (0x92)
i2s = 147 (0x93)

比较操作码(Comparisons)

比较操作数栈的值, 产生结果或进行跳转.

148 lcmp

比较 long 类型.

比较操作数栈栈顶的两个long型操作数, 结果为int类型, 压入到操作数栈栈顶. 栈顶的操作数分别为 value1, value2, ..., 如果 value1 大于 value2, 结果为 1, 如果 value1 等于 value2, 结果为0, 如果 value1 小于 value2, 结果为 -1.

149-152 <t>cmp<op>

浮点数的比较. <t>fd, <op>gl, <op>代表的是对NaN的处理方式.

fcmpl = 149 (0x95)

fcmpg = 150 (0x96)

dcmpl = 151 (0x97)

dcmpg = 152 (0x98)

这些指令比较操作数栈栈顶的两个浮点数, 比较的结果以int类型压入操作数栈栈顶. 栈顶的操作数分别为 value1, value2, ..., 如果 value1 大于 value2, 结果为 1, 如果 value1 等于 value2, 结果为0, 如果 value1 小于 value2, 结果为 -1. 当 value1value2NaN 的情况下, fcmpgdcmpg1 压入操作数栈, fcmpldcmpl-1 压入操作数栈.

153-158 if<cond>

ifeq = 153 (0x99)

ifne = 154 (0x9a)

iflt = 155 (0x9b)

ifge = 156 (0x9c)

ifgt = 157 (0x9d)

ifle = 158 (0x9e)

指令中有两个操作数, 弹出操作数栈栈顶的int类型的操作数, 比较成功之后, 跳转指令中的两个操作数所构成的16位数字的偏移量, 偏移起始地址为 if<cond> 操作码的地址.

若操作数栈弹出的操作数为 value, 则:

  • ifeq 只在 value = 0 时成功
  • ifne 只在 value ≠ 0 时成功
  • iflt 只在 value < 0 时成功
  • ifge 只在 value ≥ 0 时成功
  • ifgt 只在 value > 0 时成功
  • ifle 只在 value ≤ 0 时成功

比较不成功时, 继续执行 if<cond> 后面的操作码.

159-164 if_icmp<cond>

指令中有两个操作数, 弹出操作数栈栈顶的两个int类型的操作数, 比较成功后, 跳转指令中的两个操作数所构成的16位数字的偏移量, 偏移起始地址为if_icmp<cond>操作码的地址.

if_icmpeq = 159 (0x9f)

if_icmpne = 160 (0xa0)

if_icmplt = 161 (0xa1)

if_icmpge = 162 (0xa2)

if_icmpgt = 163 (0xa3)

if_icmple = 164 (0xa4)

若操作数栈弹出的操作数分别为 value2, value1 (value2 先弹出), 则:

  • if_icmpeq 只在 value1 = value2 时成功
  • if_icmpne 只在 value1value2 时成功
  • if_icmplt 只在 value1 < value2 时成功
  • if_icmpge 只在 value1value2 时成功
  • if_icmpgt 只在 value1 > value2 时成功
  • if_icmple 只在 value1value2 时成功

比较不成功时, 继续执行 if_icmp<cond> 后面的操作码.

165-166 if_acmp<cond>

指令中有两个操作数, 弹出操作数栈栈顶的两个reference类型的操作数, 比较成功后, 跳转指令中的两个操作数所构成的16位数字的偏移量, 偏移起始地址为if_acmp<cond> 操作码的地址.

if_acmpeq = 165 (0xa5)

if_acmpne = 166 (0xa6)

若操作数栈弹出的操作数分别为 value1, value2 (value1 先弹出), 则:

  • if_acmpeq 只在 value1 = value2 时成功
  • if_acmpne 只在 value1value2 时成功

比较不成功时, 继续执行 if_acmp<cond> 后面的操作码.

控制操作码(Control)

流程控制的操作码.

167 goto

直接跳转.

有两个操作数, 跳转到两个操作数组成的16位数字偏移量的地址, 偏移起始地址从 goto 操作码开始.

168 jsr

跳转异常处理的子过程.

class文件版本低于50(对应jdk6)或以下, 才会使用 jsr. 更高版本的jdk中已经不再使用.

jsr 用于进行 try-finally 语句的跳转控制.

jsr 有两个操作数, 组成了一个16位数字偏移, 偏移相对起始地址为 jsr 操作码的地址, JVM将跳转到指定的偏移地址上, 并且将 jsr 后面的操作码的地址以 returnAddress 类型压入到操作数栈.

169 ret

从异常处理的子过程中返回.

class文件版本低于50(对应jdk6)或以下, 才会使用 ret. 更高版本的jdk中已经不再使用.

ret 用于进行 try-finally 语句的跳转控制.

有一个操作数, 这个操作数是一个0-255之间的无符号整数, 代表当前栈帧中的局部变量表的一个包含了returnAddress类型的索引, 程序将跳转该地址, 并继续执行.

170 tableswitch

通过表索引进行跳转.

tableswitch 指令的格式: tableswitch 操作码; 0-3 字节的填充; 4字节的数字表示default语句跳转的偏移量: default offset; 4字节的最小的匹配case : low; 4字节的最大的匹配case: high; 最大case - 最小case + 1个 4字节数字表示对应case跳转的偏移量: jump offsets;

偏移量都是相对于 tableswitch 操作码的地址.

执行时弹出操作数栈栈顶的int类型数据 index, 如果不在匹配的范围中, 跳转到default偏移的地址, 否则, 通过index - low 作为索引在 jump offsets中获取对应的偏移量, 跳转偏移对应的地址继续执行.

填充的字节数由 tableswitch 离方法的开始(方法内的第一个字节)的距离决定. 在填充后, default offset 的第一个字节的开始距离方法开始的字节数必须是4的整倍数.

例如: tableswitch 是方法内的第10个字节(前面的字节码总计占据9个字节), 那么就需要填充两个字节, 如果是第12个字节, 则无需填充.

171 lookupswitch

通过查找比较进行跳转.

lookupswitch 指令的格式: lookupswitch 操作码; 0-3 字节的填充; 4字节的数字表示default语句跳转的偏移量: default offset; 4字节的数字表示有多少对match-offset: npairs; 长度为 npairsmatch-offset pairs, 每对 match-offset 为8字节, 前4字节的数字表示匹配的case, 后4字节为对应case的偏移量, match-offset pairscase 排序必须是递增的.

偏移量都是相对于 lookupswitch 操作码的地址.

执行时弹出操作数栈栈顶的int类型数据 index, 在 match-offset pairs 中查找匹配的case, 找到后跳转匹配的case的偏移量继续执行, 没有找到这跳转 default offset 的偏移量继续执行.

填充的字节数由 lookupswitch 离方法的开始(方法内的第一个字节)的距离决定. 在填充后, default offset 的第一个字节的开始距离方法开始的字节数必须是4的整倍数.

例如: lookupswitch 是方法内的第10个字节(前面的字节码总计占据9个字节), 那么就需要填充两个字节, 如果是第12个字节, 则无需填充.

172-177 <t>return

返回 <t> 类型的结果.

ireturn = 172 (0xac)
lreturn = 173 (0xad)
freturn = 174 (0xae)
dreturn = 175 (0xaf)
areturn = 176 (0xb0)
return = 177 (0xb1)

返回操作数栈栈顶的<t>类型. <t> 按顺序是: i, l, f, d, a, 以及一个返回 void 类型的 return 指令.

如果方法有 synchronized 修饰, 则会释放锁. 但是如果未持有锁则抛出IllegalMonitorStateException.

在JVM实现未遵循JVM规范中对于锁的规范时, 可能会发生这种进入了 synchronized 方法但却不持有锁的情况.

引用操作码(References)

通过引用来进行一些操作的操作码, 大部分引用操作码都是与面向对象

178 getstatic

读取 static 字段.

有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 与 ldc 的区别是, getstatic 用于读取常量池中索引位置的static字段的符号引用压入操作数栈.

成功解析字段后, 字段所属的类或接口还未初始化, 则该类或接口将初始化.

179 putstatic

设置 static 字段.

有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 将操作数栈栈顶的操作数弹出, 赋值给常量池中索引位置的字段.

如果字段的类型是boolean, byte, short, int, char, 栈顶的操作数必须为int类型, 如果为float, longdouble则必须为对应的float, long, double类型. 如果字段是引用类型, 则必须符合赋值兼容性.

180 getfield

读取对象字段.

有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 索引位置是字段的符号引用, 该引用的字段已经解析.

弹出栈顶的 objectref, objectrefreference 类型但不是数组类型, 弹出后, 获取引用对象的字段数据并压入操作数栈.

链接时: 可能抛出字段引用解析的异常;如果字段是static的, 将抛出IncompatibleClassChangeError异常.

运行时: 如果objectrefnull, 将抛出NullPointerException.

getfield 不能访问数组的length字段, 而是通过 arraylength 字节码获取数组长度.

181 putfield

设置对象字段.

有两个操作数, 这两个操作数构成一个无符号的整数, 对应常量池的索引. 索引位置是字段的符号引用, 该引用的字段已经解析.

弹出栈顶的 valueobjectref (value 在最上面), objectrefreference 类型, 但不是数组类型, 将 value 赋值给 objectref 的字段.

value 的类型必须与字段匹配

  • 如果字段描述符类型是boolean, byte, char, shortint, value 的类型必须是 int.
  • 如果字段描述符类型是float, long, double, value的类型必须也对应地是float, long, double.
  • 如果字段描述符类型是引用类型, value 的类型必须满足赋值兼容性规范.
  • 如果字段是final的, 该指令必须是在当前类的构造方法中.

valueint 类型而字段是 boolean 类型时, 被设置的值是value & 1 的结果.

182 invokevirtual

调用实例方法, 基于类进行分派.

调用类方法. 有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个方法引用. 该方法引用已被解析.

弹出栈顶的 nargsobjectref, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致. objectrefreference 类型, 但不是数组类型.

针对类型为 Cobjectref 和常量池中被解析的方法 \(m_R\) , 选择方法调用.

对于一般方法, 选择方法的流程是:

  • 如果 \(m_R\) 被 ACC_PRIVATE 标记, 该方法被选中.

  • 否则, 通过以下步骤查找方法

还有一些其它情况这里水平有限不多说了, 参考原文: https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-6.html#jvms-6.5.invokevirtual

按<<深入Java虚拟机中>>对于 invokevirtual 的解释是:

invokevirtual 用于调用所有虚方法.

invokevirtual 指令的运行时解析过程大致分为以下几步:

  1. 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果 通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

笔者注:

当存在参数时, 第一步找到调用方法的调用者不是在栈顶.

例如下面这这段代码:

public void m(){
  System.out.println("hello");
}

m 方法对应的字节码

0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #13                 // String hello
5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return

可以看到, getstatic 指定先将 System.out 压入栈, 再通过 ldc 从常量池加载 "hello", 从栈顶往下, 依次是参数列表的值(从右往左), 和调用者.

读者可以尝试调用多个参数的方法, 验证参数入栈的顺序.

另外, 下面的 B.m 方法与 A.m 方法具有相同的名称和描述符, 并且A.call 方法中的 a 的实际类型也是B, 但实际调用的方法仍然是A.m.

package a;
public class A {
  void m() {
    System.out.println("A.m()");
  }

  public void call(A a) {
    a.m();
  }
}
package b;
public class B extends A {
  void m() {
    System.out.println("B.m()");
  }
  public static void main(String[] args) {
    var b = new B();
    b.call(b);
  }
}

输出是 A.m()

另外还有一个小 trick 如下:

在包 pa 中有如下类:

package pa;
public class A {
  void m() {
    System.out.println("A.m()");
  }
  public void call(A a) {
    a.m();
  }
}
package pa;
public class B extends A {
  void m() {
    System.out.println("B.m()");
  }
  public void call(B b) {
    b.m();
  }
}
package pa;
public class C extends B {
  void m() {
    System.out.println("C.m()");
  }
  public void call(C c) {
    c.m();
  }
}

在另一个包 pb 中有:

package pb;
public class D extends C {
  void m() {
    System.out.println("D.m()");
  }
  public static void main(String[] args) {
    var d = new D();
    d.call((A) d);
    d.call((B) d);
    d.call((C) d);
  }
}

通过 javac -cp . pb\D.javajava -cp . pb.D运行的输出是:

C.m()
C.m()
C.m()

对此的解释是:

A, B, C 在一个包中, 并有继承关系, 对于 package-private 访问级别的 m 方法, B 重写了 A, C 重写了 B.

但是 D 在另一个包中, Dm 方法并没有重写 C, 因为 package-private 对于另一个包不可见.

因此通过 D 的实例调用 A.m, B.m, C.m, 都将调用到 C.m.

如果将 B 类的 m 方法改为 public 的, 即:

package pa;
public class B extends A {
  public void m() {
    System.out.println("B.m()");
  }

  public void call(B b) {
    b.m();
  }
}

此时将无法直接正确编译 CD 的, 因为子类方法的访问等级不能更低.

但可以手动地单独编译 B 类, 即 javac -cp pa\B.java, 因为 CD 已经编译过了, 所以再一次运行 D, 输出是:

D.m()
D.m()
C.m()

对此的解释是:

Bm 方法为 public 之后, 根据JVM规范对于重写的定义, 此时 D 重写了 Bm 方法, 但没有重写 C, 并且, 因为重写传递性, Am 方法也被重写.

183 invokespecial

调用实例方法, 调用构造方法或当前类方法或父类方法.

有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个方法的符号引用. 该方法已被解析.

操作数栈必须包含 nargsobjectref, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致. objectrefreference 类型, 但不是数组类型.

如果满足以下所有条件, 定义 C 为当前类的直接父类.

  • 被解析的方法不是实例初始化方法
  • 方法的符号引用中, 类的符号引用是当前类的父类
  • 当前类包含 ACC_SUPER 标记

否则, C 是方法的符号引用的类或接口.

实际被调用的方法的选择流程如下:

  1. C 中声明了与方法引用名称和描述符相同的方法, 该方法将被调用
  2. 否则, 如果 C 是一个类并且有父类, 从 C 的直接父类中开始, 按继承结构从下往上查找符合步骤1的方法, 直至没有父类或找到方法.
  3. 否则, 如果 C 是一个接口, 并且方法引用的名称和描述符与 Object 中声明的 public 方法一致, 则调用该方法.
  4. 否则, 从 C 的超接口中查找名称和描述符一致的方法.

invokespecialinvokevirtual 的最大区别是, 调用者的实际类型不会影响 invokespecial 进行方法查找的逻辑, 而 invokevirtual 必须依赖调用者的实际类型进行查找.

invokespecial 在查找方法时也不关注方法的重写关系.

另外对于构造方法 <init>, 必须使用 invokespecial 调用.

在java中, 对于 构造方法(<init>), super方法(类或接口的super), private方法, 都将使用 invokespecial 调用.

184 invokestatic

有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个方法的符号引用. 该方法已被解析.

该方法不能是实例方法, 或类和接口的初始化方法.

该方法必须是 static, 并且不能被 abstract 修饰.

成功解析方法后,如果类或接口尚未初始化,则声明已解析方法的类或接口将被初始化.

操作数栈必须包含 nargs, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致.

185 invokeinterface

调用接口方法. 有4个操作数, 前两个操作数构成一个16位的无符号数字的索引, 指向常量池中一个方法引用. 该方法引用已被解析.

第三个操作数记为 count, count 是无符号byte类型, 并且不能为0. 最后一个操作数恒为0.

操作数栈必须包含 nargsobjectref, nargs 是参数值, 其数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)与方法参数一致. objectrefreference 类型, 但不是数组类型.

其方法选择的逻辑与 invokevirtual 相同.

调用时其它处理参考原文: https://docs.oracle.com/javase/specs/jvms/se20/html/jvms-6.html#jvms-6.5.invokeinterface

186 invokedynamic

调用动态调用点.

有四个操作数, 前两个操作数构成一个16位的无符号数字的索引, 指向常量池中一个动态调用点的符号引用, 后两个操作数目前恒为0.

动态调用点的符号引用由指向引导方法表的索引和指向常量池中的名称和描述符的索引构成.

每个动态调用掉绑定一个引导方法, 在动态调用点解析的时候会调用引导方法, 将得到 java.lang.invoke.CallSite 实例的引用. java.lang.invoke.CallSite 的实例被认为是 "绑定" 这个特定的 invokedynamic 指令.

invokedynamic 执行调用点, 从操作数栈中弹出 nargs, nargs 是参数, 与动态调用点的描述符的参数列表数量, 类型和顺序(弹出的顺序与参数列表顺序刚好相反)一致, 调用结束后产生动态调用描述符的返回类型的引用, 压入操作数栈.

例如:

public static void main(String[] args) {
	Runnable runnable = () -> System.out.println();
}

其字节码信息如下(省略部分其它字节码):

Constant pool:
   #7 = InvokeDynamic      #0:#8          // #0:run:()Ljava/lang/Runnable;
   #8 = NameAndType        #9:#10         // run:()Ljava/lang/Runnable;
   #9 = Utf8               run
  #10 = Utf8               ()Ljava/lang/Runnable;
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: invokedynamic #7,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
         5: astore_1
         6: return
BootstrapMethods:
  0: #42 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #49 ()V
      #50 REF_invokeStatic moe/wymc/a/A.lambda$main$0:()V
      #49 ()V

可以看到main 方法中, invokedynamic 指令调用的动态调用点 #7, 在常量池中是一个动态调用点, 其组成是 #0 指向引导方法表, #8 则是动态调用点的名称和描述符.

稍微修改一下代码:

  public static void main(String[] args) {
    int i = 0;
    Runnable runnable = () -> System.out.println(i);
  }
Constant pool:
   #7 = InvokeDynamic      #0:#8          // #0:run:(I)Ljava/lang/Runnable;
   #8 = NameAndType        #9:#10         // run:(I)Ljava/lang/Runnable;
   #9 = Utf8               run
  #10 = Utf8               (I)Ljava/lang/Runnable;
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: invokedynamic #7,  0              // InvokeDynamic #0:run:(I)Ljava/lang/Runnable;
         8: astore_2
         9: return
BootstrapMethods:
  0: #42 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #49 ()V
      #50 REF_invokeStatic moe/wymc/a/A.lambda$main$0:(I)V
      #49 ()V

注意观察常量池中 #10, 即动态调用的描述符的变化, 其参数列表多了捕获的变量 i 的类型 int, 其表示为 I, 相应的, 在 main 方法的调用栈中也出现了局部变量的存取, 在 invokedynamic 调用时, 操作数栈中有一个 int 类型的操作数 1.

读者可以多尝试用 lambda 来实现不同的 FunctionInterface, 观察其字节码.

JVM中规定引导方法的解析必须是在运行时, 而非在类加载过程中解析, 即 invokedynamic 首次执行时才会执行绑定的引导方法.

187 new

创建新对象, 压入该对象的引用. 有两个操作数, 构成一个16位的无符号数字的索引, 指向常量池中一个类或接口类型的符号引用. 该符号引用已解析, 并且应该产生一个类的类型.

该类的新实例的内存从堆中分配,新对象的实例变量被初始化为其默认的初始值, 指令完成后, 类型为 reference 的操作数 objectref 被压入操作数栈.

当类解析成功时, 如果类还没初始化将进行初始化.

如果符号引用解析的结果是接口或抽象类, 将抛出InstantiationError.

new 指令不会完整的创建对象, 直到调用类的初始方法.

188 newarray

创建新的基础类型的数组, 压入该数组的引用. 有一个操作数 atype, atype 有 8 个值, 代表要初始化的8种不同的数组类型.

Array Type atype
T_BOOLEAN 4
T_CHAR 5
T_FLOAT 6
T_DOUBLE 7
T_BYTE 8
T_SHORT 9
T_INT 10
T_LONG 11

操作数栈栈顶为int类型的 count, 表示要创建的数组的长度.

在堆上分配长度为 count , 元素类型为 atype 的数组, 其引用压入到操作数栈中, 数组的每个元素的值被初始化为默认值.

在Oracle的JVM上, 对应 boolean 类型的数组使用的是8位值的数组. 其它虚拟机可能不一样, 但仍然必须使用 baloadbastore 来访问这些数组.

189 anewarray

创建新的引用类型的数组, 压入该数组的引用.

有两个操作数, 构成一个16位的数字, 指向常量池中一个类, 数组或接口类型的符号引用. 该符号引用已解析.

弹出操作数栈栈顶为 int 类型的 count, 表示要创建的数组的长度.

在堆上分配长度为 count 的对应类型的数组, 其引用压入到操作数栈中, 数组的每个元素的值被初始化为默认值 null.

anewarray 只能用于创建一维数组.

190 arraylength

获取数组长度.

操作数栈栈顶必须是数组的引用, 将其长度压入到操作数栈.

如果引用为 null, 将抛出空指针异常.

191 athrow

抛出异常.

操作数栈栈顶是 Throwable 类型或其子类型的实例的引用 objectref, 弹出 objectref , 然后通过在当前方法的异常表中查找第一个与 objectref 类型匹配的异常处理器, 异常表中包含处理器的跳转地址, 当前栈帧的程序计数器被设置为跳转的地址, 操作数栈被清空, 并压入异常对象的 objectref, 程序从跳转地址继续执行.

如果找不到异常处理器, 从虚拟机栈中弹出当前栈帧, 恢复调用者的栈帧, 并重新抛出异常, 如果不再存在栈帧, 则退出线程.

192 checkcast

检查对象是否是指定的类型.

有两个操作数, 构成一个16位的数字, 指向常量池中一个类, 数组或接口类型的符号引用. 该符号引用已解析.

checkcast 不改变操作数栈, 只对栈顶对象引用进行类型检查, 如果栈顶元素是 null, 或栈顶元素可以转换成已解析的符号引用的类型, 则什么都不变, 否则, 抛出ClassCastException.

判断栈顶元素 objectref 非空情况下, 其类型为 S, 已解析的符号引用的类型是 T, 判断 objectref 能转型为 T 的规则如下:

  • 如果 S 是类
    • 如果 T 是类, S 必须是 T 或者 T 的子类
    • 如果 T 是接口, S 必须实现接口 T
  • 如果 S 是数组类型 SC[], 即其元素类型是 SC
    • 如果 T 是类, T 必须是 Object
    • 如果 T 是接口, T 必须是 CloneableSerializable
    • 如果 T 是数组类型 TC[], 即元素类型是 TC, 满足下列条件之一即可
      • TCSC 是相同的原始数据类型
      • TCSC 是引用类型,并且可以通过递归应用这些规则将类型 SC 强制转换为 TC

checkcast 并不会改变引用的实际类型, 在 checkcast 成功时也不会修改操作数栈, 只会在失败时抛出 ClassCastException.

checkcastinstanceof 非常相似, 不同点在于: 1. 对 null 的处理不同; 2. 失败时的处理不同; 3. 对操作数栈的影响不同;

193 instanceof

确定对象是否是指定的类型.

有两个操作数, 构成一个16位的数字, 指向常量池中一个类, 数组或接口类型的符号引用. 该符号引用已解析.

弹出操作数栈栈顶的引用类型的 objectref, 检查其类型是否可以转换为已解析的符号引用的类型.

如果 objectrefnull, 将 int 类型的 0 压入操作数栈, 如果可以转型则将 int 类型的 1 压入操作数栈, 否则, 压入 int 类型的 0 到操作数栈.

instanceof 检查是否可以转型的条件与 checkcast 一致, 区别在于: 1. 对于 null, instanceof 视为不能转型, 压入 0 到操作数栈, 但 checkcast 视为可以转型; 2. instanceof 会弹出检查的引用类型, 并将结果压入操作数栈;

194 monitorenter

进入对象的监视器.

操作数栈栈顶是类型为 reference 类型的 objectref. 每个对象都被分配了一个监视器, 当且仅当监视器有所有者时将被锁定.

当线程执行 monitorenter 时, 弹出操作数栈栈顶的 objectref, 尝试获取分配给 objectref 的 监视器的所有权:

  • 如果 objectref 关联的监视器的计数为0, 当前线程进入监视器, 并设置其计数为1, 当前线程成为监视器是所有者.
  • 如果当前线程已经取得了 objectref 关联的监视器的所有权, 则重入监视器, 并增加其计数.
  • 如果另一个线程已经取得了 objectref 关联的监视器的所有权, 当前线程会阻塞, 直到监视器的计数为0, 再尝试获取其所有权

195 monitorexit

退出对象的监视器.

弹出操作数栈栈顶是类型为 reference 类型的 objectref, 执行 monitorexit 指令的线程必须是 objectref 的监视器的所有者, 减少监视器计数, 如果计数为0, 当前线程不再是 objectref 的监视器的所有者, 而其它被阻塞的线程可以进入监视器.

如果当前线程不是监视器的所有者, 则抛出 IllegalMonitorStateException.

monitorentermonitorexit 一起使用可以实现 synchronized 代码块, 但不用于实现 synchronized 方法, 尽管他们语义上是等价的.

拓展操作码(Extended)

196 wide

使用额外的字节拓展局部变量表的索引.

wide 操作码用于修改另一个操作码的行为, 取决于被修改的字节码, 有两种格式.

第一种格式用于修改字节码 iload, fload, aload, lload, dload, istore, fstore, astore, lstore, dstore, 或 ret.

另一种格式仅用于修改 iinc.

无论哪种情况, wide 操作码后面紧跟着的都是被修改的操作码, 随后是两个字节表示无符号16位数字索引, 指向当前栈帧的局部变量表.

wide 修改的是 lload, dload, lstore, dstore时, 索引加一的也仍然局部变量表的索引.

在第二种格式中, 紧跟着局部变量表索引后的是两个字节表示的有符号16位数字常量.

wide 指令实际上是将被修改的指令视为操作数, 从而改变指令的性质, 其嵌入的指令不能直接运行, 也不能成为控制跳转的目标.

197 multianewarray

创建多维数组.

操作码后有两个字节的立即数构成一个无符号的16位数索引指向常量池中一个类, 数组或接口类型的符号引用.

随后是一个字节的无符号数, 表示要创建的数组的维度, 必须大于等于1.

运行时, 符号引用已解析, 解析的结果必须一个数组类的类型, 其维度大于或等于指令中的维度立即数.

操作数栈栈顶依次是各个维度的数组的长度, 其入栈顺序与维度增长顺序相同, 例如:

new String[1][2];

javap -v 显示字节码为:

 0: iconst_1
 1: iconst_2
 2: multianewarray #25,  2   // class "[[Ljava/lang/String;"

一个新的多维数组将在堆上被创建, 如果其中有数组的长度为0, 其子维度数组将不再分配.

每个维度的元素都是其子维度的元素的数组类型, 最后一个维度的元素被初始化为对应的类型的默认值, 其它维度被初始化为 null.

如果被创建的数组只有一个维度, 通常使用 newarrayanewarray 更有效.

下面这段代码是常量池中的数组类型的维度大于 multianewarray 指令中维度的一个例子.

var array = new String[1][2][];
 0: iconst_1
 1: iconst_2
 2: multianewarray #25,  2   // class "[[[Ljava/lang/String;"

在这种情况下, 没有长度的维度将不会被创建, 对应的, 如果只创建了一维, 尽管类型是更多维度的, 那将会使用 newarrayanewarray 指令.

var array = new String[1][];
 0: iconst_1
 2: anewarray # 25         // class "[Ljava/lang/String;"

注意 multianewarray 指向的常量池中的类型是创建数组对象的类型, 而 newarrayanewarray 指向的常量池中的类型是数组元素的类型.

198 ifnull

引用为 null 的分支.

操作码后有两个字节构成的16为无符号数字代表的偏移量.

弹出操作数栈栈顶元素 value, 其类型为 reference, 如果 valuenull, 则跳转相对于 ifnull 指令位置的偏移量之后继续执行, 否则, 继续执行后续的执行.

199 ifnonnull

引用不为 null 的分支.

操作码后有两个字节构成的16为无符号数字代表的偏移量.

弹出操作数栈栈顶元素 value, 其类型为 reference, 如果 value null, 则跳转相对于 ifnonnull 指令位置的偏移量之后继续执行, 否则, 继续执行后续的执行.

200 goto_w

跳转一定偏移量.

操作码后有四个字节构成的32位整数构成的偏移量, 跳转相对于 goto_w 地址的偏移量的位置继续执行.

goto 唯一的不同是跳转的偏移量使用四个字节而不是两个字节表示.

201 jsr_w

class文件版本低于50(对应jdk6)或以下, 才会使用 jsr_w. 更高版本的jdk中已经不再使用.

处理同 jwr, 唯一区别是跟随操作码后的是四个字节的32位的偏移量, 而 jsr 是两个字节.

保留操作码(Reserved Opcodes)

JVM规范中定义了三个保留操作码, 供JVM内部实现, 将来JVM指令集拓展也不会使用保留操作码.

合法的class文件中不应该出现这些操作码, 但与已经加载的JVM交互的调试器或JIT代码生成器等工具可能会碰到这些指令.

202 breakpoint

breakpoint 202 (0xca), 用于debugger实现断点.

254 impdep<i>

impdep1 (0xfe), impdep2 (0xff), 用于在软硬件特定功能实现提供"后门"或 traps.

标签:操作数,指令集,压入,JVM,操作码,value2,value1,类型
From: https://www.cnblogs.com/wymc/p/17513929.html

相关文章

  • 深入学习 JVM 垃圾回收算法
    博主介绍:✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌......
  • 一个JVM参数,服务超时率降了四分之三
    先说结论:通过优化Xms,改为和Xmx一致,使系统的超时率降了四分之三1.背景一个同事说他负责的服务在一次上线之后超时率增加了一倍2.分析2.1机器的监控首先找了一台机器,看了监控上线后最明显的变化就是CPU使用率变高了2.2上线改动点上线只加了简单的判断条件,按理不应该......
  • JVM之指针压缩
    做java开发的同学一般都比较熟悉JVM,那么关于指针压缩这块内容是不是也了解呢,不熟悉的小伙伴往下看吧。首先说明,本文涉及的JDK版本是1.8,JVM虚拟机是64位的HotSpot实现为准。java对象结构了解指针压缩前,需要先搞懂java的实例对象在JVM虚拟机中内存结构是什么样的。java对象由......
  • 【JVM 方法区 04】
    从线程共享与否的角度划分“运行时数据区结构图”  线程共享区包括:堆、方法区(元空间)他两都会报OOM,现成私有化包括:虚拟机栈、本地方法栈、程序计数器(其中虚拟机栈和本地方法栈会抛StackOverflowError异常,程序计数器不会抛异常),还有一部分叫ThreadLocal一、栈、堆、方法区的交......
  • jvm-第四节垃圾回收器的细节实现
    垃圾回收器串讲及HostSpot的细节实现本篇知识点概况并发标记与三色标记gc并发下漏标问题与不同垃圾回收期下的处理方案(G1,Cms对比)跨代引用安全点与安全区域gc参数(了解)其他的垃圾回收期(了解)并发标记与三色标记三色标记诞生的历史:在三色标记之前有一个标记清除算法,根据可达性,可达设......
  • jvm-第四节垃圾回收器的细节实现
    #垃圾回收器串讲及HostSpot的细节实现本篇知识点概况并发标记与三色标记gc并发下漏标问题与不同垃圾回收期下的处理方案(G1,Cms对比)跨代引用安全点与安全区域gc参数(了解)其他的垃圾回收期(了解)并发标记与三色标记三色标记诞生的历史:在三色标记之前有一个标记清除算法......
  • JVM中的-Xms 、-Xmx 参数该如何设置
    在Java虚拟机(JVM)中,-Xms和-Xmx都是用来设置JVM堆内存大小的参数。其中,-Xms用于设置JVM启动时分配的初始堆内存大小,而-Xmx用于设置JVM堆内存的最大可用空间。默认情况下,-Xms参数的值为物理内存的1/64,-Xmx参数的值为物理内存的1/4。在设置这两个参数时,需要根据具体应......
  • JVM 类加载机制
    加载过程其中验证,准备,解析合称链接加载通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象.验证确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全.准备进行内存分配,为static修饰的类变量分配内存,并设置初始值(0或null).不包含final修饰的静态......
  • JVM_简介
    1.JVM_体系JVM组成部分1.类加载器2.运行时数据区3.执行引擎4.本地方法库JVM执行流程1.类加载器把Java代码转换为字节码2.运行时数据区把字节码加载到内存中,不能直接交给底层系统去执行3.执行引擎将字节码翻译为底层系统指令,再交由CPU去执行4.CPU执行,调用其他语言的本......
  • JVM参数如何配置
    应用服务器配置示例-server-Xmx4g-Xms4g-Xmn256m-XX:PermSize=128m-Xss256k-XX:+DisableExplicitGC-XX:+UseConcMarkSweepGC-XX:+CMSParallelRemarkEnabled-XX:+UseCMSCompactAtFullCollection-XX:LargePageSizeInBytes=128m-XX:+UseFastAccessorMethods-XX:+UseCM......