最早人们思考GC需要完成的3件事情:
经过发展,内存动态分配和回收技术已经成熟,为什么还要了解GC和内存分配呢? 当需要排查各种内存溢出,内存泄露问题时,当垃圾手机成为系统达到更高并发量的瓶颈时,就需要人为对其进行监控和调节.
前面提到的程序计数器
,虚拟机栈
,本地方法栈
3个区域随线程生和灭,每个栈帧分配内存基本上是在类结构确定后就已知的.因此方法结束或者线程结束时,内存自然就跟着回收了.
而Java堆
和方法区
则不一样:
我们只有在程序运行期间才知道会创建哪些对象,这部分内存分配和回收都是动态的,垃圾收集器所关注的就是这部分内存.
堆
中存放着Java中几乎所有的对象实例,垃圾收集器对堆
回收前,第一件事情是要确定这些对象哪些还活着.
给对象一个引用计数器,对它引用时,计数器加1;引用失效,计数器减1.任何时刻,计数为0的对象是不可能再被使用的.
引用计数法
实现简单,判定效率高,但很难解决对象之间循环引用问题.所以JVM没有使用这个方法.
基本思路是,通过一系列称为GC Roots
的对象作为起始点,从节点向下搜索.搜索走过的路径称为引用链 Reference Chain
,当一个对象到GC Roots
没有任何引用链相连时,证明此对象不可用.
下图白色对象就是可回收的对象.
可作为GC Roots的对象包括:
JDK1.2以后,对引用概念进行扩充,将引用分为:
A obj = new A()
这类普遍存在的引用.只要强引用
还在,垃圾收集器永远不会回收掉被引用的对象.
SoftReference
类实现.表示一些有用但并非必需的对象.
对于软引用
关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围中进行二次回收.如果这次回收还没有足够内存,抛出OOM.
WeakReference
类实现.表示非必需的对象,但强度比软引用
弱.
被弱引用
关联的对象只能生存到下一次垃圾收集发生之前.收集器一旦工作,就会回收掉只被弱引用
关联的对象.无关内存情况.
PhantomReference
类实现虚引用
.它是最弱的引用关系.
一个对象是否有虚引用
不对其生存时间产生影响,无法通过虚引用取得一个对象实例.
其存在的唯一目的,是对象被回收时收到一个系统通知.
在可达性分析算法
中找到的可回收的对象,会被第一次标记并进行一次筛选.条件是此对象是否有必要执行finalize()
方法.
当对象没有覆盖finalize()
方法,或者finalize()
方法已经被虚拟机调用过,虚拟机将认为没有必要触发该方法.(finalize()
方法最多只被自动调用一次)
如果这个对象被判定为有必要执行finalize()
方法,对象就被放入F-Queue
队列,等待Finalizer
线程执行.
finalize()
方法中GC将对F-Queue
中对象进行第二次标记,这个阶段对象只要重新与引用链上任何一个对象建立关联即可不被标记,从而”活”下来.
下面用代码演示finalize()
过程:
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("I am alive!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//first time save itself
SAVE_HOOK = null;
System.gc();
// finalize priority is low, so wait it
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("I am dead ...");
}
//second time. same code but save failed
SAVE_HOOK = null;
System.gc();
// finalize priority is low, so wait it
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("I am dead ...");
}
}
}
运行结果:
finalize method executed!
I am alive!
I am dead ...
代码能看到,finalize()
方法有被触发过.至于第二次自救失败,是因为任何对象的finalize()
方法只被系统自动调用一次.对象面临下一次回收,此方法不会被再次执行.
finalize()
方法不建议使用,因为运行代价高,不确定性强,无法保证各对象的调用顺序.
之前提到过,方法区
可以不实现垃圾回收,而且这里的回收”性价比”非常低(对比堆
).
方法区
回收主要是两部分内容:
回收常量和回收Java堆
类似,没有对这个常量有引用的情况就可以回收.
回收无用的类就要判断以下:
仅做原理介绍.
先标记所有需要回收的对象,然后统一回收.
不足:
将内存按容量分为相等的两块A和B.每次只使用一块,比如A,这一块内存用完了,就对A进行回收,把存活的对象复制到B上,然后把A一次清理掉.
这样没有内存碎片,按顺序移动堆指针,很高效.
不足:
标记还是和之前一样,但清除前,先将存活对象移到同一端,然后清理掉边界外的内存.
根据对象存活周期将内存分为几块,根据特点选算法.一般分为新生代,老年代:
复制算法
标记清理
或标记整理
从可达性分析入手,我们首先需要找到GC Roots
.这个GC Roots
主要存在于全局性引用与执行上下文中.但现在很多应用在方法区都有数百兆,直接检查很耗时.
另外,这项工作在分析期间,系统需要暂停,即分析时保证状态不会变化.
在系统暂停期间,虚拟机从OopMap
直接获得对象引用,不需要一个不漏地检查完所有执行上下文和全局的引用位置.
能引起OopMap
内容变化的指令很多,但HotSpot并没有为每条指令都生成OopMap
,只是在特定位置记录这些信息,这个位置称为安全点Safepoint
.
安全点
不能太多也不能太少,权衡标准就是:是否具有让程序长时间执行的特征.所以长指令流的指令才会产生安全点
.
安全点
保证程序执行时,可以进入GC的安全点
,但是程序不执行的时候,即线程处于Sleep或者Blocked时,线程无法响应JVM中断请求.这就需要安全区Safe Region
解决.
线程进入安全区
中,就会标识自己,JVM发起GC就会忽略有标记的线程.
线程离开安全区
,先检查系统是否完成根节点枚举或整个GC,如果完成,线程就继续执行,否则等待直到收到可以离开的信号.
垃圾收集器的实现没有统一的规定,所以有很多种不同实现.
这里仅列举常见的收集器.
33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: OK->210K(10240K), 0.0149142 secs] 4603K->210K(19456K), [Perm : 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
System.gc()
,则log显示[Full GC(System)
Default New Generation
,所以显示[DefNew
对象的内存分配,往大方面讲,是在堆上面分配.对象主要分配在新生代的Eden区上.
普遍的分配规则: