文章目录
上一节内容回顾 :JVM(一)内存区域与内存溢出异常
1. 垃圾回收策略
回收 针对的是线程共享内存(堆,方法区)
判断对象是否存活?
a. 引用计数法(Python,C++)
- 给每个对象附加一个引用计数器,每当有引用指向当前对象时,计数器 +1;每当有引用不再指向当前对象时,计数器 -1
- 任意时刻,引用计数器值为0的对象,被标记为 “不在存活”
- 缺点 :无法解决循环引用问题(我中有你,你中有我)
b. 可达性分析算法(Java,C#)
核心思想:
- 通过一系列称为
GC Roots
对象开始向下搜索,若到指定对象有路可走(即"可达"),认为此对象存活; - 若从任意一个
GC Roots
对象到目标对象均不可达,认为目标对象已经不再存活.
在Java语言中,可作为GC Roots
的对象包含下面几种 :
-
虚拟机栈中的临时变量引用的对象
-
本地方法栈中JNI(Native方法)引用的对象
-
方法区中类静态变量引用的对象
-
方法区中常量引用的对象
2. 引用的扩充
JDK 1.2之后关于引用的扩充:强软弱虚,引用强度依次扩充
强引用 :
程序中普遍存在的,
GC Roots
对象所指向的引用都属于强引用
Person per = new Person();
只要当前对象被任意一个强引用指向,即便内存不够用也不能回收此对象
软引用 :
有用但不必须的对象(缓存对象)
-
当前内存够用时,不回收对象;
-
当前内存不够用时,回收对象.
JDK 1.2之后用SoftReference
来表示软引用
Person per = new Person(); //必须先创建强引用对象
SoftReference<Person> softReference = new SoftReference<>(per);
per = null;
System.gc;
弱引用 :
描述非必须对象,强度若于软引用
- 被弱引用指向的对象,只能存活到下次GC之前;
- 当GC开始时,不管内存是否够用,都会回收被弱引用指向的对象
JDK 1.2之后用WeakReference
来表示弱引用
Person per = new Person(); //必须先创建强引用对象
WeakReference<Person> weakReference = new WeakReference<>(per);
per = null;//该对象指向为空,可以被回收
System.gc;
虚引用 :
最弱的引用关系,完全对对象的生存周期不造成影响,并且无法通过虚引用取得对象
为对象设置虚引用的目的 :该对象在被GC回收之前由系统发回 回收通知
JDK 1.2之后用PhantomReference
来表示虚引用
3. 对象的自我拯救
当一个对象到GC Root
不可达时,并不是"当场去世",而是进行自我拯救
finalize()
对象的一次自我拯救机会
JVM在进行GC之前,需要判断回收对象所在的类 是否覆写了finalize()
?
- 若没覆写,此对象直接回收
- 若覆写了
finalize()
-
- 若
finalize()
未被JVM调用,JVM则会调用finalize()
;对象在此调用过程中与GC Roots
有路可达,此对象不再被回收(自救成功) - 若
finalize()
已被JVM调用,此对象被回收
- 若
package iqqcode.Study.GCRoots;
import java.util.concurrent.TimeUnit;
/**
* @Author: Mr.Q
* @Date: 2019-09-05 19:37
* @Description:finalize()对象的自我拯救
*
*/
public class FinalizeTest {
private static FinalizeTest test;
@Override
protected void finalize() throws Throwable {
System.out.println("finalize method execute!");
test = this; //test等于当前调用finalize()的对象
}
public static void main(String[] args) throws InterruptedException {
test = new FinalizeTest();
test = null; //此时test对象没有被强引用指向,无引用指向
//JVM检查 是否覆写且调用过finalize()
//对象在此调用过程中与`GC Roots`有路可走,此对象不再被回收
System.gc();
TimeUnit.SECONDS.sleep(1);
if(test == null) {
System.out.println("Now I'm dead...");
}else {
System.out.println("I'm Alive!");
}
test = null;//此时无引用指向,可以被垃圾回收
//JVM检查 覆写且调用过finalize()
//不再调用finalize(),判断对象死亡
//finalize() 对象的一次自我拯救机会
System.gc();
TimeUnit.SECONDS.sleep(1);
if(test == null) {
System.out.println("Now I'm dead...");
}else {
System.out.println("I'm Alive!");
}
//TODO
}
}
4. 方法区的回收
方法区回收(永久代回收)------废弃常量和无用类
方法区的GC频率非常低
5. 垃圾回收算法(堆上)
I. 标记清除算法
算法分为"标记"和"清除"两个阶段 :
- 首先标记(根据可达性分析算法来标记)出所有需要回收的对象
- 在标记完成后统一回收所有被标记的对象
标记-清除算法的不足主要有两个 :
- 效率问题 : 标记和清除这两个过程的效率都不高
- 空间问题 : 标记清除后会产生大量不连续的内存碎片,导致
gc
频繁发生
II. 复制算法(新生代回收算法)
堆(所有对象和数组对象)
- 新生代: 对象默认在此区域产生,大部分对象都在此区域,该区域的特点是==“朝生夕死”== (存活率低)
- 老年代: 存活率高
- "复制"算法是为了解决"标记-清理"的效率问题.
- 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象 复制 到另一块上面,然后再把已经使用过的内存区域一次清理掉
这样做虽然提高了回收效率,但是降低了内存的利用率(100%的内区域变成了50%)
新生代中98%的对象都是"朝生夕死"的,所以并不需要按照1 : 1的比例来划分内存空间
而是将内存(新生代内存)分为一块较大的 Eden(伊甸区) 空间和两块较小的 Survivor(幸存区) 空间,每次使用 Eden 和其中一块Survivor(Survivor区域一个称为From区,另一个称为To区域)
画图说明
HotSpot 实现的复制算法流程如下:
- 当
Eden
区满的时候,会触发第一次Minor gc
,把还活着的对象拷贝到Survivor From
区,然后清理掉Eden
区的所有空间. 当Eden区即将满时,再次触发Minor gc
,会扫描Eden
区和From
区域,对两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To
区域,并将Eden
和From
区域清空 - 当后续
Eden
又发生Minor gc
的时候,会对Eden
和To
区域进行垃圾回收. 存活的对象复制到From
区域,并将Eden
和To
区清空. 之后依次循环 - 部分对象会在
From
和To
区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代
III. 标记-整理算法(老年代回收算法)
核心思想相较于标记清除算法------整理阶段先让存活对象向一端移动,而后清理掉端边界以外的内存
Question : 为何老年代不采用复制算法?
Answer :新生代中的绝大多数对象都是朝生夕死,所以每次复制存活的对象较少;而老年代中大部分对象都存活,反复复制反而效率低下
IV. 分代收集策略(JavaGC)
将堆空间分为新生代(-Xmn)与老年代(堆的大小 - 新生代)空间,其中新生代采用复制算法,老年代采用标记整理算法
面试题 : 请问了解Minor GC和Full GC么,这两种GC有什么不一样吗?
Minor GC
又称为新生代 GC : 指的是发生在新生代的垃圾收集. 因为Java对象大多都具备朝生夕灭的特性,因此Minor GC
(采用复制算法)非常频繁,一般回收速度也比较快.Full GC
又称为 老年代 GC 或者Major GC
: 指发生在老年代的垃圾收集. 出现了Major GC
,经常会伴随至少一次的Minor GC
(并非绝对,在Parallel Scavenge
收集器中就有直接进行Full GC
的策略选择过程).Major GC
的速度一般会比Minor GC
慢10倍以上.