JVM读书笔记(二)——GC与内存分配策略

2.1 概述

Java运行时的内存中,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生灭;栈帧随着方法的进入和退出执行出战和入栈操作。每一个栈帧分配多少内存基本上是类结构确定下来就已知的,因此在这几个区域不需多考虑回收问题。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行时期才知道会创建哪些对象,这部分内存的分配和回收是动态的,GC所关注的是这部分内存。

2.2 对象是否已死
2.2.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;计数器为0时即不可能使用。

此算法实现简单,判定效率高,在大部分情况下是不错的算法。但主流的Java虚拟机里没有选用它来管理内存,因为它无法解决对象之间的循环引用。

2.2.2 可达性分析算法

在主流的商用程序语言的主流实现中,都是通过可达性分析来判定对象是否存活的。这个算法的基本思想就是通过一系列GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。

2.2.3 生存还是死亡

即使是可达性算法分析中不可达的对象,也不是非死不可,真正宣告一个对象死亡,至少要经历两次标记过程:第一次标记并筛选,筛选条件是此对象是否有必要执行finalize()方法。如果对象没有覆盖此方法,或已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。finalize()方法时逃脱死亡的最后一次机会,稍后GC将进行第二次标记,如果对象重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时它将被移出即将回收的集合,否则将被回收。

注意,应尽量避免使用finalize()方法,此方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序。有人说它可以做关闭外部资源的工作,完全是自我安慰。只要它能做的,try-finally或者其他方法都能做的更好。

2.2.4 回收方法区

很多认为方法区(HotSpot永久代)是没有垃圾收集的,Java虚拟机规范确实不要求在方法区实现GC,而且在方法区实现GC一般性价比较低:在堆中,尤其是新生代中,一次GC一般可以回收70-95%的空间,而永久代远低于此。

2.3 垃圾收集算法
2.3.1 标记-清除算法

首先标记所有需要回收的对象,在标记完成后统一回收。它是最基础的算法,后续算法都是由此改进。它有两个不足:一是标记和清除两个过程的效率都不高;另一个是空间问题,会产生大量不连续的内存碎片。

2.3.2 复制算法

它将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,就将还存活的对象放到另一块上面,然后把已使用的内存空间一次清理掉。这样内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种方法的代价较高。

2.3.3 标记-整理算法

标记后,让所有存活的对象都向一端移动,然后清理掉边界以外的内存。

2.3.4 分代收集算法

一般把Java堆分成新生代和老年代。新生代中,每次GC都有大批对象死去,采用复制算法,只需要复制少量存活对象。老年代中对象存活率高,没有额外空间对它进行分配担保,就需要采用标记-清理或标记-整理。

2.4 HotSpot的算法
2.4.1 枚举根节点

目前主流的JVM都使用的是准确式GC,当执行系统停顿下来以后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机有办法直接得知哪些地方存放着对象引用。在HotSpot实现中,是使用一组称为OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就能直接获得这些信息了。

2.4.2 安全点

实际上,HotSpot没有为每条指令都生成OopMap,只是在特定的位置记录了这些信息,这些位置称为安全点,即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待太长时间,也不能过于频繁增大运行时负荷。所以,安全点的选定基本上是以程序是否具有让其长时间运行的特征为标准进行选定的,因为每条语句的执行时间都非常短,不太可能因为指令流长度太长这个原因而过长时间运行。长时间执行最明显的特征就是指令序列复用,如方法调用、循环跳转、异常跳转等,具有这些功能的指令才会产生Safepoint。

2.4.3 安全区域

安全区域是指在一段代码之中,引用关系不会发生变化。在这个区域中的任何地方开始GC都是安全的,可以把它当做扩展了的Safepoint。

2.5 垃圾收集器


2.5.1 Serial收集器

Serial是最基本、发展历史最悠久的收集器,曾是新生代收集的唯一选择。这个收集器是一个单线程的收集器,不仅是它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在垃圾收集时,必须暂停其他所有的工作线程,直到他收集结束。这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常地工作线程都停掉,这对很多应用来说必然难以接受。

2.5.2 ParNew收集器

其实就是Serial的多线程版本。

2.5.3 Parallel Scavenge收集器

它是一个新手代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,这些和ParNew一样。但它的关注点和其他收集器不同,CMS等收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目的是达到一个可控制的可吞吐量。吞吐量是CPU运行用户代码的时间与CPU总耗时的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+理解回收时间)。

2.5.4 Serial Old收集器

它是Serial的老年代版本,也是一个单线程收集器,使用标记-整理算法。它的意义也是给Client模式下的虚拟机使用。

2.5.5 Parallel Old收集器

它是Parallel Scanvenge收集器的老年代版本,使用多线程和标记-整理算法。

2.5.6 CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法。有4个步骤:初始标记、并发标记、重新标记、并发清除。

2.5.7 G1收集器

它有如下特点:并行或者并发;分代收集;空间整合;可预测的停顿。

2.6 内存分配与回收策略

对象的内存分配,从大来讲,就是在堆上分配,对象主要分配在新生代的Eden上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也会直接分配在老年代中,分配的规则并非固定,其细节取决于使用哪一种垃圾收集器组合,还有虚拟机与内存相关的参数设置。

2.6.1 优先在Eden分配

大多数情况下,对象在新生代Eden分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

2.6.2 大对象直接进入老年代

大对象是指需要大量连续内存的对象,最典型的大对象就是很长的字符串或数组。大对象对虚拟机内存分配来说是不好的,经常出现大对象会导致虚拟机还有不少空间时就提前出发垃圾收集以获取足够的空间来安置他们。

2.6.3 长期存活的对象进入老年代

虚拟机给每个对象定义了一个age计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,它将被移动到Survivor空间,age设为1。对象在Survivor区中每经过一次Minor GC,age加1,当它的年龄增加到一定程度(默认15),将会到老年代中去。年龄阈值可设定。

2.6.4 动态对象年龄判断

为了能更好地适应不同程序的内存情况,虚拟机并不是永远要求对象年龄必须达到阈值才能晋升到老年代,如果Survivor空间内相同年龄的所有对象大小之和大于其空间的一半,年龄大于或等于该年龄的对象就会进入老年代,无须到达阈值。

2.6.5 空间分配担保

在发生Minor GC之前,虚拟机会检查老年代的最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可用确保安全。如果不是,虚拟机会查看HandlePromotionFailture设置是否允许担保失败。如果允许,那么会检查老年代最大可用连续空间是否大于历次晋升老年代的对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这是有风险的;如果小于,或者设置不允许,此时需要进行一次Full GC。


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值