分为4个方面来介绍内存分配与回收,分别是内存是如何分配的、哪些内存需要回收、在什么情况下执行回收、如何监控和优化GC机制。
java GC(Garbage Collction)垃圾回收机制,是java与C/C++的主要区别之一。通过对jvm中内存进行标记,自主回收一些无用的内存。目前使用的最多的是sun公司jdk中的HotSpot,所以本文也以该jvm作为介绍的根本。
1.Java内存区域
在java运行时的数据取里,由jvm管理的内存区域分为多个部分:
程序计数器(program counter register):程序计数器是一个比较校的内存单元,用来表示当前程序运行哪里的一个指示器。由于每个线程都由自己的执行顺序,所以程序计数器是线程私有的,每个线程都要由一个自己的程序计数器来指示自己(线程)下一步要执行哪条指令。
如果程序执行的是一个java方法,那么计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地方法(native方法),那么计数器的值为Undefined。由于程序计数器记录的只是当前指令地址,所以不存在内存泄漏的情况,也是jvm内存区域中唯一一个没有OOME(out of memory error)定义的区域。
虚拟机栈(JVM stack):当线程的每个方法在执行的时候都会创建一个栈帧(Stack Frame)用来存储方法中的局部变量、方法出口等,同时会将这个栈帧放入JVM栈中,方法调用完成时,这个栈帧出栈。每个线程都要一个自己的虚拟机栈来保存自己的方法调用时候的数据,因此虚拟机栈也是线程私有的。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,抛出*Error,不过虚拟机基本上都允许动态扩展虚拟机栈的大小。这样的话线程可以一直申请栈,直到内存不足的时候,会抛出OOME(out of memory error)内存溢出。
本地方法栈(Native Method Stack):本地方法栈与虚拟机栈类似,只是本地方法栈存放的栈帧是在native方法调用的时候产生的。有的虚拟机中会将本地方法栈和虚拟栈放在一起,因此本地方法栈也是线程私有的。
堆(Heap):堆是java GC机制中最重要的区域。堆是为了放置“对象的实例”,对象都是在堆区上分配内存的,堆在逻辑上连续,在物理上不一定连续。所有的线程共用一个堆,堆的大小是可扩展的,如果在执行GC之后,仍没有足够的内存可以分配且堆大小不可再扩展,将会抛出OOME。
方法区(Method Area):又叫静态区,用于存储类的信息、常量池等,逻辑上是堆的一部分,是各个线程共享的区域,为了与堆区分,又叫非堆。在永久代还存在时,方法区被用作永久代。方法区可以选择是否开启垃圾回收。jvm内存不足时会抛出OOME。
直接内存(Direct Memory):直接内存指的是非jvm管理的内存,是机器剩余的内存。用基于通道(Channel)和缓冲区(Buffer)的方式来进行内存分配,用存储在JVM中的DirectByteBuffer来引用,当机器本身内存不足时,也会抛出OOME。
举例说明:Object obj = new Object();
obj表示一个本地引用,存储在jvm栈的本地变量表中,new Object()作为一个对象放在堆中,Object类的类型信息(接口,方法,对象类型等)放在堆中,而这些类型信息的地址放在方法区中。
这里需要知道如何通过引用访问到具体对象,也就是通过obj引用如何找到new出来的这个Object()对象,主要有两种方法,通过句柄和通过直接指针访问。
通过句柄:
在java堆中会专门有一块区域被划分为句柄池,一个引用的背后是一个对象实例数据(java堆中)的指针和对象类型信息(方法区中)的指针,而这两个指针都是在java堆上的。这种方法是优势是较为稳定,但是速度不是很快。
通过直接指针:
一个引用背后是一个对象的实例数据,这个实例数据里面包含了“到对象类型信息的指针”。这种方式的优势是速度快,在HotSpot中用的就是这种方式。
2.内存是如何分配和回收的
内存分配主要是在堆上的分配,如前面new出来的对象,放在堆上,但是现代技术也支持在栈上分配,较为少见,本文不考虑。分配内存与回收内存的标准是八个字:分代分配,分代回收。那么这个代是什么呢?
jvm中将对象根据存活的时间划分为三代:年轻代(Young Generation)、年老代(Old Generation)和永久代(Permannent Generation)。在jdk1.8中已经不再使用永久代,因此这里不再介绍。
年轻代:又叫新生代,所有新生成的对象都是先放在年轻代。年轻代分三个区,一个Eden区,两个Survivor区,一个叫From,一个叫To(这个名字是动态变化的)。当Eden中满时,执行Minor GC将消亡的对象清理掉,仍存活的对象将被复制到Survivor中的From区,清空Eden。当这个From区满的时候,仍存活的对象将被复制到To区,清空From区,并且原From区变为To区,原To区变为From区,这样的目的是保证To区一直为空。当From区满无对象可清理或者From-To区交换的次数超过设定(HotSpot默认为15,通过-XX:MaxTenuringThreashold控制)的时候,仍存活的对象进入老年代。年轻代中Eden和Servivor的比例通过-XX:SerivorRation参数来配置,默认为8,也就时说Eden:From:To=8:1:1。年轻代的回收方式叫做Minor GC,又叫停止-复制清理法。这种方法在回收的时候,需要暂停其他所有线程的执行,导致效率很低,现在虽然有优化,但是仅仅是将停止的时间变短,并没有彻底取消这个停止。
年老代:年老代的空间较大,当年老代内存不足时,将执行Major GC也叫Full GC。如果对象比较大,可能会直接分配到老年代上而不经过年轻代。用-XX:pertenureSizeThreashold来设定这个值,大于这个的对象会直接分配到老年代上。
3.垃圾收集器
在GC机制中,起作用的是垃圾收集器。HotSpot1.6中使用的垃圾收集器如下(有连线表示有联系):
Serial收集器:新生代(年轻代)收集器,使用停止-复制算法,使用一个线程进行GC,其他工作线程暂停。
ParNew收起:新生代收集器,使用停止-复制算法,Serial收集器的多线程版,用多个线程进行GC,其他工作线程暂停,关注缩短垃圾收集时间。
Parallel Scavenge收集器:新生代收集器,使用停止-复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。
Serial Old收集器:年老代收集器,单线程收集器,使用标记-整理算法(整理的方法包括sweep清理和compact压缩,标记-清理是先标记需要回收的对象,在标记完成后统一清楚标记的对象,这样清理之后空闲的内存是不连续的;标记-压缩是先标记需要回收的对象,把存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这样清理之后空闲的内存是连续的)。
Parallel Old收集器:老年代收集器,多线程收集器,使用标记-整理算法(整理的方法包括summary汇总和compact压缩,标记-压缩与Serial Old一样,标记-汇总是将幸存的对象复制到预先准备好的区域,再清理之前的对象)。
CMS(Concurrent Mark Sweep)收集器:老年老代收集器,多线程收集器,关注最短回收时间停顿,使用标记-清除算法,用户线程可以和GC线程同时工作。
G1收集器:JDK1.7中发布,使用较少,不作介绍。
Java GC是一个非常复杂的机制,想要详细说清楚他需要很多时间,如有错误恳请指正。