哪些内存需要回收?
什么时候回收?
如何回收?
一:哪些内存需要回收?
JAVA内存中不需要考虑内存回收问题的区域:
程序计数器、虚拟机栈,本地方法栈
(随线程生灭,栈帧分配多少内存在类结构确定是就已知,因此它们的内存分配与回收具备确定性。方法结束或线程结束时,内存自然就跟着回收了)
需要考虑内存回收问题的区域:
JAVA堆和方法区
(创建哪些对象,创建多少对象,需要在运行期间才知道。不再用创建对象的类定义要从方法区回收)
二:堆中对象的回收
死去的对象才会被回收
如何判断对象已死
a.引用计数算法:
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1。引用失效时,计数器值就减1.任何时候计数器为0的对象就是不可能再被使用的。
优点:引用计数算法实现简单,判定效率也高。(使用在COM,FlashPlayer,Python等语言中)
缺点:它很难解决对象之间相互循环引用的问题。(因此JAVA中没有选用它来管理内存)
b.根搜索算法
对于任何"GC Roots"都没有路径到达对象时,该对象就是不可用的。
主流的程序语言都用的是根搜索算法,包括JAVA语言。
哪些对象才是"GC Roots"
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI的引用的对象
JAVA中的引用分为:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,强度依次减弱
- 强引用:代码中存在,类似 "Object obj = new Object()"这类。只要强引用存在,GC就永远不会回收掉被引用的对象。
- 软引用:描述一些还有用,但非必需的对象。在系统将要发生内存溢出异常之前,将把这些对象列进回范围之中并进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。通过 SoftReference类来实现软引用。
- 弱引用:也用于描述非必需对象,但它的强度要比软引用更弱一些,被弱引用关联的对象只能生成到下一次GC发生之前。当GC工作时,无论当前内存是否足够,都会回收弱引用关联的对象。通过 WeakReference类来实现弱引用
- 虚引用:也称为幽灵引用或幻影引用。设置虚引用关联的唯一目录就是希望这个对象被回收时收到一个系统通知。通过PhantomReference来实现虚引用
究竟什么样的对象才会死掉呢?
要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行根搜索后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并放到“即将被回收的集合”中。
与此同时进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。(当对象没有覆盖 finalize()方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为 "没有必要执行"。)如果这个对象被判定为有必要执行 finalize()方法,那么这个对象将会被放置到一个名为 F-Queue 的队列之中,将在稍后由一条由虚拟机自动建立的,低优先级的 Finalizer 线程去执行(触发finalize()方法而不等待)。finalize()方法是对象逃脱死亡命运的最后一次机会。对象要在finalize()方法中拯救自己,只要重新与引用链上任何一个对象建立关联即可,这样的话第二次标记时它被移除出“即将回收的集合”。如果对象这个时候还没有逃脱,那就真的离死不远了。
三:方法区的回收
主要回收两部份内容:废弃常量、无用的类
常量回收与堆中对象回收类似。
无用的类的判断需同时满足3条:
- 该类所有的实例都已经被回收,也就是说堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机对满足上述3个条件的无用类进行回收。(只是可以,不会是必然回收)是否对类进行回收,HotSpot虚拟机了 -Xnoclassgc参数进行控制。还可以使用 -verbose:class 及-XX:+TraceClassLoading 和 -XX:+TraceClassUnLoading查看类的加载和卸载信息。
四:垃圾收集算法
4.1 标记-清除算法
标记出所有需要回收的对象,标记完成后统一回收掉所有被标记的对象。(如何标记在堆对象回收部分有介绍)
它是最基础的收集算法,后续收集算法都是基于这种思路改进而得到。
缺点:效率不高,产生大量不连续的内存碎片。
(空间碎片太多,分配较大对象时就无法找到足够连续内存空间,而不得不提前触发另一次垃圾收集动作)
4.2 复制算法
(为解决效率问题)
将内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:内存分配时不用考虑碎片的问题,实现简单,运行高效
缺点:内存缩小为原来的一半。
当对象存活率较高时,需要较多的复制操作,效率将会变低。若不想浪费50%的空间,需要额外的空间进行分配担保
现在的商业虚拟机都采用这种收集算法来回收新生代(IBM研究表明,新生代对象98%是朝夕死的)。不过,并不是按1:1的比例来划分内存空间。而是将内存分为一块较大的 Eden 空间 和两块较小的Survivor空间。每次只使用 Eden 和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性拷贝到另外一块Survivor空间上,最后清理掉刚才的Eden和Survivor。默认Eden和Survivor比例是 8:1。(98%是一般场景,另外一些场景下,当Survivor空间不够时,需依赖其它内存【老年代】进行分配担保)
4.3 标记-整理算法
复制算法的缺点,决定老年代不能使用复制算法
根据老年代特点,提出了“标记-整理算法”。
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
4.4 分代收集算法
当前虚拟机的垃圾收集都采用“分代收集”算法。
根据对象的存活周期将内存划分为几块。一般将堆分为新生代和老年代,这样根据各个年代的特点采用最适当的收集算法。
新生代采用复制算法
老年代采用“标记-清理”或“标记-整理”算法
五:垃圾收集器
算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。
Serial收集器
缺点:单线程收集器,垃圾收集时必须暂停其它所有的工作线程(Stop the world)。
优点:简单而高效,对于Client模式下的虚拟机来说是一个很好的选择。(也是Client模式下的默认新生代收集器)
ParNew 收集器
其实就是 Serial 收集器的多线程版本
是Server模式下的虚拟机的首选新生代收集器。(一个重要原因是,目前除了Serial收集器外,只有它能与CMS收集器配合工作)
Paraller Scavenge 收集器
并行的多线程收集器
它的关注点与其它收集器的不同,它关注的是吞吐量
吞吐量=运行用户代码时间/(运用用户代码时间+垃圾收集时间)
Serial Old 收集器
是Serial收集器的老年代版本,同样是单线程收集器。使用“标记-整理”算法
Parallel Old 收集器
是Parallel Scavenge收集器的老年代版本,使用多线和和“标记-整理算法”,JDK1.6才提供。
注重吞吐量及CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old组合。
CMS收集器 (Concurrent Mark Sweep)
是一种以获取最短回收停顿时间为目标的收集器。
对于B/S类重视服务器响应速度,希望停顿时间最短类应用就比较适合。
基于“标记-清除算法实现”
内存回收过程与用户线程起并发的执行。
缺点:
对CPU资源非常敏感。
无法处理浮动垃圾
收集结束时会产生大量空间碎片
G1收集器(Garbage First)`
JDK1.7正式发布时,很可能会有一个成熟的商业版本随之发布。
相对CMS的改进:
基于“标记-整理”算法实现,不会产生空间碎片
可以非常精确的控制停顿
可以实现基本不牺牲吞吐量的前提下完成低停顿的内存回收