标签(空格分隔): JVM
先讲讲自动内存回收——GC
GC的三大问题——What & When & How
- 哪些内存需要回收?
- 何时回收?
- 如何回收?
什么是内存溢出,内存泄露?
GC的三大问题(一)——哪些内存需要回收?
回收区域:Java堆和方法区(对象的创建是动态的,不确定性)(线程私有区域不考虑)
回收对象:“死亡的”对象实例
如何定义“死亡”(Java堆)
1. 引用计数法
引用计数器
简单,效率高,不可靠
无法解决“对象之间循环引用”
/*示例代码*/
public class ReferenceCountingGC {
public ReferenceCountingGC instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.GC();
}
}
2. 可达性分析
GC Roots + 引用链
被Java采用
在Java中,可作为GC Roots的对象有:
1.
2.
3.
4.
3. 引用类型:强软弱虚
Java中引用的定义:
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表中一个引用。
判断对象存活仅与引用有关,太过”狭隘“
“食之无味,弃之可惜”——缓存
Type | Purpose | GC |
---|---|---|
Strong | Object obj = new Object() | 只要强引用存在,就永远不会回收被引用的对象 |
Soft | 有用但非必需 | 发生内存溢出异常之前,将此类对象列进回收范围进行第二次回收 |
Weak | 非必需对象 | 下一次GC被回收 |
Phanton | 无实际用途 | 唯一的作用是在该对象被回收时,给出一个系统通知 |
如何定义“死亡”(方法区)
废弃常量
1. 常量池中的常量不被引用
无用的类
1. 该类的所有实例都已经被回收——Java堆中不存在该类的实例
2. 加载该类的ClassLoader已经被回收
3. 该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
GC的三大问题(二)——何时回收?
两个阶段:
1.第一次标记
2.第二次标记
第一次标记:
1. 可达性分析:若某一对象“不可达”,则标记
2. 筛选: 若对象被选中,则加入F-Queue队列
当对象未覆盖finalize()方法或者此方法已经被虚拟机调用过,则不被选中
第二次标记:
3. GC对F-Queue队列中的对象执行第二次小规模标记
对象在finalize()中实现“自我拯救”,退出F-Queue队列
4. 虚拟机自动建立”低优先级“的Finalizer线程执行回收动作——调用对象的finalize()方法
“自我拯救”????
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isLive() {
System.out.println("I am still alive!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
SAVE_HOOK = this;
}
public static void main(String[] args) {
SAVE_HOOK = new FinalizeEscapeGC();
/* 实现自我拯救 */
SAVE_HOOK = null;
System.GC();
Thread.sleep(500);
if (SAVE_HOOK != null) {
isLive();
} else {
System.out.println("I'm dead!");
}
/* finalize()只能执行一次 。不会被回收???? */
SAVE_HOOK = null;
System.GC();
Thread.sleep(500);
if (SAVE_HOOK != null) {
isLive();
} else {
System.out.println("I'm dead!");
}
}
}
finalize()
Forget it!
GC的三大问题(三)——如何回收?
GC算法
1. Mark-Sweep:效率低 + 内存碎片多
适合对象存活率较高的情况,适合回收老年代
2. Copying:把活着的对象复制到另一块内存空间,再清理已使用的那块;
适合对象存活率较低的情况,被主流商业虚拟机采用回收新生代。(“朝生夕死”)
Eden:Survivor:Survivor = 8:1:1,每次新生代可用内存空间占整个新生代容量的90%
老年代分配担保(Handle Promotion)
3. Mark-Compact:让存活的对象向内存的一端移动,直接清理掉端边界以外的内存;
适合对象存活率较高的情况,适合回收老年代
4. Generational Collection
“因地制宜”
Hotpot的GC算法实现
我们始终关注算法的执行效率!
枚举根节点
安全点
安全区域
垃圾收集器(了解)
GC日志(了解)
内存分配策略
1. 大多数情况下,对象在新生代的Eden区上分配。当Eden区上没有足够空间时,虚拟机发起一次 Minor GC(Copying)
2. 大对象直接进入老年代
需要大量连续内存空间的Java对象,例如很长的字符串和byte[4 * _1MB]
"朝生夕死"的“短命大对象”是JVM最不想看到的,写程序也应尽量避免
经常出现大对象导致内存即使仍有不少空间,也需要提前触发GC以获得足够多的连续空间
3. 长期存活的对象将进入老年代
对象年龄计数器。
如果对象在Eden区出生,经过第一次Minor GC后仍然存活,并且能被Survivor区容纳,将被移动到Survivor空间中,对象年龄变为1.
接下来,对象在Survivor区每“熬过”一次Minor GC,年龄加一,到达阈值(15),进入老年代
4. 动态对象年龄判定
如果Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象可以直接进入老年区,无需达到“阈值年龄”!
5. 空间分配担保