垃圾收集器与内存分配策略
一、GC概述
GC要做的三件事:
- 哪些内存需要回收
- 什么时候回收
- 如何回收
垃圾收集的是哪部分的内存: java堆
和方法区
。其他运行时区域的部分不需要过多考虑的原因是,他们的内存分配和回收都具备确定性。
二、哪些对象需要回收
在堆中,存放了大量的对象实例,有些对象不可能再被使用了,就已经死了,需要被回收。所以如何判断对象已经死了呢,就有下面几种方法。
1. 引用计数算法
怎么算的:给对象添加一个计数器, 当被引用时,计数器加1。当引用失效时,计数器减1。所以计数器为0 的对象,就是无用的垃圾。
但是,这种方式有个问题,就是当两个对象之间相互引用的话,两个对象的计数器都不为0,但这两个对象也不会再被访问时,就无法回收。所以,java也没有选择这种算法去管理内存。
2. 根搜索算法
算法方式:通过一系列的“GC Roots”对象为根节点,开始向下添加引用的对象节点, 子对象中如果再次引用了其他对象,则继续向下延伸。最终形成一条条分支,每一个从根节点到末端的分支被称为“引用链”。当一个对象没在任何引用链上时(从GC Roots到该对象不可达),就被判定为对象。
3. 重新认识引用
在JDK1.2以前,对象的引用只有两个状态–用或者没用。但是对于有些没多大用却又不太好丢弃的对象来说,
不太好定义它们的状态。所以在JDK1.2后,讲应用分为了如下四中类型:
- 虚引用: 最弱的引用,无法通过虚引用来取得一个对象实例。一个设置了虚引用的对象,在被回收时会收到系统通知
- 弱引用: 描述不是必须的对象,但还有点用。关联了弱引用的对象只能生存到下次垃圾回收之前
- 软引用: 不是必须的对象,还有一些用的,比弱引用更有用一点,在系统内存要溢出前,对该引用关联的对象进行回收
- 强引用: 类似于“Object object = new Object()”。此种对象不会被回收
4. 杀死对象的过程
每个对象中finalize()只会执行一次。
重新关联引用链: 比如把自己(this)赋值给某个变量,就关联上了某条引用链上。这样可以让对象自救
最后,不推荐使用finalize()自救。
5.回收方法区
永久代的垃圾回收:废弃常量和无用类
判断无用类:
- 该类的所有实例都已被回收
- 该类的ClassLoader已被回收
- 该类对用的java.lang.Class对象么有被引用。
三、垃圾收集算法
1. 标记 - 清除算法
算法: 先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。标记过程在上面已经提到过
缺点: 效率低。清除后会产生大量不连续的内存碎片,又需要额外的进行一次垃圾收集工作。
2. 复制算法
算法:将内存按容量分成相同大小的两块,每次只使用其中一块,当那块用完之后,就将还存活的对象复制到另一块上,然后再把那块上的已使用的内存空间清理掉。
缺点:内存缩小了一半。如果对象存活率很高,复制量会变大,效率也会变低
3. 标记 - 整理算法
算法: 在标记完之后,不是直接清理对象,而是让存活对象向某一端移动,然后直接清理端边界外的内存。
4. 分代收集算法
算法:根据对象的存活周期将内存划分为几块(一般把java堆分为新生代和老年代,再根据各个年代的特点采用适当的收集算法)。比如在新生代中,存活率低,就采用复制算法,在老年代中,存活率高,没有额外的空间,就使用“标记-清理”,“标记整理”
四、垃圾收集器
垃圾收集器是内存回收的具体实现。但在Java虚拟机中没有对它的实现做任何规定。所以不同的厂商和版本的实现方式差别很大。目前为止,没有万能的收集器,所以要根据具体的情况选择收集器。
1. Serial收集器
曾是新生代收集器唯一的选择,是一个单线程的收集器。在进行垃圾收集时,必须暂停其他所有工作线程。虽然这样导致的停顿让人很不满,但是也是有有点的:简单高效。适合Client模式下的虚拟机
2. ParNew收集器
就是Serial收集器的多线程版本。适合作为Server模式下的虚拟机的新生代收集器。
在垃圾回收中的并发和并行:
并发(Parallel): 多条垃圾收集线程并行工作,用户线程仍处于等待阶段
并行(Concurrent): 用户线程和垃圾收集线程同时执行(并行或交叉),用户程序继续进行,垃圾收集程序运行于另一个CPU上
3. Parallel Scavenge收集器
是一个新生代收集器, 使用复制算法和多线程收集器。这个收集器的目的是达到一个可控制的吞吐量。吞吐量高则可以最高效的利用CPU时间,尽快的完成程序的运算,主要适合后台运量且不需要太多交互的任务。
吞吐量: = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集时间)
该收集器提供两大参数去控制吞吐量:
-
-XX:MaxGCPauseMillis: 最大垃圾收集停顿时间
-
-XX:GCTimeRatio:(> 0 , < 100 ) 设置吞吐量大小, 相当于垃圾收集时间占总时间的比率。如果该值为x, 则比率=1/(1+x)。默认值为99,即为1%的比率。
4. Serial old 收集器
是Serial收集器的老年代版本。也是单线程,使用 标记-整理 算法。
5. Parallel Old 收集器
是Parallel Scavenge的老年代版本。使用多线程和 标记-整理 算法
6. CMS收集器
该收集器的目的是: 获取最短回收停顿时间。应用在大部分的互联网站或者B/S系统的服务端上。注重用户体验。
该算法是基于 标记-清除 算法实现的。以下是它的运作过程:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
7. G1收集器
基于“标记 - 整理”算法实现。可以精确控制停顿。可以在不牺牲吞吐量的情况下降低停顿的内存回收。
原理: G1将Java堆划分成了多个大小固定的区域了,并跟踪这些区域里的垃圾堆积程度,在后天维护一个优先列表。每次根据允许的收集时间,优先回收垃圾最多的区域。
8. 垃圾收集相关的常用参数
五、内存分配与回收策略
为什么存在内存分配: 因为对象要放在内存中,不可能随意乱放,要按照一定的规则分配内存。以下也主要简介几种分配方式。
内存分配:对象的内存的分配,主要分配在新生代的Eden区上。
如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配
少数情况下,会直接在老年代中分配。
1. 对象优先进入新生代
对象一般在新生代Eden区分配。如果没有空间分配了,虚拟机就发起一次Minor GC(指新生代GC,相反老年代GC就是Major GC/ Full GC)。
2. 大对象直接进入老年代
大对象: 指需要大量连续内存空间的对象。经常出现大对象,容易导致内存还有不少空间时就提前触发的垃圾收集以获取足够的连续空间来存放它们。
-XX:PretenureSizeThreshold:大于该参数值的对象,直接分配到老年代
3. 长期存活的对象进入老年代
虚拟机为每个对象定义了一个年龄计数器(默认0)。每经历一次Minor GC后年龄就+1.当到达指定年龄(默认15岁)时就进入老年代。
-XX:MaxTenuringThreshold: 设置进入老年代的指定年龄。
4. 动态判断对象年龄
怎么判断年龄: 虚拟机并不一定要在对象年龄达到指定年龄才进入老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就直接进入老年代。
4. 空间分配担保
在Minor GC发生时,如果之前每次晋升到老年代的平均大小大于老年代的剩余空间。则会直接进行Full GC。反之,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,就只进行Minor GC,不允许,就进行一次Full GC