《深入理解java虚拟机》第三章之垃圾回收

线程独占的程序计数字、虚拟机栈、本地方法栈随线程而生,随线程而亡;栈中的内存,随着方法的结束自然而然能得到回收,且每个栈帧分配多少内存基本上是在类结构确定下来就已知。因此回收的重心不在这几个区域

堆和方法区由于对象创建的动态性(例如不同的分支创建的对象不同),因此垃圾回收讨论的“内存”只针对这两个区域

GC学习围绕三个问题

  • 什么对象需要回收
  • 如何回收
  • 何时回收

问题一:如何判断对象已死?

1.1 引用计数算法

创建对象的过程是:在栈内存中的局部变量表,生成对象的引用。在堆内存中生成对象的实例,第一次加载还需在方法区加载类的信息。

这个算法就是:当对象被引用一次时,计数器+1 (创建时被栈的引用当然也算),引用失效时 - 1(null)。

优点就是实现简单、速度快;缺点就是无法处理循环引用的情况。看图
在这里插入图片描述在这里插入图片描述

1.2 可达性分析法
1.2.1GC Roots?
  • 虚拟机栈(本地变量表)中的引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地栈中JNI(也就是Native方法)引用的对象
1.2.2 算法

通过GC Roots出发,能遍历到的对象就保留,遍历不到的对象就回收。

问题二:如何回收?

2.1 回收策略(算法)

2.1.1 标记-清除算法

通过问题一的算法标记垃圾对象,再由专门的清除程序进行清除。

  • 效率低
  • 产生大量零碎空间
2.1.2 复制算法:多数虚拟机用之于回收新生代

算法核心:把内存划分成两块,一块用于放置新加入的对象,进行垃圾回收时,把这块内存中存活的对象放入另一块内存。也就是说牺牲一半的内存用于放置存活的对象。实际上不需要浪费这么多内存。

堆的内存还需做详细划分
在这里插入图片描述
新生成的对象放入Eden区、和Survivor区,垃圾回收时就把存活的对象放入另一块Survivor中。这样牺牲的内存大大减小了。
当一块Survivor放不下存活对象时,就只能放到老年代中了。这也叫内存担保。

在这里插入图片描述

2.1.3 标记-整理算法:回收老年代

老年代中对象的特点是存活概率大,因此用复制算法花销巨大。

它的做法是:垃圾回收时,对标记的对象不直接清除,而是让未标记的对象先移动到一边,然后再清除掉存活对象之外的一大块垃圾对象内存。

2.1.4 分代收集算法

把复制算法和标记-整理或标记-清除做总结,新生代中使用复制算法,老年代中使用标记-整理或者标记-清除算法。

2.2 垃圾回收器

垃圾收集器有很多种,不同java虚拟机会搭配不同的垃圾收集器。

2.2.1 Serial收集器(复制算法)

暂停世界,回收垃圾时只允许回收线程允许。适用于新生代内存小的客户端应用回收新生代。

2.2.2 ParNew 收集器

是Serial的多线程版本,优势在于多线程环境下大大提高效率,可以和CMS收集器搭配使用。

2.2.3 Parallel Scavenge收集器

和ParNew相比,它的着重点在于吞吐量可控(用户代码运行时间 / 用户代码运行时间 + 垃圾回收时间);还有一点是无法与CMS收集器合作

它提供了控制最大停顿时间的 -XX:MaxGCPauseMilis 和设置吞吐量大小的 -XX:GCTimeRatio参数。值得一提的是,最大停顿时间的实现是在调整新生代内存的基础上完成,新生代内存变小了自然收集时间缩短,这样的代价是收集频率提高,因此不见得好。

自适应调节策略

-XX:+UseAdapteriveSizePolicy:把各种参数的调节交给虚拟机完成,动态调整这些参数以提供最合适的停顿收集或者最大的吞吐量。
具体步骤: 设置好基本的内存数据(如-Xmx设置最大堆), 然后使用MaxGCPauserMillis参数(更关注最大停顿时间)或GCTimeRatio(更关注吞吐量)给虚拟机一个目标,这样的话虚拟机就会自动调节。

2.2.4 Serial Old收集器(老年代)

如同Serial, 这款收集器也主要是给Client下的虚拟机使用。

2.2.5 Parallel Old 收集器(老年代)

是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量优先的场景下,可以使用Parallel Scavenge + Parallel Old。

2.2.6 *重点:CMS收集器(老年代)

CMS(Concurrent Mark Sweep:并发标记收集器)目标是获取最短回收停顿时间,因此适用于互联网站或者B/S模式系统(浏览器-服务器模式)的服务器上。

CMS通过标记清除算法,分为以下几个步骤

  • 初始标记: 需要Stop The World
  • 并发标记: 和用户线程并发执行
  • 重新标记: 需要Stop The World
  • 并发清除: 和用户线程并发执行
    在这里插入图片描述
执行步骤
  1. 初始标记:就是对GC Roots进行标记,由于OOPMap的存在,它占用的时间很短

  2. 并发标记:有了GC Roots,再使用可达性分析法就能不断地标记哪些对象不是垃圾了,也就是Tracing的过程,这个过程是和用户线程并发执行的。

  3. 重新标记: Stop The World,书上讲的比较模糊,说是为了修正并发标记期间因用户线程运作而导致标记变动的那一部分对象。
    实际上分两种情况
    第一种:已经扫描过的引用,放弃了这块地址。 这种情况比较好办,留到下一轮再去清除
    第二种:已经扫描过的引用,指向了一块原本不可达的内存。
    例如 p1 = 内存 ; p2 = p1(p2指向了这块内存地址) ;p1 = null(p1放弃了这块内存) ;
    扫描的顺序是这样的: 先扫描p2, 之后p2指向了这块内存, 接着p1放弃了这块内存。 此时通过p1进行查找,发现内存不可达。因此这块内存被标记为垃圾,实际上它不是。
    解决这种情况就需要暂停世界,再从gc root 出发, 看看哪些标记是不应该打上去的。

  4. 并发清除:执行清除操作即可。

特点:并发占用了较高的CPU,牺牲了吞吐量,也就是CPU的运行效率来换取更短的停顿时间。
2.2.7 *重点:G1收集器(不再区分新生代、老年代)

整体基于标记-整理

将内存划分成多个相等的Region独立区域,新生代老年代不再时物理隔离的了,他们都是一部分Region(不需要连续)

G1收集器会在后台维护一个优先列表,表示每一块Region的性能指标,保证了每次回收得到的价值最大(回收所获得的空间大小以及回收所需的时间和经验值)。

Remembered Set

这是一种在可达性分析法中,避免全局扫描的优化策略。

在G1中,每一个Region都有一个RS,当程序在对Reference类型(引用数据类型)的数据进行写操作时(简单来说:Object obj = new Object(); obj = obj2;obj就进行了写操作)虚拟机就产生了一个Write Barrier暂时中断写操作,检查对象是不是处于不同的Region中,如果是就在被引用(obj2)的RS表上记录obj1所属的Region。 保证了不对全堆扫描也不会有遗漏。

在新生代老年代中的收集器也用了这种策略,检查是否有老年代的对象引用了新生代的对象。

G1收集器的特点有哪些呢?
  • 并行和并发: 很多步骤可以选择并发(和用户线程一起执行)
  • 分代收集
  • 空间整合: 整体基于标记-整理算法,不会产生空间碎片
  • 可预测的停顿: 个人理解是可以自定义停顿的时间模型。
G1收集器的收集步骤
  1. 初始标记:
    要Stop The World,暂停其他线程的工作。仅仅标记GC Roots能关联到的对象,因此时间很短。它的工作还有修改TAMS(Next To at Mart Start)的值,让下一阶段的用户程序直接找到正确的Region。

  2. 并发标记:
    使用可达性分析找出所有存活对象,基于Remembered Set的标记,因此不需要遍历整个堆。这一阶段是并发的。

  3. 最终标记:
    为了修改并发标记过程中,用户线程导致标记应该产生变动,而进行修正。值得一提的是虚拟机将用户线程操作记录在了Remembered Set Log上,最终标记把Log 上的数据整合到Remembered Set里即可。这一阶段可以并发也可以暂停

  4. 筛选回收
    根据Region的回收成本和价值进行排序,根据用户期望的GC停顿时间来指定回收计划。这里用暂停线程来提高回收的高效性。

以上垃圾回收过程完毕,来一个思维导图复习

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值