Java和C++之间有一堵由内存动态分布和垃圾回收技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
1、为什么要进行垃圾回收?
Java是一门面向对象的语言,在一个系统运行中,会伴随着很多对象的创建,而这些对象一旦创建了就占据了一定的内存,创建的对象是保存在堆中的,当对象使用完毕之后,不对其进行清理,那么会一直占据内存空间,很明显内存空间是有限的,如果不回收这些无用的对象占据的内存,那么新创建的对象申请不了内存空间,系统就会抛出异常而无法运行,所以必须要经常进行内存的回收,也就是垃圾收集。
2、那些区域内存需要回收?
Java运行时的内存结构,其中程序计数器、虚拟机栈、本地方法栈这三个区域是线程私有的,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而有条不紊的执行着入栈和出栈操作,这几个区域的内存分配和回收都具备确定性,在方法结束或线程结束时,内存也就跟着回收了,所以不需要我们考虑。
那么现在就剩下Java堆和方法区了,这两块区域在编译期间我们并不能完全确定创建多少个对象,有些是在运行时期创建的对象,所以Java内存回收机制主要是作用在这两块区域。
3、如何判断对象为垃圾对象
1)引用计数法
这种算法是这样的:给每一个创建的对象增加一个引用计数器,每当有一个地方引用它时,这个计数器就加1;而当引用失效时,这个计数器就减1。当这个引用计数器值为0时,也就是说这个对象没有任何地方在使用它了,那么这就是一个无效的对象,便可以进行垃圾回收了。
这种算法实现简单,而且效率也很高。但是Java没有采用该算法来进行垃圾回收,因为这种算法无法解决对象之间的循环引用问题。
2)可达性分析算法
我们这里直接给出结论:在主流的商用程序中(Java,C#),都是使用根搜索算法(GC Roots Tracing)来判定对象是否存活。
该算法思路:通过一系列名为“GC Roots” 的对象作为终点,当一个对象到GC Roots 之间无法通过引用到达时,那么该对象便可以进行回收了。
如下图:对象object5、object6、object7虽然互有关联,但是他们到GC Roots是不可达的,所以他们将会被判定是可回收的对象。

在Java语言中,有如下4中对象可以作为 GC Roots:

PS:红色的对象是要被当做垃圾回收的!
1 1、虚拟机栈(栈帧中的本地变量表)中引用的对象 2 2、方法区中的静态变量属性引用的对象 3 3、方法区中常量引用的对象 4 4、本地方法栈中(JNI)(即一般说的Native方法)的引用的对象
4、如何进行垃圾回收
垃圾回收涉及到大量的程序细节,而且各个平台的虚拟机操作内存的方式也不一样,但是他们进行垃圾回收的算法是通用的,所以这里我们也只介绍几种通用算法。
①、标记-清除算法
算法实现:分为标记-清除两个阶段,首先根据上面的根搜索算法标记出所有需要回收的对象,在标记完成后,然后在统一回收掉所有被标记的对象。
缺点:
1、效率低:标记和清除这两个过程的效率都不高。
2、容易产生内存碎片:因为内存的申请通常不是连续的,那么清除一些对象后,那么就会产生大量不连续的内存碎片,而碎片太多时,当有个大对象需要分配内存时,便会造成没有足够的连续内存分配而提前触发垃圾回收,甚至直接抛出OutOfMemoryExecption。

②、复制算法
为了解决标记-清除算法的两个缺点,复制算法诞生了。
算法实现:将可用内存按容量划分为大小相等的两块区域,每次只使用其中一块,当这一块的内存用完了,就将还活着的对象复制到另一块区域上,然后再把已使用过的内存空间一次性清理掉。
优点:每次都是只对其中一块内存进行回收,不用考虑内存碎片的问题,而且分配内存时,只需要移动堆顶指针,按顺序进行分配即可,简单高效。
缺点:将内存分为两块,但是每次只能使用一块,也就是说,机器的一半内存是闲置的,这资源浪费有点严重。并且如果对象存活率较高,每次都需要复制大量的对象,效率也会变得很低。

③、标记-整理算法
上面我们说过复制算法会浪费一半的内存,并且对象存活率较高时,会有过多的复制操作,效率低下。
如果对象存活率很高,基本上不会进行垃圾回收时,标记-整理算法诞生了。
算法实现:首先标记出所有存活的对象,然后让所有存活对象向一端进行移动,最后直接清理到端边界以外的内存。
局限性:只有对象存活率很高的情况下,使用该算法才会效率较高。

④、分代收集算法
当前商业虚拟机都是采用此算法,但是其实这不是什么新的算法,而是上面几种算法的合集。
算法实现:根据对象的存活周期不同将内存分为几块,然后不同的区域采用不同的回收算法。
1、对于存活周期较短,每次都有大批对象死亡,只有少量存活的区域,采用复制算法,因为只需要付出少量存活对象的复制成本即可完成收集;
2、对于存活周期较长,没有额外空间进行分配担保的区域,采用标记-整理算法,或者标记-清除算法。
比如,对于 HotSpot 虚拟机,它将堆空间分为如下两块区域:

5、何时进行垃圾回收
理清了什么是垃圾,怎么回收垃圾,最后一点就是Java虚拟机何时进行垃圾回收呢?
程序员可以调用 System.gc()方法,手动回收,但是调用此方法表示希望进行一次垃圾回收。但是它不能保证垃圾回收一定会进行,而且具体什么时候进行是取决于具体的虚拟机的,不同的虚拟机有不同的对策。
其次虚拟机会自行根据当前内存大小,判断何时进行垃圾回收,比如前面所说的,新生代满了,新产生的对象无法分配内存时,便会触发垃圾回收机制。
这里需要说明的是宣告一个对象死亡,至少要经历两次标记,前面我们说过,如果对象与GC Roots 不可达,那么此对象会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法,当对象没有覆盖 finalize()方法,或者该方法已经执行了一次,那么虚拟机都将视为没有必要执行finalize()方法。
如果这个对象有必要执行 finalize() 方法,那么该对象将会被放置在一个有虚拟机自动建立、低优先级,名为 F-Queue 队列中,GC会对F-Queue进行第二次标记,如果对象在finalize() 方法中成功拯救了自己(比如重新与GC Roots建立连接),那么第二次标记时,就会将该对象移除即将回收的集合,否则就会被回收。
6、垃圾收集器种类
事实上Java虚拟机规范对垃圾收集器应该如何实现,并没有任何的规定,所以不同的厂商、不同版本的虚拟机所提供的垃圾收集器都会有所不同,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
下图是基于 Sun HotSpot 虚拟机1.6版 Update 22的虚拟机种类:

由上图我们可以总结出几个结论:
①、新生代垃圾收集器:Serial、ParNew、Parallel Scavenge;
老年代垃圾收集器:Serial Old(MSC)、Parallel Old、CMS;
整堆垃圾收集器:G1
②、垃圾收集器之间的连线表示可以搭配使用,有如下几种组合:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
③、串行收集器Serial:Serial、Serial Old
并行收集器 Parallel:Parallel Scavenge、Parallel Old
并发收集器:CMS、G1
7、几个名词解释
①、并行
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
适合科学计算、后台处理等弱交互场景。
②、并发
指用户线程与垃圾收集器线程同时执行(但不一定是并行的,可能会交替执行),用户线程继续执行,而垃圾收集线程运行在另一块CPU上。
适合对响应快速的场景,比如Web。
③、停顿时间
垃圾收集器做垃圾回收中断应用执行的时间。
④、吞吐量
吞吐量 = 运行用户代码的时间 / (运行用户代码的时间+垃圾收集时间)
 
                     
                    
                 
                    
                 
 
         
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号