1、运行时数据区域
Java虚拟机在执行Java程序的过程中,会把它管理的内存,划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。Java虚拟机所管理的内存将会包括以下几个运行时数据区域。
运行时数据区.jpg
1.1、程序计数器(线程私有)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程恢复等基础功能,都需要依赖程序计数器来完成。
为了线程切换后,能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程间计数器互不影响,独立存储。
如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的位置;如果正在执行的是Native方法,则计数器值为空。此区域是唯一一个没有规定任何OutOfMemoryError情况的区域。
1.2、Java虚拟机栈(线程私有)
Java虚拟机栈生命周期与线程相同。每个方法在执行的同时,都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应一个栈帧从入栈到出栈的过程。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常。如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可以动态扩展),如果扩展时,无法申请到足够的内存,就会抛出OutOfMemoryError。
1.3、本地方法栈(线程私有)
本地方法栈与Java虚拟机栈发挥的作用非常相似,它们之间的区别是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
Sun HotSpot虚拟机直接把本地方法栈和Java虚拟机栈合二为一,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常
1.4、Java堆(线程共享)
Java堆是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建,几乎所有的对象实例都是在这里创建。
Java堆中还可以分为新生代和老年代;新生代又分为Eden空间,From Survivor空间和To Survivor空间。
当前主流的虚拟机,Java堆都是可以扩展的(通过-Xmx和-Xms实现)。如果在堆中没有内存完成实例分配,并且堆再也无法扩展时,将会抛出OutOfMemoryError异常。
1.5、方法区(线程共享)
方法区用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码代码等数据。相对而言,垃圾收集行为,在这个区域比较少出现。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池,是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。
2、HotSpot虚拟机对象探秘
2.1、对象的创建
1、虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过。如果没有,那必须先执行类的加载过程(第七章内容)。
2、在类加载检查通过后,接下来虚拟机将会为新生对象分配内存。对象所需内存大小,在类加载完成后便可确定。
指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存放到一边,空闲的放到另一边。中间放着一个指针作为分界点的指示器,那所分配的内存就是把指针向空闲空间移动一段与对对象大小相等的距离。
空闲列表:如果Java堆中内存是不规整的,已使用的内存和空闲的内存相互交错,虚拟机就维护一个空闲列表,记录那块内存是可用的,在分配的时候,从列表中找一块足够大的内存区域,并更新列表上的记录。
空闲列表.jpg
选择哪种分配方式,由Java堆是否规整决定,而Java堆是否规整,是由所采用的垃圾收集器是否带有压缩整理功能决定的。因此Serial和ParNew采用指针碰撞,而CMS采用空闲列表。
3、内存分配完成后,虚拟机需要将内存空间都初始化为零值,这一步操作保证对象的实例字段在不赋初值,就可以直接使用。
4、接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息等信息。这些信息存放在对象头中。
5、上面的工作都完成之后,一个新的对象已经产生了,但是所有字段都还是零。执行new指令之后,接着执行init方法,这样一个真正可用对象才算完全产生出来。
对象创建过程.jpg
2.2、对象的内存布局
对象在内存中存储的布局可以分为三个区域:对象头,实例数据和对齐填充。
1、对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志等,这部分数据在32位和64位虚拟机中分别为32bit和64bit。对象头的另一部分是类型指针,虚拟机通过这个指针来确定对象是哪个类的实例。
2、实例数据部分是对象真正存储的有效信息,也是在代码中定义的各字段的内容。
3、对齐填充并不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。
2.3、对象的访问定位
建立对象是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位和访问堆中对象的具体位置。目前主流的访问方式有使用句柄和直接指针两种。
如果使用句柄的话,那么Java堆中会划分出一块区域作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
句柄访问对象.jpg
如果使用指针访问,那么Java堆对象的布局中就必须考虑,如何放置访问类型数据的相关信息,而reference中存储的就是对象地址。
指针访问对象.jpg
这两种访问方式各有优势,第一种的好处就是reference中存储的是稳定的句柄地址,对象被移动时,只会改变句柄中的实例数据指针,reference本身不需要修改。第二种好处是速度更快,节省了一次指针定位的时间开销。HotSpot是使用指针访问对象的,但从整个软件开发的范围来看,使用句柄访问的情况也非常常见。
3、实战:OutOfMemoryError异常
除了程序计数器以外,其他几个区域都有发生OutOfMemoryError异常的可能。
3.1、Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Root到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么对象数量到达堆的容量限制后就会产生OOM异常。
package com.ljessie.jvm;
import java.util.ArrayList;
import java.util.List;
public class HeapOOM {
static class OOMObject{
byte[] bytes = new byte[1024*1024*100];
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.ljessie.jvm.HeapOOM$OOMObject.<init>(HeapOOM.java:8)
at com.ljessie.jvm.HeapOOM.main(HeapOOM.java:14)
3.2、虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError异常。如果虚拟机在扩展栈时,无法申请到足够的内存空间,将抛出OutOfMemoryError异常。
package com.ljessie.jvm;
public class StackOverFlowDemo {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackOverFlowDemo demo = new StackOverFlowDemo();
try{
demo.stackLeak();
}catch (Throwable e){
e.printStackTrace();
System.out.println("stack length:"+demo.stackLength);
}
}
}
运行结果
java.lang.StackOverflowError
at com.ljessie.jvm.StackOverFlowDemo.stackLeak(StackOverFlowDemo.java:8)
stack length:29346
结果表明,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverFlowError。
网友评论