概述
垃圾回收是JAVA很重要的特性,当内存溢出,内存泄漏时,了解垃圾回收可以有效地排查问题。
什么时候垃圾回收
在JAVA中,分为minor GC和full GC,其实垃圾回收和内存分配关系紧密,简单点说,当内存分配不够时,垃圾回收就会触发,由于内存中可能存在碎片,所以,有可能会出现总体内存是够的,但是存放大对象就不够了,这种情况也属于内存分配不够。
内存分配
垃圾回收的触发时机与内存分配策略密切相关,下面来聊一聊内存分配吧,这些规则不一定完全准确,不同的收集器可能不一样。
大部分的收集器都采用分代收集的策略,将内存区域区分为年轻代、年长代。
- 年轻代:这里面的对象大都很短命,存活时间很短,对象产生到没有任何引用十分频繁,所以可以采用复制算法,将存活的对象复制出来,这样就避免了大量的内存拷贝,Minor GC回收这部分区域。
- 年长代:这里面的对象一般不容易被垃圾回收,对象存活时间长,一般采用标记-整理或者标记-清除算法,一些大对象也有可能被存放在老年代。
下面的规则只是几条普遍的规则: - 对象优先在Eden区分配,当Eden没有足够的空间时,虚拟机启动Minor GC,这边的话,一般采用复制算法,将Eden和Survivor1中的存活对象拷贝到Survivor2中,如果某些时候,非常多的对象存活下来,这是Survivor2里面存放不了,这时这部分对象就存到老年代。
- 大对象直接进入老年代,如果一群短命大对象,就非常糟糕了。
- 长期存活的对象进入老年代,这个就是晋升,利用一个对象年龄计数器,每次熬过一个Minor GC,年龄就加1,超过阈值就晋升,大概就是这个意思。
- 分配空间担保,这个东西是用来延长Full GC的,主要是因为Full GC速度太慢了,当年轻代放不下时,触发Minor GC,只有当拖得不能再拖是才会触发Full GC。jdk6 update24之后,规则变为只要老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full GC。
回收什么
这一部分的算法基本和csapp中提到的差不多。
- 引用计数
给每个对象添加一个引用计数器,当有地方引用它时,计数器的值就加1,一旦引用失效,就减1。引用为0时,表示对象不再被使用。当时堆里面的数据互相引用时,无法回收,造成泄漏。 - 可达性分析
从GC Roots开始搜索,如果GC Roots到这个对象不可达,那么这个对象就是不可用的,通俗一点,你没有办法再引用这个对象了,那么这个对象自然就应该被垃圾回收了。在java中,GC Roots包含以下这些:- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈JNI中引用的对象。
还有一个小的注意点:
在可达性算法中不可达的对象,还是有救的,宣告一个对象的真正死亡,要经历至少两次标记过程,第1次是可达性检测后标记,观察对象是否需要执行finalize()方法,如果对象没有覆盖该方法,或者该方法已经被执行过,就是没必要执行,如果需要执行finalize()方法,那么执行过程中,如果绑定到了引用,就不会被回收。
- 方法区的回收
主要是回收废弃的常量和无用的类,一般回收不了啥。
垃圾收集算法
算法实现很复杂,这里只是几种算法的思想。
- 标记-清除(mark-swap)
书上写了很多,其实就是标记,然后标记这部分下次就可以被分配给对象了,容易产生很多碎片 - 复制 (Copying)
就是把可用的内存拷贝到另一块区间内,目前采用这种算法来回收新生代,新生代里面的大量对象都是垃圾。一般是将内存分为1个Eden和2个Survivor区域,每次使用1个Eden和1个Survivor,然后将这两个区域中存活的对象复制到另一个Survivor中,Eden和Survivor的比例是8:1。 - 标记-整理
在标记的基础上,做一下紧凑,清理掉碎片,一般在老年代使用,因为老年代中可以被回收的对象很少。 - 分代收集
目前的虚拟机大部分都这样做,分成老年代、年轻代。
网友评论