java 垃圾收集器与内存分配策略

如何判断对象是否需要被回收?

引用计数法,

  给对象增加一个引用计数器,当计数器为0即该对象没有被引用时,说明该对象可以被回收。但是主流Java虚拟机没有采用这种方法,主要原因是无法解决循环引用的问题。比如A引用B,B引用A,计数器均不为0,但是不能被访问到。

可达性分析法,

  主流的商用程序语言(Java、C#、Lisp)使用可达性分析法(Reachability Analysis)。基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可达的,可以被回收。

  GC Roots对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

 

逃脱回收的最后机会

  可达性分析中不可达的对象,需要检查是否要执行finalize()方法,如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,则标记为“没有必要执行”,会进入待回收队列。如果被判定有必要执行finalize()方法,这个对象会被放入叫做F-Queue的队列中,由虚拟机创建的低优先级Finalizer线程去触发finalize()方法,但是不保证会等待运行结束(防止一直运行导致阻塞)。如果finalize()方法中,对象重新与引用链上的任何对象简历关联,比如把自己(this)赋值给某个类变量或者对象的成员变量,那在第二次标记时将会被移出待回收集合。但是第二次进行可达性分析时,如果被标记,则直接进入待回收集合,因为任何对象的finalize()方法只会被系统自动调用一次。

  

 

回收方法区

  方法区(或者HotSpot虚拟机中的永久代)也是会进行回收的,Java虚拟机规范中不要求虚拟机在方法区实现垃圾收集,而且方法区垃圾收集“性价比”比较低。永久代回收主要包括:废弃常量和无用的类。废弃常量比如说字符串,没有引用的时候就会被回收。

  无用的类判断比较复杂,满足一下条件可以被回收,并且虚拟机参数可调:

  • 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类写在的功能,以保证永久代不会溢出。

 

垃圾收集算法

标记-清除法,

  首先标记需要回收的对象,在标记完后统一回收被标记的对象。最基础的收集算法。

  不足:1.标记和清除效率都不高,2.标记清除后会产生大量不连续碎片,导致需要分配大对象时内存不够,导致另一次GC。

复制算法,

  将可用内存划分为相等的两块,每次只使用其中一块。当一块用完后,将存活的对象移到另一块内存,将之前使用的内存一次清空。

  优点:简单高效  

  不足:内存使用率低,只能使用一半的内存。

  现代商业虚拟机都在用这种算法来回收新生代,研究发现并不需要对半分,一般会分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和一块Survivor,HotSpot默认Eden:Survivor为8:1,只会有10%的空间会被浪费。

 

 

标记-整理算法,

  标记过程与标记-清楚算法的标记过程相同,后续步骤是让所有存活的对象都向一端移动,然后直接清理端边界以外的内存。

分代收集算法,

  当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块。一般把Java堆分为新生代和老年代,根据各个年代的特点采用适当的收集算法。新生代每次GC大批对象死亡,只有少量存活,使用复制算法。老年代中对象存活率高、没有额外空间,必须使用“标记-清理”或者“标记-整理”算法进行回收。

 

垃圾收集器

 

内存分配与回收策略

  分配规则不是百分之百确定的,其细节取决于当前使用的垃圾收集器组合,以及虚拟机中与内存相关的参数配置。

  • 对象优先在Eden分配,

    当Eden区没有足够的空间时,虚拟机将发起一次Minor GC。

  • 大对象直接进入老年代,

    大对象指,需要大量连续内存空间的Java对象,典型的是很长的字符串以及数组。大对象对虚拟机是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前出发GC。

  • 长期存活的对象将进入老年代,

      虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移到Survivor空间中,并且对象年龄设为1.每在Survivor中“熬过”一次Minor GC,年龄增加一岁,增加到一定

    程度(默认15岁),就将晋升到老年代。

  • 动态对象年龄判定

    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

  •  空间分配担保

    Minor GC前会检查老年代最大可用连续空间是否大于新生代所有对象空间,如果成立则Minor GC可以确保安全。如果不成立,会检查配置是否允许失败。如果允许失败,会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如

    果大于,将尝试进行一Minor GC,如果小于,或者不允许冒险,则改为进行Full GC。

 

refs:

《深入理解Java虚拟机》

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

https://docs.oracle.com/javase/9/gctuning/toc.htm

posted on 2018-11-08 11:06  Lv Jianwei  阅读(156)  评论(0编辑  收藏  举报