写在前面:主要为《深入理解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垃圾收集器


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

Java内存分配与垃圾回收(二)相关推荐

  1. java内存分配和垃圾回收_深入理解java虚拟机(二)垃圾收集器与内存分配策略...

    垃圾收集器与内存分配策略 垃圾收集,三个步骤 什么时候收集,收集那些,怎么收集 1.收集那些 我们会将一些不使用的对象进行收集,进行回收内存空间,我们怎么知道呢 1.引用计数法 如果这个实例被其他地方 ...

  2. java内存分配和垃圾回收,Java内存分配与垃圾回收

    1.JVM管理的内存包含下图所示的几个运行时数据区域,其中方法区和堆为线程共享的数据区域,程序计数器,虚拟机栈以及本地方法栈为线程私有的数据区域. 程序计数器:可以看做是当前线程所执行的字节码的行号指 ...

  3. JavaScript内存分配及垃圾回收机制

    JavaScript内存分配及垃圾回收机制 简介 像C语言这样的高级语言一般都有底层的内存管理接口,比如 malloc()和free().另一方面,JavaScript创建变量(对象,字符串等)时分配 ...

  4. java内存 海子_Java垃圾回收机制 - 海 子

    Java垃圾回收机制 说到垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和Java联系起来.在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,这一切都交给 ...

  5. java 虚拟机 Java内存结构 JVM垃圾回收机制算法

    什么是HotSpot VM 提起HotSpot VM,相信所有Java程序员都知道,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机. 但不一定所有人都知道的是, ...

  6. Java内存管理与垃圾回收

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 整个教程中已经不时的出现一些内存管理和垃圾回收的相关知识.这里进行一个小小的总结. ...

  7. JVM结构、内存分配、垃圾回收算法、垃圾收集器。

    2019独角兽企业重金招聘Python工程师标准>>> 一.JVM结构 根据<java虚拟机规范>规定,JVM的基本结构一般如下图所示: 从左图可知,JVM主要包括四个部 ...

  8. JVM内存分配与垃圾回收

        其实已经有很多大牛在这方面做了很好的介绍,我在这篇文章里讲下我自己的一些理解,受限于我的认知水平,可能不一定正确,请自我甄别. JVM的GC自动垃圾回收器是JAVA的一大特色,垃圾回收器要解决 ...

  9. JVM内存分配与垃圾回收浅析

    为什么80%的码农都做不了架构师?>>>    想做architect,就必须对JVM的性能有所了解.JVM的内存管理是性能的一大瓶颈.JVM的性能调优,必须建立在对内存管理策略理解 ...

最新文章

  1. 【Spring】spring基于纯注解的声明式事务控制
  2. 一文详解C++文件读写(FileStorage、txt)
  3. 数据蒋堂 | 数据分布背后的逻辑
  4. Android Studio查看Android源代码失败
  5. XML DOM Node List
  6. Linux睡眠唤醒机制分析--以IMX6UL为例
  7. Max retries exceeded with url 解决方案
  8. mybatis 中 Example 的使用 :条件查询、排序、分页(三种分页方式 : RowBounds、PageHelpler 、limit )
  9. Linux命令之crontab命令
  10. 年轻人创业可以选择的3个方向
  11. 学透 LinkedList 底层实现原理,狂虐面试官!
  12. 090723 T Code Generate 的思考
  13. python的应用会超过java吗_JAVA会被Python超越成为世界上第一大编程语言吗?
  14. 机器学习 | 数学基础
  15. Java界面设计GUI
  16. 计算机在档案管理出现的问题,浅议档案管理中存在的问题及解决措施_档案管理员资格证...
  17. 从零开始学USB(十五、USB的设备状态)
  18. Bootstrap Table Fixed Columns
  19. ios 侧滑返回停顿_iOS侧滑卡死解决方法
  20. 2022年亚太杯数学建模竞赛ABC题

热门文章

  1. shell入门保姆教程
  2. Kali如何使用Reaver破解Wi-Fi网络的WPA/WPA2密码
  3. unity游戏开发日志(一)将mmd模型导入unity,并解决材质丢失的问题
  4. dependenciesManagement 和dependencies 解释
  5. ZBrush新手普遍疑惑,你遇到几个?常用ZBrush快捷键汇总
  6. 微信开发者管理工具上线内测版,增加了内置的git管理和支持npm安装,6666666……
  7. RSA密码体制(头歌)
  8. .Net开源工作流,100%源码开放
  9. 云计算基本概念IaaS,PaaS,SaaS和Serverless
  10. combobox 远程url java返回参数_TopJUI可编辑表格的列根据返回数据判断是使用 combobox 还是 numberbox...