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直接调用本地方法(通常使用C编写);
PC寄存器:寄存器也是每个线程私有的空间,Java虚拟机会为每一个Java线程创建PC寄存器。在任意时刻,一个Java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined;
执行引擎:它是Java虚拟机的最核心组件之一,它负责执行虚拟机的字节码。现代虚拟机为了提高执行效率,会使用及时编译技术将方法编译成机器码后再执行;
虚拟机参数
Java虚拟机可使用Java_HOME/bin/java程序启动(Java_HOME为JDK的安装目录),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()函数的参数,同时打印了系统的最大可用堆内存,可以使用如下命令行运行这段代码:
从结果可以看出,第一个参数 -Xmx传递给Java虚拟机,生效后,使得系统最大可用堆空间为32MB,参数a则传递给主函数main(),作为应用程序的参数
使用的Eclipse的话,在运行对话框的参数选项卡上,也可以设置这两个参数。上图显示了“程序参数”和“虚拟机参数”两个文本框,将所需的参数填入即可;
辨清Java堆
Java堆是和Java应用程序关系非常密切的内存空间,几乎所有的对象都存放在堆中。并且Java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示地释放;
Java堆的构成(最为常见的)分为:
新生代:存放新生对象或年龄不大的对象;分为eden区,s0区,s1区。s0和s1也被称为from和to区域,它们是两个大小相等,可以互换角色的内存空间老年代:存放老年对象;图示:
绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1,之后,每经过一次新生代回收,对象如果存活,它的年龄就会加1.当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代
示例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实例本身分配在堆中,描述SimpleHeap类的信息存放在方法区,main()函数中s1和s2局部变量存放在栈中,并指向堆中的两个实例
函数调用:出入Java栈
Java栈是一块线程私有的内存空间。和线程关系紧密。线程执行的基本行为是函数调用,每次函数调用的数据都是通过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)
如果将参数增大,那么调用的层数也会增加;
函数嵌套调用的层次在很大程度上由栈的大小决定,栈越大,函数可以支持嵌套调用次数越多;
局部变量表:
它用于保存函数的参数以及局部变量。局部变量表中的变量只在当前函数调用中有效,函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁;
局部变量表在栈帧之中,如果函数的参数和局部变量较多,会使得局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。再来个栗子:两个函数分别包含不等的变量和参数,测试它们谁拥有更深的调用层次;
package stringtest; 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个,不包含局部变量的函数的调用层次更深
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的;