Java内存回收
参考
- GC算法 垃圾收集器: http://www.cnblogs.com/ityouknow/p/5614961.html
- http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html
- 每隔一段时间,hbase 的读就会停顿10s的原因及解决办法
- G1 垃圾收集器入门: https://www.cnblogs.com/nashiyue/p/5797713.html
Overview:
- 程序计数器,虚拟机栈和本地方法栈中的内存分配在编译期可知,所这几个区域的内存分配和回收具有确定性,不必过多考虑。
- java堆和方法区中的内存回收是动态的(对象在运行时生成),所讨论的内存回收主要针对这两块。
- 方法区的回收主要回收废弃常量和无用的类。
- 引用计数器法无法解决循环引用的问题,使用可达性分析算法
- 引用类型: 强度依次减弱
- 强引用: 不会被回收
- 软引用: 有用但并非必须的对象,系统内存不足时才会进行回收
- 弱引用: 只能生存到下次垃圾回收前
- 虚引用: 一个对象设虚引用的唯一目的是能在这个对象被回收时收到一个系统通知.
可达性分析算法(图的dfs):
- 一个对象需要被标记2次才会被回收。第一次是在判定不可达时。第二次是调用finalize()方法没有复活后。
- 任何一个对象的finalize()方法只会被系统自动调用一次。
- 可以作为GCroot的对象主要在全局性的引用和执行上下文(如栈中的本地变量表)中;为了避免每次回收迭代所有gcroot对象,使用OopMap对gcroot对象位置进行记录.
- 为了在垃圾回收时保持一致性,不一致是指即收集的时候对象的引用还在不停变化。所有垃圾收集时需要短暂暂停所有线程的执行。
- 程序执行的时并非在所有地方都停顿下来进行GC,只有在到达安全点时才能暂停。
- 安全点保存了对象的位置和作用范围。换句话说,到了安全点才能找到对象,对其进行收集。所以收集的时候参与收集的线程需要跑到安全点才行。
- 只有如方法调用,循环跳转,异常跳转等具有让程序长时间执行的指令才会被选为安全点
- 主动式中断设置标志,各个线程执行时主动轮询这个标志(检查代码插入指令序列中,和安全点重合)
- 安全区域是指在一段代码片段之中,引用关系不会发生变化,在这个区域任一地方开始gc都是安全的
复制算法:(标记清除算法)
- 将内存分为多块,对一块进行回收时,将存活对象复制到另一块上。这样使得可用内存空间减小了,但是消灭了内存回收可能产生的碎片。
- 标记整理算法:将存活对象整为连续空间,然后直接清除剩余空间即可。
分代收集算法:
- 根据对象的存活周期内存划分为几块,采用相适应的算法收集。java堆分为老年代(标记整理)和新生代(复制算法)。
- 由于新生代对象存活时间短,收集频繁。用复制算法,因为存活对象少,复制存活对象到survivor区域开销小。
- 反之,老年代对象存活时间长,存活对象多,收集不频繁。所以用标记整理,这样不需要为survivor额外分配空间,提高内存利用率。
垃圾收集器
- Serial收集器,它会使用一个CPU或一条收集线程去完成垃圾收集的工作,而且在它进行垃圾收集时,必须暂停其他所有的工作线程。
- ParNew收集器:Serial收集器的多线程版本,关注尽可能缩短垃圾回收时用户线程的停顿时间.
- Parallel Scavenge收集器:它的目标是达到一个可控制的吞吐量,所谓吞吐量就是cpu用于用户代码的时间与cpu总消时间的比值.可以高效率的利用cpu时间,尽快完成程序的运算任务,适合无需太多交互的后台任务.
- CMS(Concurrent Mark Sweep)收集器:
- 以获取最短回收停顿时间为目标。(因为可以并发,所以能减少停顿)
- 基于标记-清除。
- 并发,用户线程和垃圾收集线程可以同时运行。
- 清除过程:
- 初始标记(仅标记GCroot可以直关联到的对象),需要Stop the world
- 并发标记(进行tracing)
- 重新标记(修正并标记期间因用户程序继续运作而导致标产生变动的那一部分对象的标记记录)。需要Stop the world
- 并发清除。
- 缺点:
- 对cpu资源敏感。和用线程竞争cpu资源。
- 无法处理浮动垃圾。需要预留空间给同时执行的用户线程。所以不能等到老年代几乎填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
- 由于基于标记-清除,会产生大量的内存碎片。需要整理。
- G1收集器: 面向服务端应用的垃圾收集器
- 优势:
- 并行与并发;
- 分代收集。
- 空间整合,基于标记-整理。不会产生内存碎片。
- 可预测的停顿(因为可以有计划(可以手动设置)的避免在整个Java堆中进行全区域的垃圾收集)
- 思路:将内存区域划分为多个大小相等的独立区域。根据这些区域的回收价值每次选取一个区域进行回收。而不是对整个堆进行垃圾回收。
- 如果划分的内存区域中对象引用了其他区域中的对象,用一个Set记录一下,避免全堆扫描
- 步骤:
- 初始标记(标记GCroot可直接关联的对象,stop the world)
- 并发标记:可达性分析
- 最终标记:修正并发标记阶段。stop the world
- 筛选回收阶段。根据设置选取相应划分区域进行回收。
内存分配
- Young Generation 区分为三部分: Eden Memory,Survivor0 Memory (S0),Survivor1 Memory(S1).
- 对象主要分配在新生代的Eden区上,如果 Eden Memory 内存满了,则进行 GC 操作。同时把未被 GC 的 Object 移动到 S0 或 S1 中。 此时 Minor GC 也会检查和移动 S0 和 S1 中的对象,最后使 S0,S1 其中一个置为空。
- 大对象(需要大量连续内存空间的java对象,如数组)直接分配到老年代。
- 长期存活的对象将进入老年代。
- 动态对象年龄判定,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
- 空间分配担保: minor gc时,根据每次回收晋升到老年代对象容量的平均大小和老年代剩余空间的对比结果,决定进行minor gc还是full gc
- 内存回收和垃圾收集器在很多时候都是影响系统性能,并发能力的主要因素之一
- full gc: 就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;
- minor gc: 针对Young Generation的gc
总结
- 设new A(); 需要在堆上新生代的Eden区域为其分配内存。如果内存不够,转2
- 对新生代进行minor gc,使用复制算法,将Eden中存活对象B复制到Survivor区域,如果Survivor区域不足,转3,清除Eden区域。然后转转1,如果Eden区域还不够,转4.
- 把B存到老年区中。如果老年区大小不足。转5.
- 把A存到老年区。如果老年区大小不足。转5.
- 对老年去进行full gc,采用标记-整理算法。