Java垃圾回收概况
Java GC(Garbage Collection,垃圾搜集,垃圾回收)机制,是Java与C++/C的主要区分之1,作为Java开发者,1般不需要专门编写内存回收和垃圾清算代 码,对内存泄漏和溢出的问题,也不需要像C程序员那样战战兢兢。这是由于在Java虚拟机中,存在自动内存管理和垃圾打扫机制。概括地说,该机制对 JVM(Java Virtual Machine)中的内存进行标记,并肯定哪些内存需要回收,根据1定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,放置出现内存泄漏和溢出问题。
关于JVM,需要说明1下的是,目前使用最多的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机制,可以帮助我们在平常工作中排查各种内存溢出或泄漏问题,解决性能瓶颈,到达更高的并发量,写出更高效的程序。
JAVA内存划分
了解Java GC机制,必须先清楚在JVM中内存区域的划分。在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:
其中:
1,程序计数器(Program Counter Register):程序计数器是1个比较小的内存区域,用于唆使当前线程所履行的字节码履行到了第几行,可以理解为是当前线程的行号唆使器。字节码解释器在工作时,会通过改变这个计数器的值来取下1条语句指令。
每一个程序计数器只用来记录1个线程的行号,所以它是线程私有(1个线程就有1个程序计数器)的。
如果程序履行的是1个Java方法,则计数器记录的是正在履行的虚拟机字节码指令地址;如果正在履行的是1个本地(native,由C语言编写 完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区 域中唯逐一个没有定义OutOfMemoryError的区域。
2,虚拟机栈(JVM Stack):1个线程的每一个方法在履行的同时,都会创建1个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法履行完成时,栈帧出栈。
局部变量表中存储着方法的相干局部变量,包括各种基本数据类型,对象的援用,返回地址等。在局部变量表中,只有long和double类型会占 用2个局部变量空间(Slot,对32位机器,1个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已肯定 好的,方法运行所需要分配的空间在栈帧中是完全肯定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过量 数Java虚拟机都允许动态扩大虚拟机栈的大小(有少部份是固定长度的),所以线程可以1直申请栈,知道内存不足,此时,会抛出 OutOfMemoryError(内存溢出)。
每一个线程对应着1个虚拟机栈,因此虚拟机栈也是线程私有的。
3,本地方法栈(Native Method Statck):本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯1的区分是:虚拟机栈是履行Java方法的,而本地方法栈是用来履行native方法的,在很多虚拟机中(如Sun的JDK默许的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在1起使用。
本地方法栈也是线程私有的。
4,堆区(Heap):堆区是理解Java GC机制最重要的区域,没有之1。在JVM所管理的内存中,堆区是最大的1块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程同享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。
1般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也能够是可扩大的,目前主 流的虚拟机都是可扩大的。如果在履行垃圾回收以后,仍没有足够的内存分配,也不能再扩大,将会抛出OutOfMemoryError:Java heap space异常。
关于堆区的内容还有很多,将在下节“Java内存分配机制”中详细介绍。
5,方法区(Method Area):在Java虚拟机规范中,将方法区作为堆的1个逻辑部份来对待,但事实 上,方法区其实不是堆(Non-Heap);方法区是各个线程同享的区域,用于存储已被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。
方法区在物理上也不需要是连续的,可以选择固定大小或可扩大大小,并且方法区比堆还多了1个限制:可以选择是不是履行垃圾搜集。1般的,方法区上 履行的垃圾搜集是很少的,这也是方法区被称为永久代的缘由之1(HotSpot),但这也不代表着在方法区上完全没有垃圾搜集,其上的垃圾搜集主要是针对 常量池的内存回收和对已加载类的卸载。
在方法区上进行垃圾搜集,条件刻薄而且相当困难,效果也不使人满意,所以1般不做太多斟酌,可以留作以落后1步深入研究时使用。
在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。
运行经常量池(Runtime Constant Pool)是方法区的1部份,用于存储编译期就生成的字面常量、符号援用、翻译出来的直接援用(符号援用就是编码是用字符串表示某个变量、接口的位置,直接援用就是根据符号援用翻译出来的地址,将在类链接阶段完成翻译);运行经常量池除存储编译期常量外,也能够存储在运行时间产生的常量(比如String类的intern()方法,作用是String保护了1个常量池,如果调用的字符“abc”已在常量池中,则返回池中的字符串地址,否则,新建1个常量加入池中,并返回地址)。
6,直接内存(Direct Memory):直接内存其实不是JVM管理的内存,可以这样理解,直接内存,就是 JVM之外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有1种基于通道(Channel)和缓冲区 (Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来援用。 由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。
Java内存分配机制
这里所说的内存分配,主要指的是在堆上的分配,1般的,对象的内存分配都是在堆上进行,但现代技术也支持将对象拆成标量类型(标量类型即原子类型,表示单个值,可以是基本类型或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总有1个为空。经过1次GC和复制,1个Survivor中保存着当前还活 着的对象,而Eden区和另外一个Survivor区的内容都不再需要了,可以直接清空,到下1次GC时,两个Survivor的角色再互换。因此,这类方 式分配内存和清算内存的效力都极高,这类垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清算法(将Eden区和1个Survivor中依然存活的对象拷贝到另外一个Survivor中),这不代表着停止复制清算法很高效,其实,它也只在这类情况下高效,如果在老年代采取停止复制,则挺悲剧的。
在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread- Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的1个对象,在对 象创建时,只需要检查最后1个对象后面是不是有足够的内存便可,从而大大加快内存分配速度;而对TLAB技术是对多线程而言的,将Eden辨别为若干 段,每一个线程使用独立的1段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每一个线程都使用Eden区的1段,并快速的分配内 存。
年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清算掉(即在几次 Young GC后存活了下来),则会被复制到年老代,年老代的空间1般比年轻代大,能寄存更多的对象,在年老代上产生的GC次数也比年轻代少。当年老代内存不足时, 将履行Major GC,也叫 Full GC。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提早GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
可能存在年老代对象援用新生代对象的情况,如果需要履行Young GC,则可能需要查询全部老年代以肯定是不是可以清算回收,这明显是低效的。解决的方法是,年老代中保护1个512 byte的块——”card table“,所有老年代对象援用新生代对象的记录都记录在这里。Young GC时,只要查这里便可,不用再去查全部老年代,因此性能大大提高。
JVM经常使用内存参数设置
a: -Xmx<n>
指定 jvm 的最大 heap 大小 , 如 :-Xmx=2gJava对象的访问方式
1般来讲,1个Java的援用访问触及到3个内存区域:JVM栈,堆,方法区。
以最简单的本地变量援用:Object obj = new Object()为例:
在Java虚拟机规范中,对通过reference类型援用访问具体对象的方式并未做规定,目前主流的实现方式主要有两种:
1,通过句柄访问(图来自于《深入理解Java虚拟机:JVM高级殊效与最好实现》):
通过句柄访问的实现方式中,JVM堆中会专门有1块区域用来作为句柄池,存储相干句柄所履行的实例数据地址(包括在堆中地址和在方法区中的地址)。这类实现方法由于用句柄表示地址,因此10分稳定。
2,通过直接指针访问:(图来自于《深入理解Java虚拟机:JVM高级殊效与最好实现》)
通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包括了在方法区中的相应类型数据。这类方法最大的优势是速度快,在HotSpot虚拟机中用的就是这类方式。
如何肯定某个对象是垃圾?
在这1小节我们先了解1个最基本的问题:如果肯定某个对象是“垃圾”?既然垃圾搜集器的任务是回收垃圾对象所占的空间供新的对象使用,那末垃圾搜集器如何肯定某个对象是“垃圾”?—即通过甚么方法判断1个对象可以被回收了。
在java中是通过援用来和对象进行关联的,也就是说如果要操作对象,必须通过援用来进行。那末很明显1个简单的办法就是通过援用计数来判断1个对象是不是可以被回收。不失1般性,如果1个对象没有任何援用与之关联,则说明该对象基本不太可能在其他地方被使用到,那末这个对象就成为可被回收的对象了。这类方式成为援用计数法。这类方式的特点是实现简单,而且效力较高,但是它没法解决循环援用的问题,因此在Java中并没有采取这类方式(Python采取的是援用计数法)。
为了解决这个问题,在Java中采取了 可达性分析法。该方法的基本思想是通过1系列的“GC Roots”对象作为出发点进行搜索,如果在“GC Roots”和1个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不1定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须最少经历两次标记进程,如果在这两次标记进程中依然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
在肯定了哪些垃圾可以被回收后,垃圾搜集器要做的事情就是开始进行垃圾回收,但是这里面触及到1个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾搜集器做出明确的规定,因此各个厂商的虚拟机可以采取不同的方式来实现垃圾搜集器,所以在此只讨论几种常见的垃圾搜集算法的核心思想。
1.Mark-Sweep(标记-清除)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是由于它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体进程以下图所示:
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有1个比较严重的问题就是容易产生内存碎片,碎片太多可能会致使后续进程中需要为大对象分配空间时没法找到足够的空间而提早触发新的1次垃圾搜集动作。
2.Copying(复制)算法
为了解决Mark-Sweep算法的缺点,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的1块。当这1块的内存用完了,就将还存活着的对象复制到另外1块上面,然后再把已使用的内存空间1次清算掉,这样1来就不容易出现内存碎片的问题。具体进程以下图所示:
这类算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,由于能够使用的内存缩减到原来的1半。
很明显,Copying算法的效力跟存活对象的数目多少有很大的关系,如果存活对象很多,那末Copying算法的效力将会大大下降。
3.Mark-Compact(标记-整理)算法
为了解决Copying算法的缺点,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep1样,但是在完成标记以后,它不是直接清算可回收对象,而是将存活对象都向1端移动,然后清算掉端边界之外的内存。具体进程以下图所示:
4.Generational Collection(分代搜集)算法
分代搜集算法是目前大部份JVM的垃圾搜集器采取的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。1般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾搜集时只有少许对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那末就能够根据不同代的特点采取最合适的搜集算法。
目前大部份垃圾搜集器对新生代都采取Copying算法,由于新生代中每次垃圾回收都要回收大部份对象,也就是说需要复制的操作次数较少,但是实际中其实不是依照1:1的比例来划分新生代的空间的,1般来讲是将新生代划分为1块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的1块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另外一块Survivor空间中,然后清算掉Eden和刚才使用过的Survivor空间。
而由于老年代的特点是每次回收都只回收少许对象,1般使用的是Mark-Compact算法。
注意,在堆区以外还有1个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描写等。对永久代的回收主要回收两部份内容:废弃常量和无用的类。
java垃圾搜集机制Java 中的堆也是 GC 搜集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。
年轻代:
Minor GC 是产生在新生代中的垃圾搜集动作,所采取的是复制算法。
新生代几近是所有 Java 对象诞生的地方,即 Java 对象申请的内存和寄存都是在这个地方。Java 中的大部份对象通常不需久长存活,具有朝生夕灭的性质。当1个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部份对象的内存空间。新生代是 GC 搜集垃圾的频繁区域。
当对象在 Eden ( 包括1个 Survivor 区域,这里假定是 from 区域 ) 诞生后,在经过1次 Minor GC 后,如果对象还存活,并且能够被另外1块 Survivor 区域所容纳( 上面已假定为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些依然还存活的对象复制到另外1块
Survivor 区域 ( 即 to 区域 ) 中,然后清算所使用过的 Eden 和 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过1次 Minor GC,就将对象的年龄 + 1,当对象的年龄到达某个值时 ( 默许是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。但这也不是1定的,对1些较大的对象 ( 即需要分配1块较大的连续内存空间 ) 则是直接进入到老年代。
老年代:
Full GC 是产生在老年代的垃圾搜集动作,所采取的是标记-清除-整理算法。现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于个,老年代里面的对象几近个个都是在 Survivor 区域中熬过来的,它们是不会那末容易就 “死掉” 了的。因此,Full GC 产生的次数不会有 Minor GC 那末频繁,并且做1次 Full GC 要比进行1次 Minor
GC 的时间更长。另外,标记-清除算法搜集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),尔后需要为较大的对象分配内存空间时,若没法找到足够的连续的内存空间,就会提早触发1次 GC 的搜集动作。
方法区(永久代):
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有援用了就能够被回收。对无用的类进行回收,必须保证3点:
垃圾搜集算法是 内存回收的理论基础,而垃圾搜集器就是内存回收的具体实现。下面介绍1下HotSpot(JDK 7)虚拟机提供的几种垃圾搜集器,用户可以根据自己的需求组合出各个年代使用的搜集器。
1.Serial/Serial Old
Serial/Serial Old搜集器是最基本最古老的搜集器,它是1个单线程搜集器,并且在它进行垃圾搜集时,必须暂停所有用户线程。Serial搜集器是针对新生代的搜集器,采取的是Copying算法,Serial Old搜集器是针对老年代的搜集器,采取的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
2.ParNew
ParNew搜集器是Serial搜集器的多线程版本,使用多个线程进行垃圾搜集。
3.Parallel Scavenge
Parallel Scavenge搜集器是1个新生代的多线程搜集器(并行搜集器),它在回收期间不需要暂停其他用户线程,其采取的是Copying算法,该搜集器与前两个搜集器有所不同,它主要是为了到达1个可控的吞吐量。
4.Parallel Old
Parallel Old是Parallel Scavenge搜集器的老年代版本(并行搜集器),使用多线程和Mark-Compact算法。
5.CMS
CMS(Current Mark Sweep)搜集器是1种以获得最短回收停顿时间为目标的搜集器,它是1种并发搜集器,采取的是Mark-Sweep算法。
6.G1
G1搜集器是现今搜集器技术发展最前沿的成果,它是1款面向服务端利用的搜集器,它能充分利用多CPU、多核环境。因此它是1款并行与并发搜集器,并且它能建立可预测的停顿时间模型。
上一篇 计算机网络之路由协议详解