java垃圾回收

来源:互联网 发布:云数据库的好处 编辑:程序博客网 时间:2024/05/16 18:24

垃圾回收

当一个对象不再被使用的时候,虚拟机就需要找到不在被使用的对象,然后将其所占的堆内存回收,以供后面的对象创建使用。
意义

引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

判断对象是否存活

这里存在这样一个问题,那就虚拟机如何去判断一个对象是否存活,是否要被回收。
主要是由下面几个方法:

1.引用计数器法

引用计数器法的原理就是当一个对象在堆中创建,并赋给一个引用的时候,该对象的引用计数器变为1,当有另外的引用指向该对象的时候,那么对象的引用计数器+1;当变量超出其作用范围,或者是指向一个新的对象的时候,对象的引用计数器-1。当对象的引用计数器变为0的时候,那么该对象就要被回收。
引用计数器法的优点就是引用计数器法实现简单判断效率也很高。
引用计数器法的著名引用案例使用ActionScript 3的FlashPlayer,Python语言。缺点就是无法解决对象之间的相互循环引用问题(java语言没有选用引用计数器法作为垃圾回收算法的原因)。
下面来看着例子:

package shida.fjh.demo;/** * 验证java垃圾回收不是采用引用计数器法 * vm args:  -XX:+PrintGCDetails */public class ReferenceCounting {    public Object instance;    public static void main(String[] args) {        ReferenceCounting objA = new ReferenceCounting();        ReferenceCounting objB = new ReferenceCounting();        objA.instance = objB;        objB.instance = objA;        objA = null;        objB = null;        System.gc();    }}输出结果:[GC (System.gc()) [PSYoungGen: 1787K->800K(16896K)] 1787K->808K(55296K), 0.0028997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 800K->0K(16896K)] [ParOldGen: 8K->617K(38400K)] 808K->617K(55296K), [Metaspace: 3219K->3219K(1056768K)], 0.0131872 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

从第一行的787K->808K(55296K)可以看出发生了GC,从来可以证明java并未采用引用计数器法作为垃圾回收算法。

2.可达性分析

可达性分析从图论出发,把所有对象的关系引用看作一张图。然后从一系列的GC ROOTS出发,然后向下搜索,把走过的路径称作引用链。当一个对象,没有任何一个可达链到GC ROOTS的时候,那么这么对象可不可用。
java语言中可以作为GC Roots的对象包括下面几种:

1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)

垃圾收集算法

1.标记-清除(Mark-Sweep)算法
首先从所有的GC Roots出发对所有对象进行标记,再扫描整个空间中未被标记的对象,直接回收其所占用的内存。
原理如图:
这里写图片描述
缺点:
-标记和清除两个过程效率都不太高。
-直接收回对象,会造成空间碎片。

2.复制算法(Copying)

首先将堆区划分为相等的两个区域,一个区域用来存放对象,另一个区域不存放任何对象。在进行GC的时候,从GC Roots出发对对象进行标记,然后将标记的对象,移动到空闲区域,再对标记区域进行清除。这样两个区域就像相当于替换了。
运行原理如图:
这里写图片描述

复制算法虽然解决了空间碎片问题,但存在一个明显的缺点就是浪费了将堆区划分为相等的两块浪费了大量的堆内存。研究表明98%的对象具有“朝生夕死”的特点,所以在划分区域的时候,通常是进行不等划分。比如新生代采用复制算法但是新生代内存划分为Eden : From Suvivor : to Suvivor = 8:1 :1

3.标记-整理(Mark-Compact)算法

为了解决复制算法的堆内存消耗的问题提出了标记-整理算法。
该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
原理如图:
这里写图片描述

标记-整理(Mark-Compact)算法不会产生空间碎片,同时也不会浪费大量的堆内存,但是对象移动增加了开销。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

4.分代(generation)算法

当前商业虚拟机垃圾回收算法基本都是用“分代收集”(Generational Collector)
这种算法是根据不同的对象具有不同的生命周期这个特点,将堆划分为新生代(Young Generation),老年代(Old Generation),持久代(Permanent Generation)
这里写图片描述
新生代
对象都在这里创建,它主要是回收那些生命周期短的对象。新生代采用的是复制算法,但是不同的是新生代将内存划分为一个Eden区、From Survivor区、To Survivor区,比例大概是8:1:1。新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高。
老年代
存放新生代经过多次垃圾回收还存活的对象,或者是大对象,一般来说老年代的所占用的内存空间要比新生代要很多(大概比例是1:2)。老年代一般采用标记-清除、或者标记-整理算法 。当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低。
持久代

i 用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

注意
持久代是HotSpot设计团队将垃圾回收扩展到了方法区,在其他的虚拟机中并不存在持久代这个概念。而且在jdk1.7之后也逐步移除了持久代。

垃圾收集器

HotSpot的垃圾收集器如下图:
这里写图片描述
新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

Serial收集器

单线程的复制算法。并不是因为着只有一个线程或者是使用一个CPU进行垃圾回收,最主要的特点是在垃圾回收的会stop the world(简称stw)。stw的特点是在进行垃圾回收的会停掉其他的线程,只有垃圾回收线程在运行。
优点
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
工作过程如图:
这里写图片描述

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本。工作过程如图:
这里写图片描述

Parallel Scavenge收集器

和ParNew收集器一样,但是不同的是Parallel Scavenge收集器最求的高的吞吐量
吞吐量 = 用户线程执行时间/(用户线程执行时间+GC线程执行时间)

Serial Old收集器

老年代垃圾回收器采用标记-整理算法

Parallel Old收集器

老年代垃圾回收器。Parallel Scavenge的老年版。

CMS(Concurrent Mark Sweep)收集器

老年代垃圾回收器,一款最求最短的时间停顿的垃圾回收算法,采用的是标记-清除算法
工作过程如图:
这里写图片描述
整个垃圾回收分为四步:
初始标记(CMS initial mark)
此过程,会短暂停顿用户线程(stop the world),并且只标记了和GC Roots直接相关的对象。
并发标记(CMS concurrent mark)
此过程和用户线程同时进行
重新标记(CMS remark)
重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发时间短。
并发清除(CMS concurrent sweep)
和用户线程同时进行。
注意
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点
1.CMS对cpu的资源很敏感。从CMS垃圾回收的步骤就可以看出来,并发标记和并发清除GC线程和用户线程需要同时运行,对cpu资源的要求当然更高。
2.无法处理浮动垃圾,可能出现“Concurrent Model Failure”失败而导致另一次Full GC的产生。因为CMS并发清除阶段是和用户线程一起运行的,但是,用户线程在运行的时候也会产生垃圾,而当前这个GC无法收集它,只能留到下次GC,这部分垃圾叫做“浮动垃圾”。因为在垃圾收集的过程中用户程序也在运行,所以,CMS无法像其他的垃圾收集器一样等到老年代满的时候再进行垃圾手机,而是预留一点空间来给用户线程分配对象。当预留的空间无法满足对象的分配的时候就发生了Concurrent Model Failure”,此时JVM会临时启动Serial Old来对老年代进行垃圾收集,因此就会导致很长的停顿时间。
3.会造成空间碎片。标记-清除算法的缺点就是造成,空间碎片,可能导致大对象分配在老年代的时候无法获得足够多的连续空间而不得不提前进行一个Full GC。

G1(Garbage-First)收集器

G1和CMS一样追求短时间停顿之外还有以下特点:
1.空间整合:和CMS标记-清除算法不同的事,G1从整体上来看是采用标记-整理算法,但是从局部(两个Region之间)上面来看是采用复制算法。这就表示G1不会像CMS一样产生空间碎片。
2.可预测时间停顿模型,这也是相对于CMS来说一个较大的改进。G1建立的原理是,G1将Java划分为不同的Region。在进行垃圾收集的时候,G1跟踪没有Region中垃圾堆积的价值大小(回收垃圾能够带来的空闲空间和回收这部份垃圾所需要的时间的经验值),再后台维护一个优先队列,每次再允许的垃圾收集时间内,回收价值最大的Region,这样可以有效的避免扫描整个堆而消耗大量的时间。
3.将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1的工作过程如图:
这里写图片描述
主要分为以下几步:
1.初始标记
只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
2.并发标记
对对象进行可达性分析,这个过程和用户线程同步进行,耗时较长。
3.最终标记
修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,这阶段需要停顿线程,但是可并行执行。
4.筛选回收
首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

原创粉丝点击