jvm运行时数据区域
JVM主要由类加载器子系统、运行时数据区(内存空间)、执行引擎以及与本地方法接口等组成。其中运行时数据区又由方法区、堆、Java栈、PC寄存器、本地方法栈,直接内存组成。其中本地方法栈与直接内存由操作系统提供,jvm负责调用。
PC寄存器
PC寄存器又称作程序计数器,每一个线程都是私有的。用于保存当前线程正在执行的位置,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被暂停的线程的程序当前执行到的位置必然要保存下来,以便用于被暂停的线程恢复执行时再按照被暂停时的指令地址继续执行下去。
堆(Heap)
java堆是Java虚拟机所管理的内存中最大的一块。Java堆是所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象都存在这个内存空间中(随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配,标量替换优化技术的出现,所以并不是绝对的所有而是机会所有)。
Java堆是自动进行管理的,通过垃圾回收系统自动的清理java堆中的垃圾对象,所以有时候被称为‘GC堆’。
从内存回收的角度来看Java堆的分类可以细分为新生代和老年代。在细致一点新生代分为eden区,s0区,s1区
从内存分配的角度来看,线程共享的Java堆中可能划分出来多个线程私有的分配缓冲区。不过无论如何划分,无论哪个区域,存储的都是对象实例,进一步划分是为了更好的回收内存,或者更快的分配内存。
Java堆可以是物理上不连续的内存空间,只需要逻辑上连续即可。Java堆的分配上是扩展的通过-Xmx和-Xms来设定最大值和最小值来控制。当堆的空间动态扩展超过最大值的时候会抛出异常OutOfMemoryError
Java虚拟机栈(Stack)
Java虚拟机栈是线程私有的,它的生命周期与线程相同。它所描述的是Java方法执行的内存模型,每个方法执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量,操作栈、动态链接、方法出口等信息。每一个方法被调用直到执行完成的过程,就对应着一个栈帧在Java虚拟机栈中从入栈到出栈的过程。
如图:方法1对应栈帧1,方法2,3,4分别对应栈帧2,3,4
方法1执行时,栈帧1入栈
方法1调用方法2时,栈帧2入栈
方法2调用方法3时,栈帧3入栈
方法3调用方法4,栈帧4入栈
方法4执行完毕,栈帧4出栈
方法3执行完毕,栈帧3出栈
方法2执行完毕,栈帧2出栈
方法1执行完毕,栈帧1出栈
Java出栈有两种方式,一种正常执行完毕,另外一种就是抛出异常,但是不管怎么样都会出栈。
Java栈是一块内存区域,是有空间大小限制的。栈帧的局部变量都会申请内存空间,超过了内存的限制就会抛出异常OutOfMemoryError。当线程请求的栈深度超过了Java虚拟机允许的深度会抛出异常Stack Overflow(-Xss表示来指定线程最大的栈空间)
本地方法栈
本地方法栈与虚拟机栈发挥的作用非常类似,区别在于Java虚拟机栈调用的是Java方法,本地方法栈调用的本地(Native)方法
方法区
方法区和堆一样,也是一块共享内存空间,它用于存储已经被Java虚拟机加载的类的信息,常量、静态变量、字段,方法等。方法区的大小决定了可以保存多少个类,如果定义的类太多,也会抛出内存溢出异常(OutOfMemoryError)
在HotSpot版本的Java虚拟机上方法区被称为“永久区(Perm)”,是因为HotSpot虚拟机的设计团队把GC分代扩展到了方法区,这块区域永远不会回收,所以也叫做永久区。但是其他版本的Java虚拟机来说不存在永久区的概念。并且在Hotspot版本的jdk1.8以后由“元数据区”取代了永久区。
永久区通过参数-XX:permSize和-XX:MaxPermSize来设定,默认最大值为64M,如果程序中有动态代理来动态的生成类,那么需要指定一个合理的永久区大小,否则将会内存溢出
元数据区参数通过-XX:MaxMetaspaceSize来指定最大元数据区大小,如果不指定,它会不断扩展,直到耗尽操作系统的内存。
直接内存
直接内存并不是虚拟机中的内存区域,而是操作系统所分配的内存区域。jdk1.4中加入了NIO,引入基于管道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能够提高性能,因为避免的Java堆和Native堆中来回复制。
对象访问
在Java语言中,对象访问无处不在。最普通的也是最简单的访问,也要涉及到Java栈,Java堆,方法区三个最重要的内存区域。
Object obj = new Object();
ojb 是一个引用(Reference)存储在Java栈
new Object() 是一个实例,存储在Java堆
在Java虚拟机规范中并没有定义这个引用类型(Reference)通过哪种方式定位,以及访问Java堆中对象的具体位置,因此不同的虚拟机实现不同,主流的有两种:使用句柄和直接指针
如果是句柄访问,Java堆中会划分出来一块内存作为句柄池,Reference中存储的是对象的句柄地址,而句柄中包含了对象的实例数据和类型数据各自的地址信息
如果是直接使用指针方式访问,Java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,Reference中直接存储的就是对象地址。
使用句柄的方式最大好处就是Reference存储的是稳定的地址,对象被移动(垃圾收集时新生代,老生代移动是非常普遍的行为)时只会改变句柄中的实例数据指针,而Reference本身不需要更改。
使用直接指针的方式最大的好处就是速度更快,它节省了一次指针定位的时间开销。
Hotspot使用的是第二种方式.
运行时指定jvm参数
Java虚拟机可以通过 %JAVA_HOME%\bin\java 来启动
java [options] class [args...]
options:表示Java虚拟机参数
args表示Java程序参数
class表示带有main方法的Java类
小程序示例
public class Args {
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");
}
}
用eclipse执行:
输出结果:
第[1]个参数 : 1
第[2]个参数 : 2
第[3]个参数 : 3
Xmx : 466M
内存溢出异常示例
在Java虚拟机规范中描述,除了程序计数器之外,虚拟机运行的其他几个运行时区域都有发生OutOfMemoryError异常的可能,这里演示一下堆内存溢出和栈的栈帧超过限制.
java堆溢出
Java堆用于存储对象实例,所以只要不断创建对象实例,并且保证GC不回收对象那么就会内存溢出。
代码片段
import java.util.LinkedList;
import java.util.List;
public class HeapOOM {
public static void main(String[] args) {
List<Object> list = new LinkedList<Object>();
while(true){
list.add(new Object());
}
}
}
启动参数:-Xms20m -Xmx20m -XX:-UseGCOverheadLimit
- -Xms20m -Xmx20m 设置堆的最大值最小值都是20M,避免自动扩展.
- -XX:-UseGCOverheadLimit : Hotspot VM1.6以后有一个特性,在堆快要溢出的时候提前预警。抛出GC overhead limit exceeded异常。这里的参数是关闭提前预警的意思。
参数扩展
Sun 官方对此的定义是:“并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。“
那么这样捕获这个异常,如果出现内存不足的情况,那么在这最后的一点内存内可以在catch代码块写上释放内存,或者保存数据,导出堆信息。
输出结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
Java虚拟机栈和本地方法栈溢出
在Hotspot虚拟机中并不区分虚拟机栈和本地方法栈.
栈溢出分两种,1 栈深度超过限制StackOverflowError 2 栈申请的内存太大 OutOfMemoryError
StackOverflowError
public class StackOf {
public void stackLeek(){
stackLeek();
}
public static void main(String[] args) {
new StackOf().stackLeek();
}
}
输出结果:
Exception in thread "main" java.lang.StackOverflowError