一.概述
java程序在java虚拟机的自动内存管理机制的帮助下,不容易出现内存泄露和内存溢出的问题,但是一旦出现内存泄露和溢出方面的问题,若是不了解虚拟机是如何使用内存的,那么排除错误将会异常困难,因此,作为java程序员,了解java虚拟机的内存管理是很有必要的。
二.jvm运行时数据区域分布
如图所示,运行期数据区域可以分为线程共享的和线程隔离的。接下来将一一介绍。
1.程序计数器
程序计数器是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。每条线程都有一个独立的程序计数器,各线程之间的计数器互不影响。如果线程在执行一个java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址,在执行native方法时,计数器的值为空。并且在java虚拟机规范中没有在此区域规定任何的outOfMemoryError异常。
2.java虚拟机栈
java虚拟机栈也是线程私有的,且生命周期和线程相同,其描述的是java方法执行的内存模型。在方法执行时,创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。此内存区域就是我们常说的栈内存。其中,局部变量表存放了编译器可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double)和对象引用(reference类型),其中long和double占用两个局部变量空间,其他的占用一个。在进入方法时,此局部变量空间就被唯一确定。
java虚拟机规范规定了两种异常状况:线程请求的栈深度大于虚拟机所允许的深度时,抛出StackOverflowError异常。
如果扩展时,无法申请到足够的内存,抛出OutOfMemoryError异常。
3.本地方法栈
本地方法栈与java虚拟机栈类似,不过java虚拟机栈为虚拟机栈执行java方法服务,而本地方法栈则为虚拟机执行Native方法服务。
Sun HotSpot虚拟机把本地方法栈和虚拟机栈合二为一。
4.java堆
java堆是java虚拟机管理的内存在中最大的一块。是线程共享的。此内存区域的唯一目的就是存放对象实例。所有的对象实例和数组都在堆上分配。java堆还是垃圾收集器管理 的主要区域,在采用分代收集算法的收集器中,java堆还可以分为新生代和老年代。java对在分配实例时,会划分出多个线程私有的分配缓冲器。java堆可以处于物理上不连续的内存空间上。通过-Xmx和-Xms扩展java堆。堆无法扩展时,抛出OutOfMemoryError异常。
5.方法区
线程共享的区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等。这个区域的回收主要针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,会抛出OutOfMemoryError异常。
三.对象的创建过程详解
对象在创建过程大致经过一下几步:类加载检查-->分配内存-->初始化内存空间为零值-->设置对象头-->按照程序员意愿初始化
1.类加载检查:当虚拟机遇到new指令时,会检查指令的参数是否能在常量池中定位类符号的引用,并检查此符号引用所代表的类是否已被加载,解析,初始化,若没有,执行类加载过程。
2.分配内存:内存规整时,使用指针碰撞的方式进行内存分配。若不是,则采用空闲列表的方式进行分配。这两种方式取决于java堆所采用的垃圾回收器是否带有压缩整理功能。在多线程的情况下,要按照线程划分在不同的空间中进行,线程会在java堆中预先分配一小块本地线程分配缓冲(TLAB),是否使用TBAB可以通过-XX:+/-UseTLAB来设定。
3.初始化内存空间:在内存分配完成后,需要把分配到的内存空间都初始化为零值,如果使用了TLAB,这一工作可以提前到TLAB分配时进行。这一操作保证了对象的实例字段在java代码中可以不赋初始值就可以使用。
4.对象头的设置:设置对象的具体信息,比如对象是哪个类的实例,对象的哈希码,对象的GC分代年龄等,这些信息保存在对象的对象头中。
5.执行方法,使得对象按照程序员的意愿进行初始化,到此为止,一个真正可用的对象才算完全产生。
四.对象的内存布局
在HotSpot虚拟机中,对象在内存的存储布局分为三个区域:对象头,实例数据,对齐填充。
1.对象头:在对象创建过程中即被初始化好,包含两部分信息,一是存储对象自身的运行时数据,如哈希码,GC分代年龄,线程的持有锁。另一部分为类型指针,即对象指向类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。特别的,若对象是一个数组,那么在对象头还有一块用于记录数组长度的数据。
2.实例数据:保存自身定义的和从父类继承而来的字段内容。HotSpot默认的存储顺序为:longs/doubles,ints,shorts/chars,bytes/booleans,oop。但是父类定义的变量会出现在子类之前。
3.对齐填充:非必须的部分,仅仅起着占位符的作用。
五.对象的访问定位
我们通过在栈中的reference数据来操作堆上的具体对象,现在主流的访问方式有两种:使用句柄和直接指针。
1.句柄方式访问:java堆中会划分出一块内存作为句柄池,reference中存储就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自的地址信息,具体如图。
2.直接指针访问:reference直接存储的就是对象的地址,在java堆中保存对象类型数据。如图。
3.两种方式比较:使用句柄的优势在于reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象经常发生)时只会改变句柄中的实例数据指针,reference本身不需要修改。
使用直接指针方式优势在于速度更快,节省了一次指针定位的时间,HotSpot是使用直接指针方式进行对象访问的。