深入理解java虚拟机 垃圾收集器与内存分配策 垃圾收集算法 HotSpot的实现 垃圾收集器 – 内存分配与回收策略

程序计数器,虚拟机栈,本地方法栈随着线程生成或销毁。栈帧随着方法的进入和退出而执行出入栈,不需要考虑垃圾回收。而java堆和方法区需要动态回收,所以这部分内存的分配需要垃圾收集

  • 引用计数算法
    • 每有一个地方引用,计数器+1;引用失效减一;为0则可回收
    • 很难解决相互循环引用
  • 可达性分析: 通过一些列成为GC ROOTS的对象最为起点,从这些节点开始向下搜索,搜索的路径成为引用链,当一个对象到GC ROOTS没有任何引用链时,则被判定为可回收对象。
    • GC ROOTS:
      • 虚拟机栈中(栈帧中的本地变量表)引用的对象
      • 方法区中类静态属性引用的对象
      • 方法区中常量引用的对象
      • 本地方法栈中JNI(一般为native方法)引用的对象

下面四种引用强度依次递减

  • 强引用: 类似Object obj= new Object(),只要强引用还存在,垃圾收集器永远不会收集该对象
  • 软引用: 还有用但并非必须的对象。在系统将要oom时,会把这些对象列入回收范围进行二次回收,还是oom才会抛出异常
  • 弱引用。当垃圾收集器工作时,无论当前内存是否足够,都会回收只被弱引用关联的对象
  • 虚引用。不会被生存时间构成影响,唯一目的是正在这个对象被回收时收到一条系统消息

对象死亡的标记过程

对象在可达性分析后发现没有与GC roots相连的引用链,那么会被第一次标记并进行一次筛选,筛选条件是该对象有没有必要执行finalize()方法

  • 如果对象没有覆盖finalize方法或者finalize方法被执行过,就是没必要执行。(finalize方法只会被执行一次)
  • 如果该对象需要执行finalize,则被放在一个F-queue中,稍后会被一个Finalizer线程执行(这里的执行并不保证finalize函数执行完毕,为了防止finalize卡住或者死循环之类导致gc失败)。如果finalize过程中能够重新和引用链建立关系,则会移除”即将回收”的集合,反之则基本走远了
  • 不建议使用finalize函数,他不稳定,不如try-finally

方法区的回收主要是废弃常量和无用类,无用类的判断标准有以下三点:

  • 该类的所有实例都已经被回收
  • 加载该类的classLoader被回收
  • 该类的java.lang.class对象没有在任何地方被引用。

在大量使用反射,动态代理,gcLib等地方需要注意防止永久代不会溢出

垃圾收集算法

  • 标记-清除算法: 首先标记所有需要会受到对象,标记完成后统一进行删除
    • 不足1:效率比较低
    • 产生大龄不连续的内存碎片
  • 复制算法:将内存一分为二,每次只使用其中一块。当这一块使用完,就将还存在的对象复制到另一块内存上,把原来的空间一次清理掉
    • 不足,内存使用率下降一半
    • 新生代中98%的对象都是“朝生夕死”,因此现在常用的方法都是基于这种方法。将内存分为一块较大的Eden和两块较小的Survivor。会收拾将eden和survivor中还存活的对象一次复制到另一个survivor,然后清理掉eden和原先的survivor。如果survivor放不下所有存活对象,则会放入老年代
  • 标记-整理:将所有存活的对象移动到内存的一端,之后将另一端完全清空
  • 分代回收:新生代用复制算法,老生带由于存活的久的对象较多,用标记清除或标记整理算法

HotSpot的实现

枚举根节点:利用OopMap数据结构来记录哪些地方存着对象引用。为了结缘内存,并不是在每条指令都生成oopMap,而是在指定的称作”安全点”的位置,安全点的选定都是能让程序长时间执行的位置,特征在于指令序列复用,如方法调用,循环跳转,异常跳转等。

一个问题就是需要让所有线程跑到安全点再停顿gc,两种方法:

  • 抢先式中断:用的少。gc时程序先停下来,如果不在安全点再恢复,让他跑到安全点再停止。
  • 主动式:GC需要中断线程时,简单的设置一个标志,各个线程执行时主动的去轮训这个标志,发现标志就中断自己。

安全点面临一个问题,对于那些sleep或者blocked的线程无法响应jvm的中断请求,这需要安全区域来解决。安全区域值得是在一段代码中,引用关系不会发生变化。这样在这个区域内的人户地方开始GC都是安全的。线程进入安全区域时,标识自己,这样jvm就不用处理检查这个线程的根节点枚举。

垃圾收集器

各种收集器简介

垃圾回收器的分类及优缺点:

  • 串行垃圾回收器:串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。
  • 并行垃圾回收器:并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候
  • 并发标记扫描垃圾回收器:并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。四个步骤:
    • 初始标记:标记初始的GC roots,需要stop world
    • 并发标记:GC root tracing。
    • 重新标记:修正并发标记期间变动的对象。 需要stop world
    • 并发清理:
    • 优点:并发收集,低停顿
    • 缺点:1 对cpu资源敏感,2基于标记清楚算法,有很多碎片。
  • G1: G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域

    - 内存分配与回收策略

  1. 对象优先分配在Eden
  2. 大对象直接进入老年代(通过-XX:PretenureSizeThreshold设置大对象阈值。)PS避免使用那些短命的大对象,从来减少为了获取连续空间而出现的GC
  3. 长期存活的对象将进入老年代。每个对象有一个年龄计数器,对徐昂在eden出生并且经过一次minor gc后如果存活并进入survivor,则age+1,每在survior中存活一次加一,超过一定阈值进入老年代(一般是15)
  4. 动态对象年龄判断。当survivor中年龄n的综合大于survivor容量的一半,则年龄大于等于n的就可以进入老年代。
  5. 空间分配担保。minor gc之前,会先检查老年代的最大可用连续空间,如果没有新生代所有对象总空间大则检查是否允许担保失败,如果不允许则会改成full GC,如果允许但是老年大最大可用连续空间小于历次晋升到老年代的对象的平均大小,也改为full gc

本文采用创作共用保留署名-非商业-禁止演绎4.0国际许可证,欢迎转载,但转载请注明来自http://thousandhu.github.io,并保持转载后文章内容的完整。本人保留所有版权相关权利。

本文链接:http://thousandhu.github.io/2016/06/18/深入理解java虚拟机-垃圾收集器与内存分配策/