Java虚拟机的基本结构
- 方法区:除了类的信息外,可能还会存放运行时常量池信息,包括字符串,字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
- Java堆:它会在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。几乎所有的Java对象实例都存放于Java堆中。堆空间是所有线程共享的,这是一块与Java应用密切相关的内存区间;
- 直接内存:Java的NIO库允许Java程序使用直接内存。直接内存是在Java堆外的,直接向系统申请的内存区间。通常,访问直接内存的速度会优于Java堆。读写频繁的场合使用此内存为优。直接内存在Java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存;
Java 栈:每个 Java 虚拟机线程都有一个私有的 Java 栈。一个线程的 Java 栈在线程创建的时候被创建。栈中保存着帧信息,局部变量,方法参数,同时和 Java 方法的调用,返回密切相关;
本地方法栈:该栈与 Java 栈类似,不同在于 Java 栈用于 Java 方法的调用,而本地方法栈用于本地方法的调用。 Java 虚拟机允许 Java 直接调用本地方法(通常使用 C 编写);
PC 寄存器:寄存器也是每个线程私有的空间, Java 虚拟机会为每一个 Java 线程创建 PC 寄存器。在任意时刻,一个 Java 线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法, PC 寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么 PC 寄存器的值就是 undefined;
执行引擎:它是 Java 虚拟机的最核心组件之一,它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率,会使用及时编译技术将方法编译成机器码后再执行;
虚拟机参数
java [-options] class [args....] |
-optionsb表示Java虚拟机的启动参数,class为带有main()函数的Java类,args表示传递给主函数main()的参数。设定特定的Java虚拟机参数,在options处指定即可 |
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");
}
}
这段代码打印了传递给
main()
函数的参数,同时打印了系统的最大可用堆内存,可以使用如下命令行运行这段代码:
辨清Java堆
Java 堆的构成 ( 最为常见的 ) 分为:
- 新生代:存放新生对象或年龄不大的对象;分为eden区,s0区,s1区。s0和s1也被称为from和to区域,它们是两个大小相等,可以互换角色的内存空间
- 老年代:存放老年对象;
- 图示:
示例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();
}
}
函数调用:出入Java栈
Java 栈是一块先进后出的数据结构,只支持出栈和入栈两种操作;栈中保存的主要内容为栈帧。
- 每次函数调用,都会有一个对应的栈帧被压入Java栈,每一个函数调用结束,都会有一个栈帧被弹出Java栈;函数1对应栈帧1,函数2对应栈帧2,依此类推;
- 函数1调用函数2,函数2调用函数3,......但函数1被调用时,栈帧1入栈;但函数2被调用时,栈帧2入栈,.....。当前正在执行的函数所对应的帧就是当前帧(位于栈顶),它保存着当前函数的局部变量,中间运算结果等数据;
- 当函数返回时,栈帧从Java栈中被弹出。返回方式有两种,正常的函数返回return,另一种是抛出异常;
- 在一个栈帧中,至少包含局部变量表,操作数栈和帧数据区几个部分;
- 图示:
-
由于每次函数调用都会生成对应的栈帧,从而占用一定的栈空间。
如果栈空间不足,那么函数调用自然无法进行。当请求的栈深度大于最大可用栈深度时,系统就会抛出StackOverflowError栈溢出错误
public class TestStackDeep {
private static int count = 0;
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args) {
try{
recursion();
}catch(Throwable e){
System.out.println("deep of calling = " + count);
e.printStackTrace();
}
}
}
使用参数-Xss128K执行此段代码,部分输出结果为:
deep of calling = 2741
java.lang.StackOverflowError
athey.up2.TestStackDeep.recursion(TestStackDeep.java:19)
athey.up2.TestStackDeep.recursion(TestStackDeep.java:20)
如果将参数增大,那么调用的层数也会增加;
函数嵌套调用的层次在很大程度上由栈的大小决定,栈越大,函数可以支持嵌套调用次数越多;
局部变量表:
局部变量表在栈帧之中,如果函数的参数和局部变量较多,会使得局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。再来个栗子 :两个函数分别包含不等的变量和参数,测试它们谁拥有更深的调用层次;
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(0L,0L,0L); recursion(); }catch(Throwable e){ System.out.println("deep of calling = " + count); e.printStackTrace(); } } }
//两个函数都使用-Xss128K参数测试。第2个,不包含局部变量的函数的调用层次更深
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的;
public void localvar1(){
int a = 0;
System.out.println(a);
int b = 0;
}
public void localvar2(){
{
int a = 0;
System.out.println(a);
}
int b = 0;
}
通过jclasslib查看:
localvar1()方法局部变量槽位:
localvar2()方法局部变量槽位:
局部变量表中的变量也是重要的垃圾回收根节点,被局部变量表中直接或间接引用的对象都是不会被回收的;
又是栗子:演示局部变量对垃圾回收的影响
public class LocalVarGC {
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) {
LocalVarGC ins = new LocalVarGC();
ins.localvarGc1();
}
}
分析:
在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”执行这些函数,下面的结果是localvarGc4()的:
[GC6819K->536K(125952K), 0.0078666 secs]
[Full GC 536K->461K(125952K), 0.0302894 secs]//堆空间从536K,变为461K
操作数栈:
该栈也是先进后出的数据结构。许多 Java 字节码指令都需要通过此栈进行参数传递;
说明:iadd 指令,它会在操作数栈中弹出两个整数并进行加法计算,计算结果会被入栈
帧数据区
在帧数据区保存着访问常量池的指针,帮助Java字节码指令进行常量池访问;此外,当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去。对于异常处理,虚拟机要有一个异常处理表,方便在发生异常的时候找到处理异常的代码,因此异常处理表也是帧数据区中重要的一部分。一个典型的
异常处理表:
它表示字节码偏移量4~16字节可能排除任意异常,如果遇到异常,则跳转到字节码偏移19处执行;
当方法排除异常时,虚拟机就会查找类似的异常表来进行处理,如果无法在异常表中找到合适的处理方法,则会结束当前函数调用,返回调用函数,并在调用函数中抛出相同的异常,并查找调用函数的异常表进行处理;
栈上分配
说明:它是 Java 虚拟机提供的一项优化技术;
基本思想:对于线程私有的对象(指不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配到堆;
好处:函数调用完毕自动销毁,不需要垃圾回收器介入,提高系统性能;
目的:判断对象的作用域是否有可能逃逸出函数体
举个栗子:下面的代码显示了一个逃逸对象:
private static User u; public static void alloc(){ u = new User(); u.id = 5; u.name = "geym"; }
//对象User u是类的成员变量,该字段有可能被任何线程访问,因此属于逃逸对象
//再次举个栗子:以下代码片段显示了一个非逃逸对象:
public static void alloc(){ User y= 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) { long b = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { alloc(); } long e = System.currentTimeMillis(); System.out.println(e-b); } }
//这段代码在主函数进行了一亿次alloc()调用进行对象创建,由于User对象实例需要占据约16字节的空间,因此累计分配空间达到将近1.5GB。如果堆空间小于这个值,就会发生GC;