Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,做为Java开发者,通常不须要专门编写内存回收和垃圾清理代 码,对内存泄露和溢出的问题,也不须要像C程序员那样战战兢兢。这是由于在Java虚拟机中,存在自动内存管理和垃圾清扫机制。归纳地说,该机制对 JVM(Java Virtual Machine)中的内存进行标记,并肯定哪些内存须要回收,根据必定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄露和溢出问题。html
关于JVM,须要说明一下的是,目前使用最多的Sun公司的JDK中,自从 1999年的JDK1.2开始直至如今仍在普遍使用的JDK6,其中默认的虚拟机都是HotSpot。2009年,Oracle收购Sun,加上以前收购 的EBA公司,Oracle拥有3大虚拟机中的两个:JRockit和HotSpot,Oracle也代表了想要整合两大虚拟机的意图,可是目前在新发布 的JDK7中,默认的虚拟机仍然是HotSpot,所以本文中默认介绍的虚拟机都是HotSpot,相关机制也主要是指HotSpot的GC机制。程序员
Java GC机制主要完成3件事:肯定哪些内存须要回收,肯定何时须要执行GC,如何执行GC。通过这么长时间的发展(事实上,在Java语言出现以前,就有 GC机制的存在,如Lisp语言),Java GC机制已经日臻完善,几乎能够自动的为咱们作绝大多数的事情。然而,若是咱们从事较大型的应用软件开发,曾经出现过内存优化的需求,就一定要研究 Java GC机制。算法
学习Java GC机制,能够帮助咱们在平常工做中排查各类内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。编程
咱们将从4个方面学习Java GC机制,1,内存是如何分配的;2,如何保证内存不被错误回收(即:哪些内存须要回收);3,在什么状况下执行GC以及执行GC的方式;4,如何监控和优化GC机制。数组
了解Java GC机制,必须先清楚在JVM中内存区域的划分。在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:网络
其中:多线程
1,程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,能够理解为是当前线程的行号指示器。字节码解释器在工做时,会经过改变这个计数器的值来取下一条语句指令。并发
每一个程序计数器只用来记录一个线程的行号,因此它是线程私有(一个线程就有一个程序计数器)的。函数
若是程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;若是正在执行的是一个本地(native,由C语言编写 完成)方法,则计数器的值为Undefined,因为程序计数器只是记录当前指令地址,因此不存在内存溢出的状况,所以,程序计数器也是全部JVM内存区 域中惟一一个没有定义OutOfMemoryError的区域。性能
2,虚拟机栈(JVM Stack):一个线程的每一个方法在执行的同时,都会建立一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操做站、动态连接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
局部变量表中存储着方法的相关局部变量,包括各类基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占 用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。须要注意的是,局部变量表是在编译时就已经肯定 好的,方法运行所须要分配的空间在栈帧中是彻底肯定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,若是线程调用的栈深度大于虚拟机容许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都容许动态扩展虚拟机栈的大小(有少部分是固定长度的),因此线程能够一直申请栈,知道内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。
每一个线程对应着一个虚拟机栈,所以虚拟机栈也是线程私有的。
3,本地方法栈(Native Method Statck):本地方法栈在做用,运行机制,异常类型等方面都与虚拟机栈相同,惟一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在不少虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一块儿使用。
本地方法栈也是线程私有的。
4,堆区(Heap):堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由全部线程共享,在虚拟机启动时建立。堆区的存在是为了存储对象实例,原则上讲,全部的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。
通常的,根据Java虚拟机规范规定,堆内存须要在逻辑上是连续的(在物理上不须要),在实现时,能够是固定大小的,也能够是可扩展的,目前主 流的虚拟机都是可扩展的。若是在执行垃圾回收以后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。
关于堆区的内容还有不少,将在下节“Java内存分配机制”中详细介绍。
5,方法区(Method Area):在Java虚拟机规范中,将方法区做为堆的一个逻辑部分来对待,但事实 上,方法区并非堆(Non-Heap);另外,很多人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些做者将方法区定义为“永久代”,这是由于,对于以前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot以外的多数虚拟机,并不将方法区当作永 久代,HotSpot自己,也计划取消永久代。本文中,因为笔者主要使用Oracle JDK6.0,所以仍将使用永久代一词。
方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时须要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。
方法区在物理上也不须要是连续的,能够选择固定大小或可扩展大小,而且方法区比堆还多了一个限制:能够选择是否执行垃圾收集。通常的,方法区上 执行的垃圾收集是不多的,这也是方法区被称为永久代的缘由之一(HotSpot),但这也不表明着在方法区上彻底没有垃圾收集,其上的垃圾收集主要是针对 常量池的内存回收和对已加载类的卸载。
在方法区上进行垃圾收集,条件苛刻并且至关困难,效果也不使人满意,因此通常不作太多考虑,能够留做之后进一步深刻研究时使用。
在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类连接阶段完成翻译);运行时常量池除了存储编译期常量外,也能够存储在运行时间产生的常量(好比String类的intern()方法,做用是String维护了一个常量池,若是调用的字符“abc”已经在常量池中,则返回池中的字符串地址,不然,新建一个常量加入池中,并返回地址)。
6,直接内存(Direct Memory):直接内存并非JVM管理的内存,能够这样理解,直接内存,就是 JVM之外的机器内存,好比,你有4G的内存,JVM占用了1G,则其他的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。 因为直接内存收到本机器内存的限制,因此也可能出现OutOfMemoryError的异常。
通常来讲,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。
以最简单的本地变量引用:Object obj = new Object()为例:
在Java虚拟机规范中,对于经过reference类型引用访问具体对象的方式并未作规定,目前主流的实现方式主要有两种:
1,经过句柄访问(图来自于《深刻理解Java虚拟机:JVM高级特效与最佳实现》):
经过句柄访问的实现方式中,JVM堆中会专门有一块区域用来做为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法因为用句柄表示地址,所以十分稳定。
2,经过直接指针访问:(图来自于《深刻理解Java虚拟机:JVM高级特效与最佳实现》)
经过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优点是速度快,在HotSpot虚拟机中用的就是这种方式。
这里所说的内存分配,主要指的是在堆上的分配,通常的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,能够是基本类型或String等),而后在栈上分配,在栈上分配的不多见,咱们这里不考虑。
Java内存分配和回收的机制归纳的说,就是:分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。以下图(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html):
年轻代(Young Generation):对象被建立时,内存的分配首先发生在年轻代(大对象能够直接 被建立在年老代),大部分的对象在建立后很快就再也不使用,所以很快变得不可达,因而被年轻代的GC机制清理掉(IBM的研究代表,98%的对象都是很快消 亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不表明年轻代内存不足,它事实上只表示在Eden区上的GC。
年轻代上的内存分配是这样的,年轻代能够分为3个区域:Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再 贴切不过)和两个存活区(Survivor 0 、Survivor 1)。内存分配过程为(来源于《成为JavaGC专家part I》,http://www.importnew.com/1993.html):
从上面的过程能够看出,Eden区是连续的空间,且Survivor总有一个为空。通过一次GC和复制,一个Survivor中保存着当前还活 着的对象,而Eden区和另外一个Survivor区的内容都再也不须要了,能够直接清空,到下一次GC时,两个Survivor的角色再互换。所以,这种方 式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“中止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另外一个Survivor中),这不表明着中止复制清理法很高效,其实,它也只在这种状况下高效,若是在老年代采用中止复制,则挺悲剧的。
在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread- Local Allocation Buffers),这两种技术的作法分别是:因为Eden区是连续的,所以bump-the-pointer技术的核心就是跟踪最后建立的一个对象,在对 象建立时,只须要检查最后一个对象后面是否有足够的内存便可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干 段,每一个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每一个线程都使用Eden区的一段,并快速的分配内 存。
年老代(Old Generation):对象若是在年轻代存活了足够长的时间而没有被清理掉(即在几回 Young GC后存活了下来),则会被复制到年老代,年老代的空间通常比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时, 将执行Major GC,也叫 Full GC。
可使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,若是动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。
若是对象比较大(好比长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提早GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
可能存在年老代对象引用新生代对象的状况,若是须要执行Young GC,则可能须要查询整个老年代以肯定是否能够清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,全部老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里便可,不用再去查所有老年代,所以性能大大提升。
GC机制的基本算法是:分代收集,这个不用赘述。下面阐述每一个分代的收集方法。
年轻代:
事实上,在上一节,已经介绍了新生代的主要垃圾回收方法,在新生代中,使用“中止-复制”算法进行清理,将新生代内存分为2部分,1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。每次进行清理时,将Eden区和一个Survivor中仍然存活的对象拷贝到 另外一个Survivor中,而后清理掉Eden和刚才的Survivor。
这里也能够发现,中止复制算法中,用来复制的两部分并不老是相等的(传统的中止复制算法两部份内存相等,但新生代中使用1个大的Eden区和2个小的Survivor区来避免这个问题)
因为绝大部分的对象都是短命的,甚至存活不到Survivor中,因此,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。若是一次回收中,Survivor+Eden中存活下来的内存超过了10%,则须要将一部分对象分配到 老年代。用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,表明Eden:Survivor1:Survivor2=8:1:1.
老年代:
老年代存储的对象比年轻代多得多,并且不乏大对象,对老年代进行内存清理时,若是使用中止-复制算法,则至关低效。通常,老年代用的算法是标记-整理算法,即:标记出仍然存活的对象(存在引用的),将全部存活的对象向一端移动,以保证内存的连续。
在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,若是大于,则直接触发一次Full GC,不然,就查看是否设 置了-XX:+HandlePromotionFailure(容许担保失败),若是容许,则只会进行MinorGC,此时能够容忍内存分配失败;若是不 容许,则仍然进行Full GC(这表明着若是设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有不少内存,因此,最好不要这样作)。
方法区(永久代):
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就能够被回收。对于无用的类进行回收,必须保证3点:
永久代的回收并非必须的,能够经过参数来设置是否对类进行回收。HotSpot提供-Xnoclassgc进行控制
使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading能够查看类加载和卸载信息
-verbose、-XX:+TraceClassLoading能够在Product版HotSpot中使用;
-XX:+TraceClassUnLoading须要fastdebug版HotSpot支持
在GC机制中,起重要做用的是垃圾收集器,垃圾收集器是GC的具体实现,Java虚拟机规范中对于垃圾收集器没有任何规定,因此不一样厂商实现的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器以下图(图来源于《深刻理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们能够配合使用):
在介绍垃圾收集器以前,须要明确一点,就是在新生代采用的中止复制算法中,“停 止(Stop-the-world)”的意义是在回收内存时,须要暂停其余所 有线程的执行。这个是很低效的,如今的各类新生代收集器愈来愈优化这一点,但仍然只是将中止的时间变短,并未完全取消中止。
CMS收集的方法是:先3次标记,再1次清除,3次标记中前两次是初始标记和从新标记(此时仍然须要中止(stop the world)), 初始标记(Initial Remark)是标记GC Roots能关联到的对象(即有引用的对象),停顿时间很短;并发标记(Concurrent remark)是执行GC Roots查找引用的过程,不须要用户线程停顿;从新标记(Remark)是在初始标记和并发标记期间,有标记变更的那部分仍须要标记,因此加上这一部分 标记的过程,停顿时间比并发标记小得多,但比初始标记稍长。在完成标记以后,就开始并发清除,不须要用户线程停顿。
因此在CMS清理过程当中,只有初始标记和从新标记须要短暂停顿,并发标记和并发清除都不须要暂停用户线程,所以效率很高,很适合高交互的场合。
CMS也有缺点,它须要消耗额外的CPU和内存资源,在CPU和内存资源紧张,CPU较少时,会加剧系统负担(CMS默认启动线程数为(CPU数量+3)/4)。
另外,在并发收集过程当中,用户线程仍然在运行,仍然产生内存垃圾,因此可能产生“浮动垃圾”,本次没法清理,只能下一次Full GC才清理,所以在GC期间,须要预留足够的内存给用户线程使用。因此使用CMS的收集器并非老年代满了才触发Full GC,而是在使用了一大半(默认68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction来设置)的时候就要进行Full GC,若是用户线程消耗内存不是特别大,能够适当调高-XX:CMSInitiatingOccupancyFraction以下降GC次数,提升性能,若是预留的用户线程内存不够,则会触发Concurrent Mode Failure,此时,将触发备用方案:使用Serial Old 收集器进行收集,但这样停顿时间就长了,所以-XX:CMSInitiatingOccupancyFraction不宜设的过大。
还有,CMS采用的是标记清除算法,会致使内存碎片的产生,可使用-XX:+UseCMSCompactAtFullCollection来设置是否在Full GC以后进行碎片整理,用-XX:CMSFullGCsBeforeCompaction来设置在执行多少次不压缩的Full GC以后,来一次带压缩的Full GC。
注意并发(Concurrent)和并行(Parallel)的区别:
并发是指用户线程与GC线程同时执行(不必定是并行,可能交替,但整体上是在同时执行的),不须要停顿用户线程(其实在CMS中用户线程仍是须要停顿的,只是很是短,GC线程在另外一个CPU上执行);
并行收集是指多个GC线程并行工做,但此时用户线程是暂停的;
因此,Serial和Parallel收集器都是并行的,而CMS收集器是并发的.
关于JVM参数配置和内存调优实例,见个人下一篇博客(编写中:Java系列笔记(4) - JVM监控与调优),原本想写在同一篇博客里的,无奈内容太多,只好另起一篇。
这篇文章写了好久,主要是Java内存和 GC机制相对复杂,难以理解,加上本人这段时间项目和生活中耗费的时间不少,因此进度缓慢。文中大多数笔记内容来源于我在网络上查到的博客和《深刻理解 Java虚拟机:JVM高级特效与最佳实现》一书。
本人能力有限,若是有错漏,请留言指正。
《JAVA编程思想》,第5章;
《Java深度历险》,Java垃圾回收机制与引用类型;
《深刻理解Java虚拟机:JVM高级特效与最佳实现》,第2-3章;
成为JavaGC专家Part II — 如何监控Java垃圾回收机制, http://www.importnew.com/2057.html
JDK5.0垃圾收集优化之--Don't Pause,http://calvin.iteye.com/blog/91905