一、对象死亡判断
在进行垃圾回收之前,要先对对象进行判断,只有已死亡的对象才可以被回收。判断对象是否死亡的方法有以下两种:
1.引用计数法
实现原理
在每个对象中添加一个引用计数器
,每当有一个地方引用该对象时,计数器的值就加一,当某个地方对该对象的引用失效时,计数器的值就减一。任何时刻,当计数器变为0时该对象就是不可能再被使用的。
引用计数法的实现较为简单,判定效率也比较高,但是却不是主流虚拟机管理内存的方式,这是因为它很难解决对象之间相互引用的问题。
2.可达性分析法
实现原理
通过一系列称为GC Roots
的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链
,当一个对象与GC Roots
没有任何引用链
相连时,说明该对象是不可用的。
主流商用语言中,都是通过此方法判断对象是否存活,从而实现内存管理的。
在java中,可以作为GC Roots
的对象包括以下几种:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈(Native方法)中引用的对象。
补充关于对象的四种引用(强度依次递减):
强引用:类似通过new获得的对象,只要强引用存在,垃圾收集器永远不会回收该对象。
软引用:用来描述有用但非必需的对象。当内存不够时,会对此类对象进行回收。
弱引用:用来描述非必需的对象。当垃圾收集器工作时,无论内存是否足够,都会对其进行回收。
虚引用:为一个对象设置虚引用关联的唯一目的是能在这个对象被垃圾收集器回收时收到一个 通知。
3.非死不可吗
尽管经过分析后对象不可达,但是该对象并不算真正死亡。一个对象的真正死亡要经过两次标记:
- 经过可达性分析后没有与
GC Roots
相连,它会被第一次标记并进行筛选,如果该对象没有覆盖finalize方法或者该方法已经执行过,那么该对象会被判定为死亡,否则对其进行第二次标记; - 如果该对象有必要执行finalize方法,只要它在执行过程中与
引用链
上任何对象关联,那么它就仍然可以存活。
二、垃圾收集算法
1.标记-清除算法
该算法分为两个阶段:标记和清除。首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
该算法会产生两个问题:
- 标记和清除的过程效率都不高;
- 空间上会产生大量不连续内存碎片,碎片太多会导致大对象无法分配,从而提前触发另一次GC。
2.标记-复制算法(新生代)
该算法将可用内存划分为相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块,然后把使用过的内存空间全部清理,这样就避免了内存碎片的产生。现在大多都使用这种算法回收新生代。
不过经过研究表明,新生代中98%对象都是“朝生夕死”的,所以并不需要按照1:1来划分内存空间,而是将内存划分为一块较大的Eden和两块较小的Survivor空间,每次使用Eden和一块Survicor。当回收时,将Eden和Survivor中存活的对象复制到另一块Survivor空间上,然后清理使用过的空间。在HotSpot中,Eden和Survivor的大小比例为8:1。
对于2%的情况,也就是Survivor空间不够用时,需要依靠老年代进行分配担保。
分配担保
可理解为向银行贷款。当另一块Survivor空间没有足够空间存放上一次新生代收集存活下来的对象时,这些对象将通过分配担保机制直接进入老年代。老年代会通过分配担保机制进行判断,如果空间仍然不够的话,将会发起一次Full GC。
3.标记-整理算法(老年代)
复制收集算法在对象存活率较高时需要进行较多的复制操作,效率会降低,所以并不适合老年代。对于老年代存活时间长的对象,往往采用标记整理算法。
标记过程和”标记-清除“一样,但是后续不是直接清理,而是让所有存活的对象向一端移动,然后清理掉边界以外的内存。
4.分代收集算法
该算法只是一种收集思想,是按照对象的存活周期将堆内存分为新生代和老年代,然后根据每个年代不同的特点采用不同的收集算法。新生代中的对象存活时间短,老年代中对象的存活时间长。
三、垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
下面分别说说图中的7种垃圾收集器。
1.Serial收集器
单线程收集器,简单高效,但是在垃圾收集时会暂停其他所有工作线程(“stop the world”),这是我们难以接受的。
2.ParNew收集器
Serial的多线程版本,采用标记-复制算法,可以让垃圾收集线程与用户线程基本同时工作。
3.Parallel Scavenge收集器
类似ParNew,不过更关注于可控制的吞吐量
而不是停顿时间。高吞吐量可以高效利用CPU使劲按,主要适合在后台运算而不需要太多交互的任务。
4.Serial Old收集器
Serial的老年代版本,采用标记-整理算法。主要有两个用途:一是与Parallel Scavenge搭配使用,另一个是作为CMS的后备方案。
5.Parallel Old收集器
Parallel Scavenge的老年代版本,多线程+标记-整理算法。
6.CMS收集器
CMS是以获取最短停顿时间为目标的收集器,一般用于B/S的服务器端,采用标记-清除算法。CMS可以并发收集,且做到低停顿。但是由于是并发设计的,所以对CPU资源敏感,且无法处理浮动垃圾(因为用户线程还在运行而产生的垃圾)。由于标记-清除算法的原因,可能会产生空间碎片,虽然可以通过参数进行控制,不过也会加长停顿时间。
7.G1收集器
收集器发展最前沿成果之一,具有以下特点:
- 缩短停顿时间,并发收集
- 分代收集
- 空间整合,整体使用标记-整理,局部使用复制
- 停顿可预测
G1将整个java堆化整为零,分成了一个个小的region,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了。G1进行垃圾回收时,会优先回收价值最大的region,这也是G1能够实现停顿可预测
的原因之一。
四、内存分配与回收策略
1.对象优先在Eden分配
大多数情况下,对象在新生代Eden中分配,当Eden中没有足够空间进行分配时,虚拟机将发起一次Minor GC
。
Minor GC和Major GC/Full GC
新生代GC(Minor GC):发生在新生代的GC,非常频繁,回收速度快。
老年代GC(Full GC):发生在老年代的GC,大多数情况下,出现Full GC会伴随至少一次的 Minor GC,Full GC速度一般比Minor GC慢10倍以上。
内存分配导致的并发问题:
- CAS+失败重试:虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块私有缓存区域,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
2.大对象直接进入老年代
需要大量连续内存空间的对象即大对象,比如长字符串以及数组。虚拟机提供了一个参数,大于该参数的的对象将直接在老年代分配。
3.长期存活对象进入老年代
为了区别哪些对象在新生代,哪些对象在老年代,虚拟机给每个对象定义了一个年龄计数器。如果对象在Eden出生并且经过一次Minor GC后仍然存活,且能被Survior容纳的话,就会被移动到Survivor空间中,然后对象年龄设为1。以后对象在survivor区中每熬过一次Minor GC,年龄就增加一岁,当年龄到达15岁(默认值,可以通过参数设置),就会晋升到老年代中。