一、垃圾回收(GC)
在Java语言中,垃圾回收(Garbage Collection,GC) 是一个非常重要的概念,它的主要作用是回收程序中不再使用的内存。为了减轻开发人员的工作,同时增加系统的安全性与稳定性,Java语言提供了垃圾回收器来自动检测对象的作用域,可自动地把不再使用的存储空间释放掉。具体而言,垃圾回收器要负责完成3项任务:分配内存、确保被引用对象的内存不被错误地回收、回收不再被引用的对象的内存空间。
垃圾回收器的存在一方面把开发人员从释放内存的复杂工作中解脱出来,提高了开发人员的生产效率;另一方面,对开发人员屏蔽了释放内存的方法,可以避免因开发人员错误地操作内存而导致应用程序地崩溃,保证了程序的稳定性。但是,垃圾回收也带来了问题,为了实现垃圾回收,垃圾回收器必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放后还需要处理堆中的碎片,这些操作必定会增加JVM的负担,从而降低程序的执行效率。
在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
对对象而言,如果没有任何变量去引用它,那么该对象将不可能被程序访问,因此可以认为它是垃圾信息,可以被回收。只要有一个以上的变量引用该对象,该对象就不会被垃圾回收。(注:当一个对象不再被引用后就成为垃圾可以被回收,但是线程就算没有被引用也可以独立运行的,因此与对象不同。)
对于垃圾回收器来说,它使用有向图来记录和管理堆内存中的所有对象,通过这个有向图就可以识别哪些对象是”可达的“(有引用变量引用它就是”可达的“),哪些对象是”不可达的“(没有引用变量引用它就是“不可达”的),所有”不可达“对象都是可被垃圾回收的。
二、垃圾回收算法
垃圾回收都是依据一定的算法进行的,下面介绍其中几种常用的垃圾回收算法。
1、 引⽤计数(Reference Counting)
⽐较古⽼的回收算法。原理是此对象有⼀个引⽤,即增加⼀个计数,删除⼀个引⽤则减少⼀个计数。垃圾回收时,只⽤收集计数为0的对象。此算法最致命的是⽆法处理循环引⽤的问题。
2、标记-清除(Mark-Sweep)
此算法执⾏分两阶段。第⼀阶段从引⽤根节点开始标记所有被引⽤的对象,第⼆阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应⽤,同时,会产⽣内存碎⽚。(具体内容见十一节)
3、复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使⽤其中⼀个区域。垃圾回收时,遍历当前使⽤区域,把正在使⽤中的对象复制到另外⼀个区域中。此算法每次只处理正在使⽤中的对象,因此复制成本⽐较⼩,同时复制过去以后还能进⾏相应的内存整理,不过出现“碎⽚”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
4、标记-整理(Mark-Compact)
此算法结合了 “标记-清除”和“复制”两个算法的优点。也是分两阶段,第⼀阶段从根节点开始标记所有被引⽤对象;第⼆阶段,遍历整个堆,清除未标记对象并且把存活对象 “压缩”到堆的其中⼀块,按顺序排放。此算法避免了“标记-清除”的碎⽚问题,同时也避免了“复制”算法的空间问题。
5、增量收集(Incremental Collecting)
实施垃圾回收算法,即:在应⽤进⾏的同时进⾏垃圾回收。不知道什么原因JDK5.0中的收集器没有使⽤这种算法的。
6、分代(Generational Collecting)
基于对对象⽣命周期分析后得出的垃圾回收算法。把对象分为年⻘代、年⽼代、持久代,对不同⽣命周期的对象使⽤不同的算法(上述⽅式中的⼀个)进⾏回收。现在的垃圾回收器(从J2SE1.2开始)都是使⽤此算法的。
(1) Young(年轻代)- 复制收集算法
年轻代分三个区。⼀个Eden区,两个 Survivor区。⼤部分对象在Eden区中⽣成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的⼀个),当这个 Survivor区满时,此区的存活对象将被复制到另外⼀个Survivor区,当这个Survivor去也满了的时候,从第⼀个Survivor区复制过来的并且此时还存活的对象,将被复制“年⽼区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同⼀个区中可能同时存在从Eden复制过来对象,和从前⼀个Survivor复制过来的对象,⽽复制到年⽼区的只有从第⼀个Survivor去过来的对象。⽽且,Survivor区总有⼀个是空的。
(2) Tenured(年⽼代)- 标记整理算法
年⽼代存放从年轻代存活的对象。⼀般来说年⽼代存放的都是⽣命期较⻓的对象。年轻代的对象如果能够挺过数次收集,就会进⼊⽼⼈区。⽼⼈区使⽤标记整理算法。因为⽼⼈区的对象都没那么容易死的,采⽤复制算法就要反复的复制对象,很不合算,只好采⽤标记清理算法,但标记清理算法其实也不轻松,每次都要遍历区域内所有对象,所以还是没有免费的午餐啊。-XX:MaxTenuringThreshold= 设置熬过年轻代多少次收集后移⼊⽼⼈区,CMS中默认为0,熬过第⼀次GC就转⼊,可以⽤-XX:+PrintTenuringDistribution 查看。
(3) Perm(持久代/永生代)
⽤于存放静态⽂件,如今Java类、⽅法等。持久代对垃圾回收没有显著影响,但是有些应⽤可能动态⽣成或者调⽤⼀些class,例如Hibernate等,在这种时候需要设置⼀个⽐较⼤的持久代空间来存放这些运⾏过程中新增的类。持久代⼤⼩通过-XX:MaxPermSize=进⾏设置。注意Spring,Hibernate这类喜欢AOP动态⽣成类的框架需要更多的持久代内存。⼀般情况下,持久代是不会进⾏GC的,除⾮通过 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled进⾏强制设置。
注:
(1)Java 8取消了永生代,采用了Metaspace。
(2)新生代内存不够时发生Micro GC也叫Young GC,JVM内存不够时,会发生Full GC。
三、是否可以主动通知JVM进行垃圾回收?
答:由于垃圾回收器的存在,Java语言本身没有给开发人员提供显式释放已分配内存的方法,也就是说,开发人员不能实时地调用垃圾回收器对某个对象或所有对象进行垃圾回收。但开发人员却可以通过调用System.gc()方法来”通知“垃圾回收器运行,当然,JVM也并不会保证垃圾回收器马上就会运行。由于System.gc()方法的执行会停止所有响应,去检查内存中是否有可回收的对象,这会对程序的正常运行以及性能造成极大的威胁,因此不推荐频繁的使用这一方法。
四、GC的停顿现象
GC任务是识别和回收垃圾对象,进行内存清理。为了让GC可以高效的执行,在进行GC时,系统会进入一个停顿的状态。
停顿目的:终止所有应用线程 ,只有这样,系统才不会有新的垃圾产生。
停顿保证了系统状态:在某一瞬间的一致性,有益于更好的标记垃圾对象。因此,在GC时,都会产生应用程序的停顿。减少GC 可以减少程序的停顿,提高系统的性能。