Java内存分配与垃圾回收(二)

写在前面:主要为《深入理解Java虚拟机》的读书笔记,加上自己的思考,本篇主要讲垃圾回收,图片主要来自网络,侵删。

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外的人想进来,墙里面的人却想出来。


一、概述

Java虚拟机中的内存回收主要集中在Java堆和方法区内。
关于内存回收:哪些内存需要回收?什么时候回收?如何回收??

由上篇Java内存分配可知,Java运行时数据存储区域包括:Java方法栈、Native方法栈、程序计数器、Java堆和方法区。
其中,Java方法栈、Native方法栈和程序计数器属于线程私有,伴随线程的创建而创建,当线程结束或方法结束时,内存自然回收,因此不需要垃圾回收。
而Java堆和方法区,所有线程共享,随着虚拟机的创建而创建,程序在运行期间才知道创建哪些对象,这部分内存的分配和回收都是动态的,因此需要进行垃圾回收。

1.1 如何判断对象死亡?
我们知道,当对象死亡的时候,可以进行垃圾回收。那么如何判断对象是否死亡呢?通常有引用计数算法和可达性分析算法。

  • 引用计数算法
    给对象添加引用计数器,当计数为0时表示没有被任何对象引用,即可回收。
    优点:判定效率高;
    缺点:难以解决对象之间相互循环引用的问题(一群无用对象的循环引用)。

  • 可达性分析算法
    把一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,当一个节点到GC Roots没有任何引用链时(没有通路,即不可达),则证明这个对象是不可用的。

    然而,哪些对象可以作为“GC Roots”对象呢?(栈中的内容肯定是当前线程所需,方法区中的内容也会长期存活)
    ①Java方法栈中引用的对象(reference引用的对象);
    ②本地方法栈(即Native方法)中引用的对象;
    ③方法区中静态属性引用的对象;
    ④方法区中常量引用的对象。

1.2 Java中的对象引用

不管是引用计数还是可达性分析,都和引用有关。Java中引用包括:强引用、软引用、弱引用和虚引用。

  • 强引用
    最普遍的引用,只要强引用存在,对象肯定不会被回收。

  • 软引用
    描述有用但并非必需的对象,当内存不足时,就会回收这些对象。Java中提供SoftReference类。
    一般用于内存敏感的高速缓存中。

  • 弱引用
    描述非必需对象,强度比软引用更弱。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。Java中提供WeakReference类。

  • 虚引用
    又称幽灵引用或幻影引用,最弱的一种引用关系。虚引用对对象的生命周期不会有任何影响,不能通过虚引用取得对象的实例。唯一的目的是,设置虚引用的对象在被回收时会收到一个系统通知。Java中提供PhantomReference类。

这里写图片描述

1.3 方法区何时回收
Java虚拟机规范中,对方法区(HotSpot虚拟机中称为永久代)的回收并非必需的,而且在方法区的垃圾回收“性价比”很低。在堆中,尤其新生代中,一次垃圾收集一般可回收70%~95%的空间,而永久代的回收效率远低于此。

永久代的垃圾收集对象包括:废弃的常量和无用的类。

  • 废弃的常量
    与回收堆里面的对象非常相似,假设字符串“abc”存在于常量池中,然而系统中没有任何String对象的内容是“abc”,即没有任何对象引用常量池中的常量,如果这个时候发生内存回收,这个常量就会被清理出常量池。
  • 无用的类

    • 该类所有实例都被回收,即Java堆中不存在该类的实例;
    • 加载该类的ClassLoader已被回收;
    • 该类对应的Class对象没有在任何地方被引用,即没有任何地方通过反射使用该对象。

    在大量使用反射、动态代理的框架中虚拟机一般需具备类卸载的能力。

1.4 finalize()自我拯救
即便在可达性分析中的不可达对象,也不是“非死不可”的。他们暂时处于缓刑阶段,可通过finalize自我救赎进行逃逸。
判定对象死亡,至少要经历两次标记。

  • 第一次标记
    进行一次可达性分析,不可达对象将被第一次标记,并对这些对象进行筛选:对象是否有必要执行finalize方法。(对象没有覆写finalize或finalize方法已被调用过,则视为“没有必要执行”),把有必要执行finalize方法的对象放入F-Queue队列中。(没有必要执行的一定会被回收,没有逃逸机会)

  • 第二次标记
    第二次标记的作用对象是:F-Queue队列中的对象。虚拟机会启用一个低优先级的线程去触发F-Queue队列中对象的finalize方法,但不保证它会执行完。稍后GC将进行第二次标记。finalize方法是对象死亡逃逸的最后一次机会,只要重新与引用链的任何一个对象建立关联即可。

注意:
1)任何对象的finalize方法只会被系统自动调用一次,因此也只会自救一次。再次面临回收的时候,自救失败。(然而,不确定性大,并不鼓励程序员用这种方法拯救对象)
2)很多书中建议在finalize方法中进行关闭资源的工作,然而这些在try-finally中可以做的更好。finalize方式不确定性大、运行代价高昂,可以忽略此用法。

1.5 垃圾回收算法

  • 标记 - 清理算法
    这里写图片描述

    • 算法思想
      首先标记出所需清理的对象,然后对标记的对象统一清理。

    • 不足
      效率低;产生大量空间碎片分配较大对象时容易引起频繁的GC。

    • 应用
      是最基础的垃圾回收算法,后续的算法在此基础上改进。
  • 复制算法
    这里写图片描述

    • 算法思想
      1)把可用内存分成大小相等的两块,每次只使用其中的一块;
      2)当一块内存不足时,就将这块内存上“活”的对象复制到另外一块未使用的内存上;
      3)把已使用的那块内存全部清理掉。

    • 优缺点
      1)内存分配时不用考虑空间碎片,只要移动堆顶指针即可。
      2)实现简单,运行高效。
      3)代价:内存容量缩小为原来的1/2,当存活的对象较多时要进行大量复制。

    • 应用
      一般用于新生代的回收(新生代98%对象都是“朝生夕死”),作了一点改进:
      1)把可用内存分为Eden区:Survivor1区:Survivor2区 = 8:1:1;
      2)分配内存时,只是用Eden区和其中一块Survivor区(假设Survivor1);
      3)回收时将存活的对象一次性复制到另一块空闲的Survivor区(Survivor2),最后清理掉Eden和刚才用过的Survivor区(Survivor1);
      4)可以看出,Survivor1和Survivor2总有一个是空白的,二者交替充当被使用和空闲的角色。
      这样,每次只有10%的内存被“浪费”。
      考虑一种情况,回收时存活的对象可能超过10%,会如何应对呢?
      答案是分配担保。(如果另一块Survivor没有足够空间存放存活的对象,这些对象将通过分配担保直接进入老年代)

  • 标记 - 整理算法

这里写图片描述
- 产生背景
容易看出,复制算法在有大量对象存活期较长时是不适用的,因此不适用于老年代。
针对老年代的特点,提出了“标记 - 整理”算法。

  • 算法思想
    1)标记过程与“标记 - 清理”算法一样;
    2)后续步骤不是直接清理,而是把所有存活的对象都移向一端,直接清理掉边界以外的内存。

  • 优点
    1)避免产生大量空间碎片;
    2)避免1/2的容量浪费。

    • 分代收集算法
      综合使用以上几种算法的优点。根据对象的存活周期,把内存划分为几块,一般为新生代和老年代。
      新生代:大部分对象存活周期短,选用“复制”算法;
      老年代:对象存活率高,一般选用“标记 - 清理”或“标记 - 整理”算法。

二、HotSpot的垃圾回收算法实现

2.1 如何枚举GC Roots根节点
我们知道,对象可达性分析中GC Roots根节点主要包括栈和方法区所引用的对象。
那么实际设计中如何逐个检查这些引用呢?
为了保证准确性,显然我们在枚举根节点的时候,应该停止所有的Java用户线程。(Stop-The-World,使整个分析过程,系统好像冻结到某个时间点),为了让这个时间尽量短(否则用户线程阻塞太久),主流的虚拟机都是采用准确式GC,并不需要挨个扫描方法栈,就可以得知哪些位置上存放着对象引用。这个又是如何实现的呢?
在HotSpot中,使用一组OopMap的数据结构,在类加载完的时候,就把对象内什么偏移量什么数据类型的数据计算出来,在编译期也会在特定位置(安全点)记录栈和寄存器中哪些位置是引用。这样可以不用全局扫描即可知道根节点。

2.2 安全点SafePoint

前面我们知道了OopMap的概念,然而为每一条指令的位置都生成对应的OopMap显然不显示。前面提到的“特定位置”即安全点:程序执行并非所有地方都可以停下来GC,只有到达安全点才可以。
○ 关于安全点的选择?
既不能让GC等待太久,也不能太过频繁增加负荷。
普通指令执行很快,一般遇到“长时间执行”的指令才会产生安全点,包括:方法调用、循环跳转等。

○ GC时如何让所有线程跑到安全点再停下来?

抢先式中断和主动式中断。
抢先式中断:
一般不采用。GC时暂停所有线程,如果发现有线程没在安全点,则让它跑到安全点。
主动式中断:
GC中断线程不直接对线程操作,而是设置一个中断标志位。线程在每一个安全点检查这个标志位即可。

2.3 安全区域SafeRegion
考虑一下,程序不执行的时候(没有分配到cpu时间片)如何跑到安全点呢?
于是,提出了扩展的安全点——安全区域的概念。
安全区域:一段代码中,对象引用没有发生变化,任何地方开始GC都是安全的。
当线程执行到安全区域时,首先标识自己已经进入安全区域,这中间如果发生GC,就不用管标识为安全区域的线程了。
线程离开安全区域之前,需要确定自己已经完成了根节点枚举的过程,否则必须等待完成。


三、HotSpot垃圾收集器

HotSpot垃圾收集器
3.1 基本概念
(1) 并发 & 并行

  • 并发(concurrent)
    指用户线程和垃圾收集线程同时执行(可能是分配时间片交替执行),用户程序继续运行,而垃圾收集线程运行于另一个CPU上。

  • 并行(Parallel)
    指多条垃圾收集线程并行工作,但此时用户进程处于等待状态。

(2)Minor GC & Full GC

  • 新生代GC(Minor GC)
    指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 老年代GC(Full GC 或 Major GC)
    指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的)。Major GC的速度一般会比Minor GC慢10倍以上。

(3)吞吐量
吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
如:系统运行100min,其中垃圾收集1min,那么吞吐量就为99%。

3.2 Serial 收集器

  • 使用场景
    是虚拟机运行在client模式下默认的新生代收集器,可与CMS配合。
    这里写图片描述
  • 基本原理
    顾名思义,单线程收集器。垃圾回收时,必须暂停其他所有线程(Stop-The-World)。
  • 优缺点
    简单而高效(相比于其他收集器的单线程),对于单CPU环境,没有线程交互的开销。
    一般来说,停顿时间为几十毫秒到一百多毫秒,可以接受。

3.3 ParNew 收集器

  • 使用场景
    是虚拟机运行在Server模式下首选的新生代收集器,并行,可与CMS配合。
    这里写图片描述
  • 基本原理
    是Serial 收集器的多线程版本,默认开启的收集线程数等于CPU数。
  • 优缺点
    单CPU环境:Serial收集器效果更好(没有线程切换的开销)。
    多CPU环境:GC时能有效利用系统资源。

3.4 Parallel Scavenge 收集器

  • 使用场景
    吞吐量优先的新生代收集器,并行,不可与CMS配合。
    吞吐量优先可尽快完成程序的运算任务(没有优先相应用户交互),适于后台任务。
  • 基本原理
    提供两个参数精确控制吞吐量:
    ○ -XX:MaxGCPauseMillis参数 – 最大垃圾收集停顿时间。
    ○ -XX:GCTimeRatio参数 – 设置吞吐量。
    此外,还挺提供-XX:+UseAdaptiveSizePolicy开关参数来自适应调节Eden和Survivor比例等参数来达到最大的吞吐量。

  • 优缺点
    可自适应调节;
    可控吞吐量。

3.5 Serial Old 收集器

  • 使用场景
    Serial 收集器的老年代版本,单线程,主要给client模式下的虚拟机使用。
    在JDK 1.5以及之前的版本中,搭配Parallel Scavenge 收集器使用;
    作为CMS后备方案,CMS并发收集失败时使用。
  • 基本原理
    这里写图片描述
    使用“标记 - 整理”算法。

3.6 Parallel Old 收集器

  • 使用场景
    Parallel Scavenge的老年代版本,并行,JDK1.6才开始提供。
    注重吞吐量和CPU资源敏感的场合,可使用Parallel Scavenge + Parallel Old组合。
    这里写图片描述
  • 基本原理
    使用多线程和“标记 - 整理”算法。

  • 优缺点
    在JDK1.6之前,Parallel Scavenge 处于比较尴尬的位置(因为无法搭配CMS),只能和Serial Old配合,由于Serial Old在服务端性能的拖累,使得Parallel Scavenge + Serial Old组合还不如ParNew+CMS给力。
    Parallel Old的出现,Parallel Scavenge + Parallel Old组合改变了这种尴尬局面。

3.7 CMS 收集器

  • 使用场景
    强调最短的回收停顿时间,重视服务的响应速度,带来良好用户体验;
    适于互联网网站或B/S服务端。
  • 基本原理
    这里写图片描述
    Concurrent Mark Sweep,并发,“标记 - 清理 ”。包括4个步骤:

    • 初始标记
      需要stop-the-world,速度快,标记GC Roots能直接关联到的对象。
    • 并发标记
      与用户进程并发,进行GC Roots Tracing。
    • 重新标记
      需要stop-the-world,速度快,修正并发标记过程中用户进程的执行所带来的改变。
    • 并发清除
      与用户进程并发,并发清除阶段会清除对象。
      由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
  • 优缺点

    • 并发收集、低停顿。
    • 对CPU资源十分敏感:当CPU不足时,因为CMS的内存回收和用户线程并发执行,导致CMS分配比较大的比例用于内存回收,导致用户进程执行缓慢。
    • 无法处理浮动垃圾
      浮动垃圾:因为垃圾收集和用户线程并发,用户线程产生的新的垃圾必须等下次才能回收,称为浮动垃圾。
      此外,由于垃圾收集和用户线程并发执行,需要预留一部分空间给用户线程使用,因而不能等老年代全部使用才进行回收,JDK1.5默认情况下68%被使用就会激活Full GC。而JDK1.6调整至92%。如果CMS期间预留的内存无法满足需求,将会“Concurrent mode Failure”,临时启用Serial Old的后备方案。
    • 产生大量空间碎片
      是”标记 - 清理“算法带来的后果。
      解决方案:
      1. CMS提供-XX:+UseCMSCompactAtFullCollection,在进行FullGC时,开启碎片整理过程,代价是停顿时间变长。
      2. CMS提供-XX:CMSFullGCsBeforeCompaction,设置执行多少次不压缩的FullGC,进行一次压缩的FullGC。

3.8 G1 收集器

  • 使用场景
    G1 收集器是当前最前沿成果之一,JDK1.7被提出,一款面向服务端应用的垃圾收集器。
    这里写图片描述
  • 基本原理
    G1(Garbage First),回收区域带有优先级的垃圾回收。
    其他收集器进行收集的范围都是整个新生代或者老年代,而G1将Java堆分成多个大小相等的独立区域Region,新生代和老年代不再是物理隔离(可能是一部分不需要连续的Region的集合)。
    之所以能建立可预测的停顿,是因为G1避免全区域的垃圾收集,每次根据允许的收集时间,优先回收价值最大的Region(价值按回收所获得的空间大小和回收所需时间的经验值来计算),保证G1在有限时间内获得最高的收集效率。

  • 初始标记
    需要stop-the-world,速度快,标记GC Roots能直接关联到的对象。

  • 并发标记
    与用户进程并发,进行GC Roots Tracing。
  • 最终标记
    需要stop-the-world,速度快,修正并发标记过程中用户进程的执行所带来的改变。
  • 筛选回收
    对各Region的回收价值和成本进行排序,根据用户期望的GC停顿时间制定回收计划。
    这一阶段,可以与用户线程并发,但停顿用户线程可大幅度提高效率。

  • 优缺点

    • 并行与并发
      充分利用多CPU的优势来缩短stop-the-world的时间。
    • 分代收集
      虽然G1不需要与其他收集器配合就能完成新生代和老年代的回收,但仍保留分代概念,采用不同的方式处理。
    • 空间整合
      G1整体上是”标记 - 整理“,局部基于”复制“,所以不会产生空间碎片。
    • 可预测的停顿
      G1的一大优势就是基于按优先级分区回收的可预测的停顿。

这里写图片描述


四、内存分配与回收策略

4.1 对象优先在Eden区分配
大多数情况,对象在新生代Eden区分配内存,当Eden区没有足够空间时,将发起一次Minor GC。
4.2 大对象直接进入老年代
大对象:如很长的字符串和数组。
大对象直接进入老年代可以节省大量的复制开销。
程序员应尽量避免”短命大对象“,”短命大对象“容易引起频繁的GC。
4.3 长期存活的对象将进入老年代
当对象进入Survivor区,设置年龄为1,在Survivor区每熬过一次GC,年龄加1。
当年龄到某个值(默认15),将被晋升到老年代。
4.4 动态对象年龄判定
如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,大于等于该年龄的对象可直接进入老年代。
这样可以防止Survivor区大量复制和减小空间分配担保的可能性。
4.5 空间分配担保
进行Minor GC之前,会检查老年代最大可用连续空间是否大于新生代所有对象空间。如果是,那么这次Minor GC是安全的。否则,检查是否允许担保失败:
允许:比较老年代最大可用连续空间是否大于历次晋升到老年代的平均值,如果大于,则尝试Minor GC,显然这是有风险的。如果小于,或者设置不允许冒险,则要先进行一次Full GC。


本文介绍了7种垃圾收集器,但并不能说哪个收集器更好,需要根据实际情况选择最优组合。


参考链接:
http://www.jianshu.com/p/50d5c88b272d
http://book.51cto.com/art/201107/278908.htm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值