对于 Java 程序员来说,虚拟机自带内存管理机制,不容易出现内存泄露和内存溢出问题,一旦出现内存泄露方面的问题,如果我们了解虚拟机是怎样使用内存的,将很容易的找到错误发生的原因.
Java 虚拟机在执行 Java 程序时,会把它所管理的内存分成若干的区域,每一个区域都有各自的用途.
程序计数器是一块很小的内存空间,它可以看做是当前程序所执行字节码的行号指示器.每一个线程都有一个程序计数器.
线程私有的,生命周期与线程相同.虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息.每一个方法从调用到完成,就对应着栈帧从入栈到出栈的过程.
与虚拟机栈发挥的作用类似,区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈为虚拟机用到的 Native 方法服务.
是 Java 虚拟机所管理内存最大的一块,被所有线程共享的一块内存区域.所有对象实例以及数组都要在堆上分配.
Java 堆是垃圾收集器管理的主要区域,也被成为”GC 堆”,从内存回收角度看,现在收集器基本都采用分代收集算法,所以 Java 堆还可以分为:新生代和老年代.
线程共享的内存区域,用于存储虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据.
是方法区的一部分.Class 文件除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放.
直接内存不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域.
在 JDK1.4中新加入了 NIO(New Input/Output)类,引入了一种基于通道与缓冲区的 I/O 方式,可以使用 Native 函数库直接分配堆内存,然后通过 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用操作,这样做能显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据.
基于使用优先原则,我们以常用的虚拟机 HotSpot 和常用的内存区域 Java 堆为例,探讨 HotSpot 虚拟机在 Java 堆中对象分配,布局和访问的全过程.
虚拟机遇到一个 new 指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个类是否已被加载,解析和初始化过,如果没有,必须先执行相应的类加载过程.
在类加载通过后,虚拟机开将为新对象分配内存,如果 Java 堆中内存是绝对规整的,分配内存仅仅是把指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配成为”指针碰撞”.如果 Java 堆中内存不是规整的,就必须维护一个列表,记录那些内存可用,在分配时从列表找到一空间划分给对象实例,并更新列表上的记录,这种分配方式称为”空闲列表”.
接下来,虚拟机要对对象进行必要的设置,如这个对象是哪个类的实例,如果才能找到类的元数据信息,对象的 GC 分代年龄等信息,将这些信息存放在对象头之中.
在 HotSpot 虚拟机中,对象在内存中存储的区域分为3块:对象头,实例数据和对齐填充.
对象头包括两部分信息,第一部分是存储对象自身运行时数据.如哈希表, GC 分代年龄,锁状态等,另一部分是对象指向它的类元数据的指针,通过这个指针来确定这个对象是哪个类的实例.
实例数据部分存储程序代码中所定义的各种类型的字段内容.
对齐填充不是必然存在的,当对象的大小不是8字节整数倍时,就需要通过对齐填充来补全.
Java 程序中通过栈上的 reference 数据来操作堆上的具体对象,reference 只规定了一个指向对象的引用,没有定义引用应该通过何种方式定位,访问堆中对象的具体位置,所以对象访问取决于虚拟机实现而定.主流的访问方式有使用句柄和直接指针两种.
如果使用句柄访问,在 Java 堆中将会划分一块内存作为句柄池, reference 中存储的就是对象的句柄地址,而句柄地址中包含了对象实例数据与类型数据各自的具体地址
如果使用直接指针访问,在 Java 堆的布局就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址.
使用句柄优点是对象移动后只需改变句柄中的事例数据指针.使用指针的优点是速度更快,节省了一次指针定位的时间开销.