五万字长篇java虚拟机看这篇文章就够了(上)
java虚拟机虚拟机的结构
类加载子系统
类加载子系统负责从文件系统或者网络中加载Class 信息,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。Java堆:
在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。几乎所有的Java对象实例都存放于Java堆中。堆空间是所有线程共享的,这是一块与Java应用密切相关的内存区间。直接内存
Java的NIO库允许Java程序使用直接内存。直接内存是在Java堆外的、直接向系统申请的内存区间。通常,访问直接内存的速度会优于Java堆。因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在Java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。垃圾回收系统
是Java虛拟机的重要组成部分,垃圾回收器可以对方法区、Java堆和直接内存进行回收。其中,Java 堆是垃圾收集器的工作重点。java栈
每一个Java虚拟机线程都有一个私有的 Java栈。一个线程的Java栈在线程创建的时候被创建。Java 栈中保存着帧信息,Java 栈中保存着局部变量、方法参数,同时和Java方法的调用、返回密切相关。
本地方法栈和Java栈非常类似,最大的不同在于Java栈用于Java方法的调用,而本地方
法栈则用于本地方法调用。作为对Java虛拟机的重要扩展,Java虚拟机允许Java直接调用本地方法(通常使用C编写)。PC ( Program Counter)寄存器
PC ( Program Counter)寄存器也是每个线程私有的空间,Java 虚拟机会为每一个 Java线程
创建PC寄存器。在任意时刻,一个Java 线程总是在执行一个方法,这个正在被执行的方法称当前方法。执行引擎
执行引擎是Java虚拟机的最核心组件之一, 它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。
Java虚拟机参数设置
Java虚拟机可以使用JAVA_ HOME/bin/java程序启动(JAVA_ HOME为JDK的安装目录),
一般来说,Java 进程的命令行使用方法如下:
java [-options] class [args...]
其中,
-options表示Java虚拟机的启动参数,
class 为带有main()函数的Java类,
args 表示传递给主函数main(的参数。
如果需要设定特定的Java 虚拟机参数,在options处指定即可。目前,Hotspot 虚拟机支持大量的虚拟机参数,可以帮助开发人员进行系统调优和故障排查。
看下面的示例:
public class SimpleArgs {
public static void main (String[] args) {
for ( int i = 0 ; i < args. length ; i++) {
System. out .println( "参数" + (i + 1 ) + ":" + args[i]);
System. out .println( "-Xmx" + Runtime. getRuntime ().maxMemory() / 1000 / 1000 + "M" );
}
}
}
从结果可以看到,第一个参数-Xmx32m传递给Java虚拟机,生效后,使得系统最大可用堆空间为32MB,参数a则传递给主函数main作为应用程序的参数。java堆
Java堆是和Java应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆中。并且Java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显式地释
根据垃圾回收机制的不同,Java堆有可能拥有不同的结构。最为常见的一种构成是将整个Java堆分为新生代和老年代。新生代包含Eden+Survivor区,survivor区里面分为from和to区,内存回收时,如果用的是复制算法,从from复制到to(我们也教s0,s1),当经过一次或者多次GC之后,存活下来的对象会被移动到老年区 其中,新生代有可能分为eden区、s0区、s1区,s0 和s1也被称为from和to区域,它们是两块大小相等、可以互换角色的内存空间。
在绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1,之后,每经过一次新生代回收, 对象如果存活,它的年龄就会加1。当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。
下面通过一个简单的示例,来展示Java堆、方法区和Java栈之间的关系。
public class SimpleHeap {
private int id ;
public SimpleHeap( int id) {
this . id = id;
}
public void show() {
System. out .println( "My ID is " + id );
}
public static void main(String[] args) {
SimpleHeap s1 = new SimpleHeap( 1 );
SimpleHeap s2 = new SimpleHeap( 2 );
s1.show();
s2.show();
}
}
上述代码声明了一个SimpleHeap类,并在main(函数中创建了两个SimpleHeap实例。此时,各对象和局部变量的存放如图所示。SimpleHeap实例本身分配在堆中,描述SimpleHeap类的信息存储在方法区,main中的s1和s2的局部变量存储在栈中并且指向两个实例。
出入java栈
Java栈是一块线程私有的内存空间。如果说,Java堆和程序数据密切相关,那么Java栈就是和线程执行密切相关的。线程执行的基本行为是函数调用,每次函数调用的数据都是通过Java栈传递的。
Java栈只支持出栈和入栈两种操作。
栈帧
在Java栈中保存的主要内容为栈帧。每一次函数调用, 都会有一个对应的栈帧
被压入Java栈,每一个函数调用结束,都会有一个栈帧被弹出Java栈。
如图所示,
函数1对应栈帧1,函数2对应栈帧2,依此类推。函数1中调用函数2,函数2中调用函数3,函数3中调用函数4。
当函数I被调用时,栈帧1入栈;当函数2被调用时,栈帧2入栈;当函数3被调用时,栈帧3入栈;当函数4被调用时,栈帧4入栈。当前正在执行的函数所对应的帧就是当前的帧(位于栈顶),它保存着当前函数的局部变量、中间运算结果等数据。
当函数返回时,栈帧从Java栈中被弹出。Java 方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。在一个栈帧中,至少要包含局部变量表、操作数栈和帧数据区几个部分。
在一个栈帧中,至少要包含局部变量表、操作数栈和帧数据区几个部分。
提示:由于每次函数调用都会生成对应的栈帧,从而占用一定的栈空间,因此,如果
栈空间不足,那么函数调用自然无法继续进行下去。当请求的栈深度大于最大
可用栈深度时,系统就会抛出StackOverflowError 栈溢出错误。
Java虚拟机提供了参数-Xss来指定线程的最大栈空间,这个参数也直接决定了函数调用的最大深度。
下面的代码是一个递归调用,由于递归没有出口,这段代码可能会出现栈溢出
错误,在抛出错误后,程序打印了最大的调用深度。
public class TestStackDeep {
private static int count = 0 ;
public static void recursion( long a, long b, long c) {
long e= 1 , f= 2 ,g= 3 ,h= 4 ,i= 5 ,k= 6 ,q= 7 ,x= 8 ,y= 9 ,z= 10 ;
count ++;
recursion (a,b,c) ;
}
public static void recursion() {
count ++;
recursion ();
}
public static void main(String args[]) {
try {
recursion ( 1 , 2 , 3 );
} catch (Throwable e) {
System. out .println( "deep of calling = " + count );
e.printStackTrace();
}
}
}
-Xss128k 的参数来执行代码
-Xss256k 的参数来执行代码
可以看到,在进行大约2700次调用后,发生了栈溢出错误,通过增大-Xss的值,可以获得更深的调用层次,尝试使用参数-Xss256K执行上述代码,可能产生如下输出,很明显,调用层次有明显的增加。局部变量表局部变量的剖析
局部变量表用于保存函数的参数以及局部变量。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。
由于局部变量表在栈帧之中,因此,如果函数的参数和局部变量较多,会使得局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。
[示例]下面的代码演示了这种情况,第1个recursion()函数含有3个参数和10个局
部变量,因此,其局部变量表含有13个变量。而第2个recursion()函数不含有任何参数和局部变量。当这两个函数被嵌套调用时,第2个recursion()函数可以拥有更深的调用层次。
public class TestStackDeep {
private static int count = 0 ;
public static void recursion( long a, long b, long c) {
long e= 1 , f= 2 ,g= 3 ,h= 4 ,i= 5 ,k= 6 ,q= 7 ,x= 8 ,y= 9 ,z= 10 ;
count ++;
recursion (a,b,c) ;
}
public static void recursion() {
count ++;
recursion ();
}
public static void main(String args[]) {
try {
recursion ( 1 , 2 , 3 );
} catch (Throwable e) {
System. out .println( "deep of calling = " + count );
e.printStackTrace();
}
}
}
-Xss128k调用无参数的方法
-Xss128k调用有参数的方法
可以看到,在相同的栈容量下,局部变量少的函数可以支持更深的函数调用。
使用jclasslib工具可以更进一步查看函数的局部变量信息。图2.6显示了第一个recursion()函数的最大局部变量表的大小为26个字。因为该函数包含总共13 个参数和局部变量,且都为long型,long 和double在局部变量表中需要占用2个字,其他如int、short、 byte、 对象引用等占用1个字。
第一个方法
第二个方法
可以看到,在Class文件的局部变量表中,显示了每个局部变量的作用域范围、所在槽位的索引(index 列)、变量名(name 列)和数据类型(J 表示long型)。
栈帧中的局部变量表中的槽位是可以重用的,如果一个 局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
[示例]下面的代码显示了局部变量表槽位的复用。
在localvar1()函数中,局部变量a和b都作用到了函数末尾,故b无法复用a所在的位置。而在localvar2()函数中,局部变量a在第16行时不再有效,故局部变量b可以复用a的槽位(1个字)。
我们看到localvar1有三个槽位
我们看到localvar2中槽位1得到复用,b复用了a的槽位
局部变量的回收
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都是不会被回收的。因此,理解局部变量表对理解垃圾回收也有一定帮助。
示例:
public class LocalVarGCTest {
public void localvarGc1() {
byte [] a = new byte [ 6 * 1024 * 1024 ];
System. gc ();
}
public void localvarGc2() {
byte [] a = new byte [ 6 * 1024 * 1024 ];
a = null ;
System. gc ();
}
public void localvarGc3() {
{
byte [] a = new byte [ 6 * 1024 * 1024 ];
}
System. gc ();
}
public void localvarGc4() {
{
byte [] a = new byte [ 6 * 1024 * 1024 ];
}
int c = 10 ;
System. gc ();
}
public void localvarGc5() {
localvarGc1();
System. gc ();
}
public static void main(String[] args) {
LocalVarGCTest ins = new LocalVarGCTest();
ins.localvarGc4();
}
}
上述代码中,每一个localvarGc函数都分配了一块6MB的堆空间,并使用局部变量引用
这块空间。
在localvarGc1中,在申请空间后,立即进行垃圾回收,很明显,由于byte 数组被变量a引用,因此无法回收这块空间。
在localvarGc2中,在垃圾回收前,先将变量a置为null,使byte数组失去强引用,故垃
圾回收可以顺利回收byte数组。
对于localvarGc3, 在进行垃圾回收前,先使局部变量a失效,虽然变量a已经离开了作用域,但是变量a依然存在于局部变量表中,并且也指向这块byte数组,故byte数组依然无法被回收。
对于localvarGc4,在垃圾回收之前,不仅使变量a失效,更是申明了变量c,使变量c复用了变量a的字,由于变量a此时被销毁,故垃圾回收器可以顺利回收byte数组。
对于localvarGc5,它首先调用了localvarGc1很明显,在localvarGc1中 并没有释放
byte数组,但在localvarGc1返回后,它的栈帧被销毁,自然也包含了栈帧中的所有局部变量,故byte数组失去引用,在localvarGc5的垃圾回收中被回收。
可以使用参数-XX:+PrintGC执行上述几个函数,在输出的日志中,可以看到垃圾回收前后堆的大小,进而推断byte数组是否被回收。下 面的输出是函数localvarGc4的运行结果:
从日志中可以看到,堆空间从回收前的10081KB变为回收后的816KB,释放了约很多空间。进而可以推断,byte 数组已被回收释放。操作数栈
操作数栈也是栈帧中重要的内容之一,它主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈也是一个先进后出的数据结构,只支持入栈和出栈两种操作。许多Java字节码指令都需要通过操作数栈进行参数传递。比如iadd指令,它就会在操作数栈中弹出两个整数并进行加法计算,计算结果会被入栈,如图所示,显示了iadd前后操作数栈的变化。
看一个简单的案例
public class Test1 {
public static int add(){
int i = 10 ;
int j = 20 ;
int z = i +j;
return z;
}
}
javap -c -l target/classes/cn/tx/test/Test1public class cn.tx.test.Test1 {
public cn.tx.test.Test1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/tx/test/Test1;
public static int add();
Code:
0: bipush 10
2: istore_0
3: bipush 20
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: istore_2
10: iload_2
11: ireturn
LineNumberTable:
line 6: 0
line 7: 3
line 8: 6
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
3 9 0 i I
6 6 1 j I
10 2 2 z I
}
同时我们也可以通过jcclasslib视图查看
bytecode中的指令可以在下面的指令表中查找到
public class TestUser {
private int count ;
public void test( int a) {
count = count + a;
}
public User initUser( int age, String name) {
User user = new User();
user.setAge(age);
user.setName(name);
return user;
}
public void changeUser(User user, String newName) {
user.setName(newName);
}
}
class User {
private String name ;
private int age ;
public String getName() {
return name ;
}
public void setName(String name) {
this . name = name;
}
public int getAge() {
return age ;
}
public void setAge( int age) {
this . age = age;
}
}
解析test方法0: aload_0 //取this对应的对应引用值,压入操作数栈
1: aload_0//取this对应的对应引用值,压入栈,此时栈中有两个值,都是this对象引用
2: getfield #18 // 引用出栈,通过引用获得对应count的值,并压入栈
5: iload_1 //从局部变量表中取得a的值,压入栈中
6: iadd //弹出栈中的count值和a的值,进行加操作,并将结果压入栈
7: putfield #18 // 经过上一步操作后,栈中有两个值,栈顶为上一步操作结果,栈顶下面是this引用,这一步putfield指令,用于将栈顶的值赋值给引用对象的count字段
10: return //return void
解析initUser方法0: new #23 // class com/justest/test/User 创建User对象,并将引用压入栈
3: dup //复制栈顶值,再次压入栈,栈中有两个User对象的地址引用
4: invokespecial #25 // Method com/justest/test/User."":()V 调用user对象初始化
7: astore_3 //从栈中pop出User对象的引用值,并赋值给局部变量表中user变量
8: aload_3 //从局部变量表中获得user的值,也就是User对象的地址引用,压入栈中
9: iload_1 //从局部变量表中获得a的值,并压入栈中,注意aload和iload的区别,一个取值是对象引用,一个是取int类型数据
10: invokevirtual #26 // Method com/justest/test/User.setAge:(I)V 操作数栈pop出两个值,一个是User对象引用,一个是a的值,调用setAge方法,并将a的值传给这个方法,setAge操作的就是堆中对象的字段了
13: aload_3 //同7,压入栈 14: aload_2 //从局部变量表取出name,压入栈
15: invokevirtual #29 //MethodUser.setName:(Ljava/lang/String;)V 操作数栈pop出两个值,一个是User对象引用,一个是name的值,调用setName方法,并将a的值传给这个方法,setName操作的就是堆中对象的字段了
18: aload_3 //从局部变量取出User引用,压入栈
19: areturn //areturn指令用于返回一个对象的引用,也就是上一步中User的引用,这个返回值将会被压入调用当前方法的那个方法的栈中
int i=0;
i = i++; 解析
0 iconst_0 //把常量0入栈
1 istore_0 //栈顶元素出栈存储在第0个局部变量上
2 iload_0 //从第0个局部变量加载到栈顶
3 iinc 0 by 1 //局部变量值加1并存储
6 istore_0 //把栈顶元素存储到第0个局部变量(此处覆盖加1的值)
int i=0;
i = ++i; 解析
0 iconst_0 //把常量0入栈顶
1 istore_0 //栈顶元素出栈存储在第0个局部变量上
2 iinc 0 by 1 //局部变量值加1并且存储
5 iload_0 //从第0个局部变量加载到栈中
6 istore_0 //把栈顶元素存储在第0个局部变量
常量入栈指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x01
aconst_null
null值入栈。
0x02
iconst_m1
-1(int)值入栈。
0x03
iconst_0
0(int)值入栈。
0x04
iconst_1
1(int)值入栈。
0x05
iconst_2
2(int)值入栈。
0x06
iconst_3
3(int)值入栈。
0x07
iconst_4
4(int)值入栈。
0x08
iconst_5
5(int)值入栈。
0x09
lconst_0
0(long)值入栈。
0x0a
lconst_1
1(long)值入栈。
0x0b
fconst_0
0(float)值入栈。
0x0c
fconst_1
1(float)值入栈。
0x0d
fconst_2
2(float)值入栈。
0x0e
dconst_0
0(double)值入栈。
0x0f
dconst_1
1(double)值入栈。
0x10
bipush
valuebyte
valuebyte值带符号扩展成int值入栈。
0x11
sipushvaluebyte1
valuebyte2
(valuebyte1 << 8) | valuebyte2 值带符号扩展成int值入栈。
0x12
ldc
indexbyte1
常量池中的常量值(int, float, string reference, object reference)入栈。
0x13
ldc_windexbyte1
indexbyte2
常量池中常量(int, float, string reference, object reference)入栈。
0x14
ldc2_windexbyte1
indexbyte2
常量池中常量(long, double)入栈。
局部变量值转载到栈中指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x19
(wide)aload
indexbyte
从局部变量indexbyte中装载引用类型值入栈。
0x2a
aload_0
从局部变量0中装载引用类型值入栈。
0x2b
aload_1
从局部变量1中装载引用类型值入栈。
0x2c
aload_2
从局部变量2中装载引用类型值入栈。
0x2d
aload_3
从局部变量3中装载引用类型值入栈。
0x15
(wide)iload
indexbyte
从局部变量indexbyte中装载int类型值入栈。
0x1a
iload_0
从局部变量0中装载int类型值入栈。
0x1b
iload_1
从局部变量1中装载int类型值入栈。
0x1c
iload_2
从局部变量2中装载int类型值入栈。
0x1d
iload_3
从局部变量3中装载int类型值入栈。
0x16
(wide)lload
indexbyte
从局部变量indexbyte中装载long类型值入栈。
0x1e
lload_0
从局部变量0中装载int类型值入栈。
0x1f
lload_1
从局部变量1中装载int类型值入栈。
0x20
lload_2
从局部变量2中装载int类型值入栈。
0x21
lload_3
从局部变量3中装载int类型值入栈。
0x17
(wide)fload
indexbyte
从局部变量indexbyte中装载float类型值入栈。
0x22
fload_0
从局部变量0中装载float类型值入栈。
0x23
fload_1
从局部变量1中装载float类型值入栈。
0x24
fload_2
从局部变量2中装载float类型值入栈。
0x25
fload_3
从局部变量3中装载float类型值入栈。
0x18
(wide)dload
indexbyte
从局部变量indexbyte中装载double类型值入栈。
0x26
dload_0
从局部变量0中装载double类型值入栈。
0x27
dload_1
从局部变量1中装载double类型值入栈。
0x28
dload_2
从局部变量2中装载double类型值入栈。
0x29
dload_3
从局部变量3中装载double类型值入栈。
0x32
aaload
从引用类型数组中装载指定项的值。
0x2e
iaload
从int类型数组中装载指定项的值。
0x2f
laload
从long类型数组中装载指定项的值。
0x30
faload
从float类型数组中装载指定项的值。
0x31
daload
从double类型数组中装载指定项的值。
0x33
baload
从boolean类型数组或byte类型数组中装载指定项的值(先转换为int类型值,后压栈)。
0x34
caload
从char类型数组中装载指定项的值(先转换为int类型值,后压栈)。
0x35
saload
从short类型数组中装载指定项的值(先转换为int类型值,后压栈)。
将栈顶值保存到局部变量中指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x3a
(wide)astore
indexbyte
将栈顶引用类型值保存到局部变量indexbyte中。
0x4b
astroe_0
将栈顶引用类型值保存到局部变量0中。
0x4c
astore_1
将栈顶引用类型值保存到局部变量1中。
0x4d
astore_2
将栈顶引用类型值保存到局部变量2中。
0x4e
astore_3
将栈顶引用类型值保存到局部变量3中。
0x36
(wide)istore
indexbyte
将栈顶int类型值保存到局部变量indexbyte中。
0x3b
istore_0
将栈顶int类型值保存到局部变量0中。
0x3c
istore_1
将栈顶int类型值保存到局部变量1中。
0x3d
istore_2
将栈顶int类型值保存到局部变量2中。
0x3e
istore_3
将栈顶int类型值保存到局部变量3中。
0x37
(wide)lstore
indexbyte
将栈顶long类型值保存到局部变量indexbyte中。
0x3f
lstore_0
将栈顶long类型值保存到局部变量0中。
0x40
lstore_1
将栈顶long类型值保存到局部变量1中。
0x41
lstore_2
将栈顶long类型值保存到局部变量2中。
0x42
lstroe_3
将栈顶long类型值保存到局部变量3中。
0x38
(wide)fstore
indexbyte
将栈顶float类型值保存到局部变量indexbyte中。
0x43
fstore_0
将栈顶float类型值保存到局部变量0中。
0x44
fstore_1
将栈顶float类型值保存到局部变量1中。
0x45
fstore_2
将栈顶float类型值保存到局部变量2中。
0x46
fstore_3
将栈顶float类型值保存到局部变量3中。
0x39
(wide)dstore
indexbyte
将栈顶double类型值保存到局部变量indexbyte中。
0x47
dstore_0
将栈顶double类型值保存到局部变量0中。
0x48
dstore_1
将栈顶double类型值保存到局部变量1中。
0x49
dstore_2
将栈顶double类型值保存到局部变量2中。
0x4a
dstore_3
将栈顶double类型值保存到局部变量3中。
0x53
aastore
将栈顶引用类型值保存到指定引用类型数组的指定项。
0x4f
iastore
将栈顶int类型值保存到指定int类型数组的指定项。
0x50
lastore
将栈顶long类型值保存到指定long类型数组的指定项。
0x51
fastore
将栈顶float类型值保存到指定float类型数组的指定项。
0x52
dastore
将栈顶double类型值保存到指定double类型数组的指定项。
0x54
bastroe
将栈顶boolean类型值或byte类型值保存到指定boolean类型数组或byte类型数组的指定项。
0x55
castore
将栈顶char类型值保存到指定char类型数组的指定项。
0x56
sastore
将栈顶short类型值保存到指定short类型数组的指定项。
wide指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xc4
wide
使用附加字节扩展局部变量索引(iinc指令特殊)。
通用(无类型)栈操作指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x00
nop
空操作。
0x57
pop
从栈顶弹出一个字长的数据。
0x58
pop2
从栈顶弹出两个字长的数据。
0x59
dup
复制栈顶一个字长的数据,将复制后的数据压栈。
0x5a
dup_x1
复制栈顶一个字长的数据,弹出栈顶两个字长数据,先将复制后的数据压栈,再将弹出的两个字长数据压栈。
0x5b
dup_x2
复制栈顶一个字长的数据,弹出栈顶三个字长的数据,将复制后的数据压栈,再将弹出的三个字长的数据压栈。
0x5c
dup2
复制栈顶两个字长的数据,将复制后的两个字长的数据压栈。
0x5d
dup2_x1
复制栈顶两个字长的数据,弹出栈顶三个字长的数据,将复制后的两个字长的数据压栈,再将弹出的三个字长的数据压栈。
0x5e
dup2_x2
复制栈顶两个字长的数据,弹出栈顶四个字长的数据,将复制后的两个字长的数据压栈,再将弹出的四个字长的数据压栈。
0x5f
swap
交换栈顶两个字长的数据的位置。Java指令中没有提供以两个字长为单位的交换指令。
类型转换指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x86
i2f
将栈顶int类型值转换为float类型值。
0x85
i2l
将栈顶int类型值转换为long类型值。
0x87
i2d
将栈顶int类型值转换为double类型值。
0x8b
f2i
将栈顶float类型值转换为int类型值。
0x8c
f2l
将栈顶float类型值转换为long类型值。
0x8d
f2d
将栈顶float类型值转换为double类型值。
0x88
l2i
将栈顶long类型值转换为int类型值。
0x89
l2f
将栈顶long类型值转换为float类型值。
0x8a
l2d
将栈顶long类型值转换double类型值。
0x8e
d2i
将栈顶double类型值转换为int类型值。
0x90
d2f
将栈顶double类型值转换为float类型值。
0x8f
d2l
将栈顶double类型值转换为long类型值。
0x91
i2b
将栈顶int类型值截断成byte类型,后带符号扩展成int类型值入栈。
0x92
i2c
将栈顶int类型值截断成char类型值,后带符号扩展成int类型值入栈。
0x93
i2s
将栈顶int类型值截断成short类型值,后带符号扩展成int类型值入栈。
整数运算
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x60
iadd
将栈顶两int类型数出栈相加,结果入栈。
0x64
isub
将栈顶两int类型数相减,结果入栈。
0x68
imul
将栈顶两int类型数相乘,结果入栈。
0x6c
ip
将栈顶两int类型数相除,结果入栈。
0x70
irem
将栈顶两int类型数取模,结果入栈。
0x74
ineg
将栈顶int类型值取负,结果入栈。
0x61
ladd
将栈顶两long类型数相加,结果入栈。
0x65
lsub
将栈顶两long类型数相减,结果入栈。
0x69
lmul
将栈顶两long类型数相乘,结果入栈。
0x6d
lp
将栈顶两long类型数相除,结果入栈。
0x71
lrem
将栈顶两long类型数取模,结果入栈。
0x75
lneg
将栈顶long类型值取负,结果入栈。
0x84
(wide)iincindexbyte
constbyte
在局部变量上将整数值constbyte加到indexbyte指定的int类型的局部变量中。
浮点运算
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x62
fadd
将栈顶两float类型数相加,结果入栈。
0x66
fsub
将栈顶两float类型数相减,结果入栈。
0x6a
fmul
将栈顶两float类型数相乘,结果入栈。
0x6e
fp
将栈顶两float类型数相除,结果入栈。
0x72
frem
将栈顶两float类型数取模,结果入栈。
0x76
fneg
将栈顶float类型值取反,结果入栈。
0x63
dadd
将栈顶两double类型数相加,结果入栈。
0x67
dsub
将栈顶两double类型数相减,结果入栈。
0x6b
dmul
将栈顶两double类型数相乘,结果入栈。
0x6f
dp
将栈顶两double类型数相除,结果入栈。
0x73
drem
将栈顶两double类型数取模,结果入栈。
0x77
dneg
将栈顶double类型值取负,结果入栈。
逻辑运算——移位运算
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x78
ishl
左移int类型值。
0x79
lshl
左移long类型值。
0x7a
ishr
算术右移int类型值。
0x7b
lshr
算术右移long类型值。
0x7c
iushr
逻辑右移int类型值。
0x7d
lushr
逻辑右移long类型值。
逻辑运算——按位布尔运算
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x73
iand
对int类型按位与运算。
0x7f
land
对long类型的按位与运算。
0x80
ior
对int类型的按位或运算。
0x81
lor
对long类型的按位或运算。
0x82
ixor
对int类型的按位异或运算。
0x83
lxor
对long类型的按位异或运算。
控制流指令——条件跳转指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x99
ifeqbranchbyte1
branchbyte2
若栈顶int类型值为0则跳转。
0x9a
ifnebranchbyte1
branchbyte2
若栈顶int类型值不为0则跳转。
0x9b
ifltbranchbyte1
branchbyte2
若栈顶int类型值小于0则跳转。
0x9e
iflebranchbyte1
branchbyte2
若栈顶int类型值小于等于0则跳转。
0x9d
ifgtbranchbyte1
branchbyte2
若栈顶int类型值大于0则跳转。
0x9c
ifgebranchbyte1
branchbyte2
若栈顶int类型值大于等于0则跳转。
0x9f
if_icmpeqbranchbyte1
branchbyte2
若栈顶两int类型值相等则跳转。
0xa0
if_icmpnebranchbyte1
branchbyte2
若栈顶两int类型值不相等则跳转。
0xa1
if_icmpltbranchbyte1
branchbyte2
若栈顶两int类型值前小于后则跳转。
0xa4
if_icmplebranchbyte1
branchbyte2
若栈顶两int类型值前小于等于后则跳转。
0xa3
if_icmpgtbranchbyte1
branchbyte2
若栈顶两int类型值前大于后则跳转。
0xa2
if_icmpgebranchbyte1
branchbyte2
若栈顶两int类型值前大于等于后则跳转。
0xc6
ifnullbranchbyte1
branchbyte2
若栈顶引用值为null则跳转。
0xc7
ifnonnullbranchbyte1
branchbyte2
若栈顶引用值不为null则跳转。
0xa5
if_acmpeqbranchbyte1
branchbyte2
若栈顶两引用类型值相等则跳转。
0xa6
if_acmpnebranchbyte1
branchbyte2
若栈顶两引用类型值不相等则跳转。
控制流指令——比较指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0x94
lcmp
比较栈顶两long类型值,前者大,1入栈;相等,0入栈;后者大,-1入栈。
0x95
fcmpl
比较栈顶两float类型值,前者大,1入栈;相等,0入栈;后者大,-1入栈;有NaN存在,-1入栈。
0x96
fcmpg
比较栈顶两float类型值,前者大,1入栈;相等,0入栈;后者大,-1入栈;有NaN存在,-1入栈。
0x97
dcmpl
比较栈顶两double类型值,前者大,1入栈;相等,0入栈;后者大,-1入栈;有NaN存在,-1入栈。
0x98
dcmpg
比较栈顶两double类型值,前者大,1入栈;相等,0入栈;后者大,-1入栈;有NaN存在,-1入栈。
控制流指令——无条件跳转指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xa7
gotobranchbyte1
branchbyte2
无条件跳转到指定位置。
0xc8
goto_wbranchbyte1
branchbyte2
branchbyte3
branchbyte4
无条件跳转到指定位置(宽索引)。
控制流指令——表跳转指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xaa
tableswitchdefaultbyte1
defaultbyte2
defaultbyte3
defaultbyte4
lowbyte1
lowbyte2
lowbyte3
lowbyte4
highbyte1
highbyte2
highbyte3
highbyte4
jump offsets...
通过索引访问跳转表,并跳转。
0xab
lookupswitchdefaultbyte1
defaultbyte2
defaultbyte3
defaultbyte4
npairs1
npairs2
npairs3
npairs4
match offsets
通过键值访问跳转表,并跳转。
控制流指令——异常和finally
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xbf
athrow
抛出异常。
0xa8
jsrbranchbyte1
branchbyte2
跳转到子例程序。
0xc9
jsr_wbranchbyte1
branchbyte2
branchbyte3
branchbyte4
跳转到子例程序(宽索引)。
0xa9
(wide)ret
indexbyte
返回子例程序。
对象操作指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xbb
newindexbyte1
indexbyte2
创建新的对象实例。
0xc0
checkcastindexbyte1
indexbyte
类型强转。
0xc1
instanceofindexbyte1
indexbyte2
判断类型。
0xb4
getfieldindexbyte1
indexbyte2获取对象字段的值。
引用出栈,通过引用获得对应属性的值,并压入栈
0xb5
putfieldindexbyte1
indexbyte2
给对象字段赋值。
0xb2
getstaticindexbyte1
indexbyte2
获取静态字段的值。
0xb3
putstaticindexbyte1
indexbyte2
给静态字段赋值。
数组操作指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xbc
newarray
atype
创建type类型的数组。
0xbd
anewarrayindexbyte1
indexbyte2
创建引用类型的数组。
0xbe
arraylength
获取一维数组的长度。
0xc5
multianewarrayindexbyte1
indexbyte2
dimension
创建dimension维度的数组。
方法调用指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xb7
invokespecialindexbyte1
indexbyte2
编译时方法绑定调用方法。
0xb6
invokevirtualindexbyte1
indexbyte2
运行时方法绑定调用方法。
0xb8
invokestaticindexbyte1
indexbyte2
调用静态方法。
0xb9
invokeinterfaceindexbyte1
indexbyte2
count
0
调用接口方法。
方法返回指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xac
ireturn
返回int类型值。
0xad
lreturn
返回long类型值。
0xae
freturn
返回float类型值。
0xaf
dreturn
返回double类型值。
0xb0
areturn
返回引用类型值。
0xb1
return
void函数返回。
线程同步指令
指令码
操作码(助记符)
操作数
描述(栈指操作数栈)
0xc2
monitorenter
进入并获得对象监视器。
0xc3
monitorexit
释放并退出对象监视器。
帧数据区
Java栈帧需要一些数据来支持常量池解析、正常方法返回和异常处理等。大部分Java字节码指令需要进行常量池访问,在帧数据区中保存着访问常量池的指针,方便程序访问常量池。栈上分配
栈上分配是Java虚拟机提供的一项优化技术,它的基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。
栈_上分配的一个技术基础是进行逃逸分析。逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。如下代码显示了一"个逃逸的对象:
private static User u ;
public static void alloc() {
u = new User ();
u . id = 5 ;
u . name = "geym" ;
}
对象User u是类的成员变量,该字段有可能被任何线程访问,因此属于逃逸对象。而以下代码片段显示了一个非逃逸的对象:
public static void alloc1() {
User u= new User ();
u. id = 5 ;
u. name = "geym" ;
}
在上述代码中,对象User以局部变量的形式存在,并且该对象并没有被alloc()函数返回,或者出现了任何形式的公开,因此,它并未发生逃逸,所以对于这种情况,虚拟机就有可能将User分配在栈上,而不在堆上。
下面这个简单的示例显示了对非逃逸对象的栈上分配。
public class OnStackTest {
public static class User {
public int id = 0 ;
public String name = "" ;
}
public static void alloc() {
User u= new User() ;
u. id = 5 ;
u. name = "geym" ;
}
public static void main(String[] args) throws InterruptedException {
long b = System. currentTimeMillis ();
for ( int i = 0 ; i < 100000000 ; i++) {
alloc ();
long e = System. currentTimeMillis ();
System. out .println(e - b);
}
}
}
上述代码在主函数中进行了1亿次alloc(调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB.如果堆空间小于这个值,就必然会发生GC。使用如下参数运行上述代码:
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-UseTLAB -XX:+EliminateAllocations
这里使用参数-server执行程序,因为在Server模式下,才可以启用逃逸分析。
参数-XX:+DoEscapeAnalysis启用逃逸分析,
-Xmx 10m指定了堆空间最大为10MB,显然,如果对象在堆上分配,必然会引起大量的GC。如果GC真的发生了。
参数-XX:+PrintGC 将打印GC日志。
参数-XX:+EliminateAllocations开启了标量替换(默认打开),允许将对象打散分配在栈上。
比如对象拥有id和name两个字段,那么这两个字段将会被视为两个独立的局部变量进行分配。
参数-XX:-UseTLAB关闭了TLAB。
可以看到,没有任何形式的GC输出,程序就执行完毕了。说明在执行过程中,User 对象的分配过程被优化。如果关闭逃逸分析或者标量替换中任何一个,再次执行程序,就会看到大量的GC日志,说明栈上分配依赖逃逸分析和标量替换的实现。
总结:
对于大量的零散小对象,栈上分配提供了一种很好的对象分配优化策略,栈上分配速度快,并且可以有效避免垃圾回收带来的负面影响,但由于和堆空间相比,栈空间较小,因此对于大对象无法也不适合在栈上分配。类都去哪了?识别方法区
和Java堆一样,方法区是一块所有线程共享的内存区域。它用于保存系统的类信息,比如类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。
public class PermTest {
public static void main(String[] args) {
int i = 0 ;
try {
for (i = 0 ; i < 1000000 ; i++) {
CglibBean bean = new CglibBean( "cn.tx.Perm" +i , new HashMap());
System. out .println(bean);
}
} catch (Exception e){
e.printStackTrace();
}
}
}
class CglibBean {
/**
* 实体Object
*/
public Object object = null ;
/**
* 属性map
*/
public BeanMap beanMap = null ;
public CglibBean() {
super ();
}
@SuppressWarnings ( "unchecked" )
public CglibBean(Map propertyMap) {
this . object = generateBean(propertyMap);
this . beanMap = BeanMap. create ( this . object );
}
public CglibBean(String msg, Map propertyMap) throws ClassNotFoundException {
propertyMap.put(msg, msg.getClass());
this . object = generateBean(propertyMap);
this . beanMap = BeanMap. create ( this . object );
}
/**
* 给bean属性赋值
* @param property 属性名
* @param value 值
*/
public void setValue(String property, Object value) {
beanMap .put(property, value);
}
/**
* 通过属性名得到属性值
* @param property 属性名
* @return 值
*/
public Object getValue(String property) {
return beanMap .get(property);
}
/**
* 得到该实体bean对象
* @return
*/
public Object getObject() {
return this . object ;
}
private Object generateBean(Map propertyMap) {
BeanGenerator generator = new BeanGenerator();
Set keySet = propertyMap.keySet();
for (Iterator i = keySet.iterator(); i.hasNext();) {
String key = (String) i.next();
generator.addProperty(key, (Class) propertyMap.get(key));
}
return generator.create();
}
}
class CglibTest {
@SuppressWarnings ( "unchecked" )
public static void main(String[] args) throws ClassNotFoundException {
// 设置类成员属性
HashMap propertyMap = new HashMap();
propertyMap.put( "id" , Class. forName ( "java.lang.Integer" ));
propertyMap.put( "name" , Class. forName ( "java.lang.String" ));
propertyMap.put( "address" , Class. forName ( "java.lang.String" ));
// 生成动态 Bean
CglibBean bean = new CglibBean(propertyMap);
// 给 Bean 设置值
bean.setValue( "id" , new Integer( 123 ));
bean.setValue( "name" , "454" );
bean.setValue( "address" , "789" );
// 从 Bean 中获取值,当然了获得值的类型是 Object
System. out .println( " >> id = " + bean.getValue( "id" ));
System. out .println( " >> name = " + bean.getValue( "name" ));
System. out .println( " >> address = " + bean.getValue( "address" ));
// 获得bean的实体
Object object = bean.getObject();
// 通过反射查看所有方法名
Class clazz = object.getClass();
Method[] methods = clazz.getDeclaredMethods();
for ( int i = 0 ; i < methods. length ; i++) {
System. out .println(methods[i].getName());
}
}
}
在JDK 1.6、JDK 1.7中,方法区可以理解为永久区(Perm)。永久区可以使用参数
-XX:PermSize和-XX:MaxPermSize指定,默认情况下,-XX:MaxPermSize 为64MB。一个大的永久区可以保存更多的类信息。如果系统使用了一些动态代理,那么有可能会在运行时生成大量的类,如果这样,就需要设置一个合理的永久区大小,确保不发生永久区内存溢出。
-XX:+PrintGCDetails -XX:PermSize=5M -XX:MaxPermSize= 5m
这里指定了初始永久区5MB,最大永久区5MB,即当5MB空间耗尽时,系统将抛出内存溢出。
在JDK 1.8中,永久区已经被彻底移除。取而代之的是元数据区,元数据区大小可以使
参数-XX:MaxMetaspaceSize指定(一个大的元数据区可以使系统支持更多的类),这是一块外的直接内存。与永久区不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系内存
运行参数: -XX:MetaspaceSize=5m -XX:MaxMetaspaceSize=40m
通过Visual VM也可以查看到移除情况。
常用的java虚拟机参数
要诊断虚拟机,我们就要学习如何对Java虚拟机进行最基本的配置和跟踪。本章将主要介绍一些常用的Java虛拟机参数,它们可以对系统进行跟踪和配置,对系统故障诊断、性能优化有着重要的作用。跟踪调试参数
Java的一大特色就是 支持自动的垃圾回收(GC) ,但是有时候,如果垃圾回收频繁出现,
或者占用了太长的CPU时间,就不得不引起重视。此时,就需要一些跟踪 参数来进一步甄别垃圾回收器的效率和效果。
最简单的一个GC参数是-XX:+PrintGC,使用这个参数启动Java虚拟机后,只要遇到GC,
就会打印日志,如下所示:[GC 4793K->377K (15872K),0. 0006926 secs]
[GC 4857K->377K(15936K), 0. 0003595 secs]
[GC 4857K->377K(15936K), 0.0001755 secs]
[GC 4857K->377K (15936K),0. 0001957 secs]
该日志显示,共进行了4次GC,每次GC占用一行,在GC前,
堆空间使用量(已占用)约为4MB,
GC后,堆空间使用量(已占用)为377KB,
当前可用的堆空间总和(空闲)约为16MB ( 15936KB)。最后,显示的是本次GC所花费的时间。
如果需要更加详细的信息,则可以使用-XX:+PrintGCDetails参数。它的输出可能如下:[GC (System.gc()) [PSYoungGen: 4957K->856K(76288K)] 4957K->864K(251392K), 0.0028907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 856K->0K(76288K)] [ParOldGen: 8K->629K(175104K)] 864K->629K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0057303 secs] [Times: user=0.16 sys=0.00, real=0.01 secs]
[GC (System.gc()) [PSYoungGen: 2334K->0K(76288K)] 2964K->629K(251392K), 0.0010263 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 0K->0K(76288K)] [ParOldGen: 629K->629K(175104K)] 629K->629K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0024450 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 3645K->96K(76288K)] 4275K->725K(251392K), 0.0009841 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 96K->0K(76288K)] [ParOldGen: 629K->624K(175104K)] 725K->624K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0060740 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (System.gc()) [PSYoungGen: 3645K->96K(76288K)] 4270K->720K(251392K), 0.0007587 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 96K->0K(76288K)] [ParOldGen: 624K->624K(175104K)] 720K->624K(251392K), [Metaspace: 3391K->3391K(1056768K)], 0.0028285 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (System.gc()) [PSYoungGen: 4956K->64K(76288K)] 5580K->688K(251392K), 0.0006692 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 64K->0K(76288K)] [ParOldGen: 624K->624K(175104K)] 688K->624K(251392K), [Metaspace: 3405K->3405K(1056768K)], 0.0062451 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 76288K, used 1966K [0x000000076b380000, 0x0000000770880000, 0x00000007c0000000)
eden space 65536K, 3% used [0x000000076b380000,0x000000076b56b9e0,0x000000076f380000)
from space 10752K, 0% used [0x000000076f380000,0x000000076f380000,0x000000076fe00000)
to space 10752K, 0% used [0x000000076fe00000,0x000000076fe00000,0x0000000770880000)
ParOldGen total 175104K, used 624K [0x00000006c1a00000, 0x00000006cc500000, 0x000000076b380000)
object space 175104K, 0% used [0x00000006c1a00000,0x00000006c1a9c2d8,0x00000006cc500000)
Metaspace used 3419K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 371K, capacity 388K, committed 512K, reserved 1048576K
[GC (System.gc()) [PSYoungGen: 4957K->856K(76288K)] 4957K->864K(251392K), 0.0028907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2. PSYoungGen 是指GC发生的区域,还有一个ParOldGen
3. 4957K->856K(76288K) ,这三个数字分别对应GC之前占用年轻代的大小,GC之后年轻代占用,以及整个年轻代的大小。
4. 4957K->864K(251392K) ,这三个数字分别对应GC之前占用堆内存的大小,GC之后堆内存占用,以及整个堆内存的大小。
5. 0028907 是该时间点GC占用耗费时间。
-Xms --jvm堆的最小值
-Xmx --jvm堆的最大值
-XX:MaxNewSize --新生代最大值
-XX:MaxPermSize=1028m --永久代最大值
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳
-XX:+PrintGCDetails --打印出GC的详细信息
-verbose:gc --开启gc日志
-Xloggc:d:/gc.log -- gc日志的存放位置
-Xmn -- 新生代内存区域的大小
-XX:SurvivorRatio=8 --新生代内存区域中Eden和Survivor的比例系统参数查看
参数-XX:+PrintVMOptions可以在程序运行时,打印虚拟机接受到的命令行显式参数。其输
-XX:+PrintVMOptions
参数-XX:+PrintCommandLineFlags可以打印传递给虚拟机的显式和隐式参数,隐式参数未必是通过命令行直接给出的,它可能是由虚拟机启动时自行设置的,使用-XX:+PrintCommandL ineFlags
-XX:InitialHeapSize=266658240 -XX:MaxHeapSize=4266531840 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesInpidualAllocation -XX:+UseParallelGC堆的参数配置最大堆和初始堆的设置
1.8后永久代变成matespace在jvm之外。主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,不受MaxPermSize控制
当Java进程启动时,虚拟机就会分配一块初始堆空间,可以使用参数-Xms指定这块空间的大小。一般来说,虚拟机会尽可能维持在初始堆空间的范围内运行。但是如果初始堆空间耗尽,虚拟机将会对堆空间进行扩展,其扩展上限为最大堆空间,最大堆空间可以使用参数-Xmx指定。
-Xms:初始堆大小
-Xmx:最大堆大小
案例
public class HeapAlloc {
public static void main(String[] args) {
System. out .print( "maxMemory=" );
System. out .println(Runtime. getRuntime ().maxMemory() + " bytes" );
System. out .print( "free mem=" );
System. out .println(Runtime. getRuntime ().freeMemory() + " bytes" );
System. out .print( "total mem=" );
System. out .println(Runtime. getRuntime ().totalMemory() + " bytes" );
byte [] b = new byte [ 1 * 1024 * 1024 ];
System. out .println( "分配了1M空间给数组" );
System. out .print( "maxMemory=" );
System. out .println(Runtime. getRuntime ().maxMemory() + " bytes" );
System. out .print( "free mem=" );
System. out .println(Runtime. getRuntime ().freeMemory() + "bytes" );
System. out .print( "total mem=" );
System. out .println(Runtime. getRuntime ().totalMemory() + " bytes" );
b = new byte [ 4 * 1024 * 1024 ];
System. out .println( "分配了4M空间给数组" );
System. out .print( "maxMemory=" );
System. out .println(Runtime. getRuntime ().maxMemory() + " bytes" );
System. out .print( "free mem=" );
System. out .println(Runtime. getRuntime ().freeMemory() + " bytes" );
System. out .print( "total mem=" );
System. out .println(Runtime. getRuntime ().totalMemory() + " bytes" );
}
}
参数:
-XX:+PrintGCDetails -XX:+PrintCommandLineFlags -Xmx20m
上述代码首先在第4~9行打印了基本的系统信息,包括最大可用内存、当前空闲内存和当前总内存。
接着,在第10行申请了1MB内存空间,显然,这块空间将在堆上分配。
在第12~18行,同样打印了最大可用内存、当前空闲内存和当前总内存。
接着,在第20行再次申请了4MB空间。最后,同样打印了这3个参数。
根据前 文的介绍很容易让人想到,这里的最大可用内存就是指-Xmx的取值,当前总内存应该不小于-Xms的设定,因为当前总内存总是在-Xms和-Xmx之间,从-Xms开始根据需要向上增长。而当前空闲内存应该是当前总内存减去当前已经使用的空间。但实际也会很快就能发现中间的偏差。
可以看到,当前的最大内存由-XX:MaxHeapSize 20971520指定,它正好是20* 1024* 1024=20971520字节。而打印的最大可用内存仅仅为20316160字节,比设定值略少。这是因为分配给堆的内存空间和实际可用的内存空间并非一个 概念。由于垃圾回收的需要,虚拟机会对堆空间进行分区管理,不同的区域采用不同的回收算法,一些算法会使用空间换时间的策略工作。
提示:在实际工作中,也可以直接将初始堆-Xms与最大堆-Xmx设置相等。这样的好处是可以减少程序运行时进行的垃圾回收次数,从而提高程序的性能。新生代的配置-XX: SurvivorRatio
参数-Xmn可以用于设置新生代的大小。设置一个较大的新生代会减小老年代的大小,这个参数对系统性能以及GC行为有很大的影响。
新生代的大小一般般设置为整个堆空间的1/3到1/4左右。
SurvivorRatio定义了新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10
参数-XX:SurvivorRatio用来设置新生代中eden空间和from/to 空间的比例关系,它的含义
如下:
-XX: SurvivorRatio=eden/ from=eden/to
考察以下这段简单的Java程序,它连续向系统请求10MB空间(每次申请1MB)。
public class NewSizeDemo {
public static void main(String[] args) {
byte [] b = null ;
for ( int i = 0 ; i < 10 ; i++) {
b = new byte [ 1 * 1024 * 1024 ];
}
}
}
使用不同的堆分配参数执行这段程序,虚拟机的行为表现受到堆空间分配的影响
使用-Xmx20m -Xms20m -Xmn2m -XX:SurvivorRatio=2 -XX:+PrintGCDetails运行
这里eden与from的比值为2比1,故eden区为1024KB。总可用的新生代为1024KB+512KB=1536KB,而新生代总大小为1024KB+512KB+512KB =2048KB=2MB。
由于eden区无法容纳任何一个程序中分配的1MB数组,故触发了一次新生代GC,对eden区进行了部分回收,同时,这个偏小的新生代无法为1MB数组预留空间,故所有的数组都分配在老年代,老年代最终占用18432KB空间。
使用-Xmx20m -Xms20m -Xmn8m -XX:SurvivorRatio=2 -XX:+PrintGCDetails运行
在这个参数下,由于eden区有足够的空间,因此所有的数组都首先分配在eden区。但eden区并不足以预留全部10MB的空间,故在程序运行期间,出现了3次新生代GC。由于程序中每申请一次空间,也同时废弃了,上一次申请的内存(上次申请的内存失去了引用),故在新生代GC中,有效回收了这些失效的内存。最终结果是:所有的内存分配都在新生代进行,通过GC保证了新生代有足够的空间,而老年代没有为这些数组预留任何空间,只是在GC过程中,部分新生代对象晋升到老年代。
使用-Xmx30m -Xms30m -Xmn20m -XX:SurvivorRatio=8 -XX:+PrintGCDetails运行
在这次执行中,由于新生代使用20MB空间,其中eden区占用了16384KB,完全满足10MB
数组的分配,因此所有的分配行为都在eden直接进行,且没有触发任何GC行为。因此from/to和老年代tenured的使用率都为0。
由此可见,不同的堆分布情况,对系统执行会产生一定影响。在实际工作中,应该根据系统的特点做合理的设置,基本策略是:尽可能将对象预留在新生代,减少老年代GC的次数(在本例中的第一种情况, 对象都分配在老年代,显然为后续的老年代GC埋下了伏笔)。-XX:NewRatio=老年代/新生代
除了可以使用参数-Xmn指定新生代的绝对大小外,还可以使用参数-XX:NewRatio来设置新生代和老年代的比例
使用-Xmx20m -Xms20m -XX:NewRatio=2 -XX:+PrintGCDetails运行
此时,因为堆大小为20MB。新生代和老年代的比为1比2。故新生代大小为20MB*1/3= 6MB左右,老年代为13MB左右。
由于在新生代GC时,from/to 空间不足以容纳任何一个1MB数组,影响了新生代的正常回收,故在新生代回收时需要老年代进行空间担保。因此,导致两个1MB数组进入老年代(在新生代GC时,尚有1MB数组幸存,理应进入from/to, 而from/to只有640KB,不足以容纳)。
堆的分配参数示意图。
堆溢出参数
在Java程序的运行过程中,如果堆空间不足,则有可能抛出内存溢出错误(OutOfMemory),
简称为OOM。如下文字显示了典型的堆内存溢出:
一旦发生这类问题,系统就会被迫退出。如果发生在生产环境,可能会引起严重的业务中断。为了能够不断改善系统,避免或减少这类错误的发生,需要在发生错误时,获得尽可能多的现场信息,以帮助研发人员排查现场问题。
Java 虚拟机提供了参数-XX:+HeapDumpOnOutOfMemoryError,使用
该参数,可以在内存溢出时导出整个堆信息。和它配合使用的还有-XX:HeapDumpPath,可以指定导出堆的存放路径。
在很多java所写的服务器脚本中我们可以看到,如nacos
演示案例:
public class DumpOOM {
public static void main(String[] args) {
Vector v= new Vector () ;
for ( int i= 0 ; i< 25 ; i++)
v.add( new byte [ 1 * 1024 * 1024 ]) ;
}
}
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
可以看到,虚拟机将当前的堆导出,并保存到D:/a.dump文件下。使用MAT等工具打开该文件进行分析,如图所示,可以很容易地找到这些byte数组和保存它们的Vector 对象实例。
非堆内存的参数设置方法区配置
在JDK 1.8中,永久区被彻底移除,使用了新的元数据区存放类的元数据。默认情况下,
元数据区只受系统可用内存的限制,但依然可以使用参数-XX:MaxMetaspaceSize指定永久区的值大小
16g栈配置
在Java虚拟机中可以使用-Xss参数指定线程的栈最大大小直接内存配置
直接内存也是Java程序中非常重要的组成部分,特别是在NIO被广泛使用后,直接内存的使用也变得非常普遍。直接内存跳过了Java堆,使Java程序可以直接访问原生堆空间,因此,从一定程度上加快了内存空间的访问速度。但是,武断地认为使用直接内存一定可以提高内存访问速度也是不正确的。
最大可用直接内存可以使用参数-XX:MaxDirectMemorySize设置,如不设置,默认值为最大堆空间,即-Xmx。当直接内存使用量达到-XX:MaxDirectMemorySize时,就会触发垃圾回收,
如果垃圾回收不能有效释放足够空间,直接内存溢出依然会引起系统的OOM。
[示例]一般来说,直接内存的访问速度(读或者写)会快于堆内存。下面的代码统计
了对直接内存和堆内存的读写速度。
public class AccessDirectBuffer {
public void directAccess() {
long starttime = System. currentTimeMillis ();
ByteBuffer b = ByteBuffer. allocateDirect ( 500 );
for ( int i = 0 ; i < 100000 ; i++) {
for ( int j = 0 ; j < 99 ; j++)
b.putInt(j);
b.flip();
for ( int j = 0 ; j < 99 ; j++)
b.getInt();
b.clear();
}
long endtime = System. currentTimeMillis ();
System. out .println( "testDi rectWrite:" + (endtime - starttime));
}
public void bufferAccess() {
long starttime = System. currentTimeMillis ();
ByteBuffer b = ByteBuffer. allocate ( 500 );
for ( int i = 0 ; i < 100000 ; i++) {
for ( int j = 0 ; j < 99 ; j++)
b.putInt(j);
b.flip();
for ( int j = 0 ; j < 99 ; j++)
b.getInt();
b.clear();
}
long endtime = System. currentTimeMillis ();
System. out .println( "testBufferwrite:" + (endtime - starttime));
}
public static void main(String[] args) {
AccessDirectBuffer alloc = new AccessDirectBuffer();
alloc.bufferAccess();
alloc.directAccess();
alloc.bufferAccess();
alloc.directAccess();
}
}
程序中,对bufferAccess()和directACcess()方法分别进行了两次调用,第一次视为热身代码,这里忽略其输出,只关注第2次调用的输出结果。从结果可以看出,直接内存的访问比堆内存快很多。
虽然在访问读写上直接内存有较大的优势,但是在内存空间申请时,直接内存毫无优势
示例代码:
public class AllocDirectBuffer {
public void directAllocate() {
long starttime = System. currentTimeMillis ();
for ( int i = 0 ; i < 200000 ; i++) {
ByteBuffer b = ByteBuffer. allocateDirect ( 1000 );
}
long endtime = System. currentTimeMillis ();
System. out .println( "directAllocate:" + (endtime - starttime));
}
public void bufferAllocate() {
long starttime = System. currentTimeMillis ();
for ( int i = 0 ; i < 200000 ; i++) {
ByteBuffer b = ByteBuffer. allocate ( 1000 );
}
long endtime = System. currentTimeMillis ();
System. out .println( "bufferAllocate:" + (endtime - starttime));
}
public static void main(String[] args) {
AllocDirectBuffer alloc = new AllocDirectBuffer();
alloc.bufferAllocate();
alloc.directAllocate();
}
}
由此,可以得出结论:直接内存适合申请次数较少、访问较频繁的场合。如果内存空间本身需要频繁申请,则并不适合使用直接内存。虚拟机的工作模式Server和Client
目前的Java虚拟机支持Client和Server两种运行模式。使用参数-client可以指定使用Client模式,使用参数-server可以指定使用Server 模式。默认情况下,虚拟机会根据当前计算机系统环境自动选择运行模式。使用-version 参数可以查看当前的模式,如下所示:
与Client模式相比,Server模式的启动比较慢,因为Server模式会尝试收集更多的系统性能信息,使用更复杂的优化算法对程序进行优化。因此,当系统完全启动并进入运行稳定期后,Server模式的执行速度会远远快于Client 模式。
所以,对于后台长期运行的系统,使用-server参数启动对系统的整体性能可以有不小的帮助。但对于用户界面程序,运行时间不长,又追求启动速度,Client 模式也是不错的选择。
从发展趋势上看,未来64位系统必然会逐步取代32位系统,而在64位系统中虚拟机更倾向于使用Server模式运行。
五万字长篇-java虚拟机看这篇文章就够了(中)
五万字长篇-java虚拟机看这篇文章就够了(下)
Git版本控制学习总结