JVM第三节:垃圾回收算法与垃圾回收器

学习垃圾回收的意义

Java 与 C++等语言最大的技术区别:自动化的垃圾回收机制(GC)

为什么要了解 GC 和内存分配策略

1、面试需要
2、GC 对应用的性能是有影响的;
3、写代码有好处

栈:栈中的生命周期是跟随线程,所以一般不需要关注

堆:堆中的对象是垃圾回收的重点

方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点

判断对象的存活

引用计数法

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。(Python 在用,但主流虚拟机没有使用)

优点:快,方便,实现简单。

缺陷:对象相互引用时(A.instance=B 同时 B.instance=A),很难判断对象是否该回收。

可达性分析(Java 中使用)

(面试时重要的知识点,牢记)

来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。作为 GC Roots 的对象包括下面几种:
当前虚拟机栈中局部变量表中的引用的对象
当前本地方法栈中局部变量表中的引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象

请忘记 finalize

finalize 可以完成对象的拯救,但是 JVM 不保证一定能执行,所以请忘记这个“坑”。

各种引用(Reference)

传统定义:Reference 中存储的数据代表的是另一块内存的起始地址。

强引用

一般的 Object obj = new Object() ,就属于强引用。

(如果有 GCroots 的强引用)垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止

软引用 SoftReference

垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它

软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。

一些有用但是并非必需,用软引用关联的对象,系统将要发生 OOM 之前,这些对象就会被回收。参见代码:

VM 参数 -Xms10m -Xmx10m -XX:+PrintGC

public static void main(String[] args) {
		User u = new User(1,"King"); //new是强引用
		SoftReference<User> userSoft = new SoftReference<User>(u);
		u = null;//干掉强引用,确保这个实例只有userSoft的软引用
		System.out.println(userSoft.get()); //看一下这个对象是否还在
		System.gc();//进行一次GC垃圾回收  千万不要写在业务代码中。
		System.out.println("After gc");
		System.out.println(userSoft.get());
		//往堆中填充数据,导致OOM
		List<byte[]> list = new LinkedList<>();
		try {
			for(int i=0;i<100;i++) {
				System.out.println("*************"+userSoft.get());
				list.add(new byte[1024*1024*1]); //1M的对象
			}
		} catch (Throwable e) {
			//抛出了OOM异常时打印软引用对象
			System.out.println("Exception*************"+userSoft.get());
		}

	}

运行结果

User [id=1, name=King]
[Full GC (System.gc())  2011K->794K(9920K), 0.0017989 secs]
After gc
User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]
*************User [id=1, name=King]
[GC (Allocation Failure)  2923K->2847K(9920K), 0.0011817 secs]
*************User [id=1, name=King]
*************User [id=1, name=King]
[GC (Allocation Failure)  4967K->4897K(9920K), 0.0009077 secs]
*************User [id=1, name=King]
*************User [id=1, name=King]
[Full GC (Allocation Failure)  7011K->6946K(9920K), 0.0016093 secs]
*************User [id=1, name=King]
[Full GC (Allocation Failure)  8086K->7972K(9920K), 0.0009392 secs]
[Full GC (Allocation Failure)  7972K->7858K(9920K), 0.0017270 secs]
*************null
[Full GC (Allocation Failure)  8931K->8883K(9920K), 0.0009942 secs]
[Full GC (Allocation Failure)  8883K->8883K(9920K), 0.0008181 secs]
Exception*************null

Process finished with exit code 0

例如,一个程序用来处理用户提供的图片。如果将所有图片读入内存,这样虽然可以很快的打开图片,但内存空间使用巨大,一些使用较少的图片浪费内存空间,需要手动从内存中移除。如果每次打开图片都从磁盘文件中读取到内存再显示出来,虽然内存占用较少,但一些经常使用的图片每次打开都要访问磁盘,代价巨大。这个时候就可以用软引用构建缓存。

弱引用 WeakReference

垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。

一些有用(程度比软引用更低)但是并非必需,用弱引用关联的对象,只能生存到下一次垃圾回收之前,GC 发生时,不管内存够不够,都会被回收。

参看代码:

public static void main(String[] args) {
		User u = new User(1,"King");
		WeakReference<User> userWeak = new WeakReference<User>(u);
		u = null;//干掉强引用,确保这个实例只有userWeak的弱引用
		System.out.println(userWeak.get());
		System.gc();//进行一次GC垃圾回收
		System.out.println("After gc");
		System.out.println(userWeak.get());
	}
User [id=1, name=King]
After gc
null

注意:软引用 SoftReference 和弱引用 WeakReference,可以用在内存资源紧张的情况下以及创建不是很重要的数据缓存。当系统内存不足的时候,缓存中的内容是可以被释放的。

实际运用(WeakHashMap、ThreadLocal)

虚引用 PhantomReference

幽灵引用,最弱,被垃圾回收的时候收到一个通知

如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动

GC(Garbage Collection)

案例 Oom 类

-Xms 堆区内存初始内存分配的大小

-Xmx 堆区内存可被分配的最大上限

-XX:+PrintGCDetails

打印 GC 详情

-XX:+HeapDumpOnOutOfMemoryError

当堆内存空间溢出时输出堆的内存快照

新生代大小配置参数的优先级:

中间 -Xmn 限定大小

-XX:SurvivorRatio

2 个 Survivor 区和 Eden 区的比值

8表示 两个 Survivor : Eden = 2: 8 ,每个 Survivor 占 1/10

可以修改为 2

8 表示 两个 Survivor : Eden = 2: 2 ,各占一半

GC overhead limit exceeded 超过 98%的时间用来做 GC 并且回收了不到 2%的堆内存时会抛出此异常

1.垃圾回收会占据资源

2.回收效率过低也会有限制

为什么 new 出的对象不会被回收了,我们来看看 GC 是如何判断对象的存活

Minor GC

特点: 发生在新生代上,发生的较频繁,执行速度较快

触发条件: Eden 区空间不足\空间分配担保

Full GC

特点: 主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢

触发条件:

调用 System.gc()

老年代区域空间不足

空间分配担保失败

JDK 1.7 及以前的永久代(方法区)空间不足

CMS GC 处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发 Full GC

垃圾回收算法

复制算法(Copying)

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使

用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,

实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。

注意:内存移动是必须实打实的移动(复制),不能使用指针玩。

专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以一般来说回收占据 10%的空间够用了,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存会被“浪费”。

标记-清除算法(Mark-Sweep)

过程:

1.首先标记所有需要回收的对象

2.统一回收被标记的对象缺点:

1.效率问题,标记和清除效率都不高

2.标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

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

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

垃圾回收器

分代收集

根据各个年代的特点选取不同的垃圾收集算法

新生代使用复制算法

老年代使用标记-整理或者标记-清除算法

jps -v 显示当前使用的垃圾回收器

在这里插入图片描述

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

请记住下图的垃圾收集器和之间的连线关系。

在这里插入图片描述

并行:垃圾收集的多线程的同时进行。

并发:垃圾收集的多线程和应用的多线程同时进行。

注:吞吐量=运行用户代码时间/(运行用户代码时间+ 垃圾收集时间)

垃圾收集时间= 垃圾回收频率 * 单次垃圾回收时间

**

各种垃圾回收器

**

Serial/Serial Old

最古老的,单线程,独占式,成熟,适合单 CPU 服务器

-XX:+UseSerialGC 新生代和老年代都用串行收集器

-XX:+UseParNewGC 新生代使用 ParNew,老年代使用 Serial Old

-XX:+UseParallelGC 新生代使用 ParallerGC,老年代使用 Serial Old

ParNew

和Serial 基本没区别,唯一的区别:多线程,多 CPU 的,停顿时间比 Serial 少 -XX:+UseParNewGC 新生代使用 ParNew,老年代使用 Serial Old

除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。

Parallel Scavenge(ParallerGC)/Parallel Old

关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。

Concurrent Mark Sweep (CMS)

收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。

-XX:+UseConcMarkSweepGC ,一般新生代使用 ParNew,老年代的用 CMS

从名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,

垃圾回收过程

整个过程分为 4 个步骤,包括:

初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW -Stop the world)。

并发标记:从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。

重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

并发清除:不需要停顿。

在这里插入图片描述

优点:

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

缺点:

CPU 资源敏感:因为并发阶段多线程占据 CPU 资源,如果 CPU 资源不足,效率会明显降低。

浮动垃圾:由于 CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为“浮动垃圾”。

由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。在 1.6 的版本中老年代空间使用率阈值(92%)

如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。

会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片

G1 垃圾回收器

G1 中重要的参数:

-XX:+UseG1GC 使用 G1 垃圾回收器

内部布局改变

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

算法:标记—整理 (humongous) 和复制回收算法(survivor)。

在这里插入图片描述

GC 模式
Young GC

选定所有年轻代里的 Region。通过控制年轻代的 region 个数,即年轻代内存大小,来控制 young GC 的时间开销。(复制回收算法)

Mixed GC

选定所有年轻代里的 Region,外加根据 global concurrent marking 统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内尽可能选择收益高的老年代 Region。

Mixed GC 不是 full GC,它只能回收部分老年代的 Region。如果 mixed GC 实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行 Mixed GC,就会使用 serial old GC(full GC)来收集整个 GC heap。所以我们可以知道,G1 是不提供 full GC 的。

全局并发标记(global concurrent marking)

初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的 Region 中创建对象,此阶段需要停顿线程(STW),但耗时很短。

并发标记:从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。

最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。

筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

在这里插入图片描述

特点

空间整合:不会产生内存碎片

算法:标记—整理 (humongous) 和复制回收算法(survivor)。

可预测的停顿:

G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region (这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。

G1 把内存“化整为零”的思路,理解起来似

G1 GC 主要的参数

参数 含义

-XX:G1HeapRegionSize=n 设置 Region 大小,并非最终值

-XX:MaxGCPauseMillis 设置 G1 收集过程目标时间,默认值 200ms,不是硬性条件

-XX:G1NewSizePercent 新生代最小值,默认值 5%

-XX:G1MaxNewSizePercent 新生代最大值,默认值 60%

-XX:ParallelGCThreads STW 期间,并行 GC 线程数

-XX:ConcGCThreads=n 并发标记阶段,并行执行的线程数

-XX:InitiatingHeapOccupancyPercent 设置触发标记周期的 Java 堆占用率阈值。默认值是 45%。这里的 java 堆占比指的是
non_young_capacity_bytes,包括 old+humongous

垃圾回收器的重要参数(使用-XX:)

参数 描述

UseSerialGC 虚拟机运行在 Client 模式下的默认值,打开此开关后,使用 Serial+Serial Old 的收集器组合进行内存回收
UseParNewGC 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用 ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为 CMS 收集
器出现 Concurrent Mode Failure 失败后的后备收集器使用
UseParallelGC 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集
器组合进行内存回收

UseParallelOldGC 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行内存回收
SurvivorRatio 新生代中 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加 1,当超过这个参数值时就进入
老年代

UseAdaptiveSizePolicy 动态调整 Java 堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活
的极端情况

ParallelGCThreads 设置并行 GC 时进行内存回收的线程数
GCTimeRatio GC 时间占总时间的比率,默认值为 99,即允许 1% 的 GC 时间,仅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 设置 GC 的最大停顿时间,仅在使用 Parallel Scavenge 收集器时生效
CMSInitiatingOccupancyFraction 设置 CMS 收集器在老年代空间被使用多少后触发垃圾收集,默认值为 68%,仅在使用 CMS 收集器时生效
UseCMSCompactAtFullCollection 设置 CMS 收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用 CMS 收集器时生效
CMSFullGCsBeforeCompaction 设置 CMS 收集器在进行若干次垃圾收集后再启动一次内存碎片整理,仅在使用 CMS 收集器时生效

Stop The World 现象

GC 收集器和我们 GC 调优的目标就是尽可能的减少 STW 的时间和次数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值