java的GC

作者: ACtong | 来源:发表于2020-06-01 11:05 被阅读0次
    1. 哪些内存需要回收?
    2. 什么时候回收?
    3. 如何回收?

    在Java内存运行时区域各个部分,其中程序计数器、虚拟机栈、本地方法栈是不用过多考虑回收问题,因为他们是随线程而生、随线程而灭,每一个栈帧中分配了多少内存基本就是在类结构确定下来就是已知的。
    但是Java堆和方法区则有着很显著的不确定性

    一、可达性分析算法

    通过一系列的”GC Roots“的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为”引用链“(Reference Chain),如果某个对象到GC Roots 间没有任何引用链,则证明这个对象是不可能再被使用,就会被回收。

    在Java技术体系中,固定可作为GC Roots 的对象包括以下几种:

    • 方法区中静态属性的引用的对象,例如Java类的引用类型静态变量。
    • 方法区中常量引用的对象,例如字符串常量池(String Table)里的引用。
    • 本地方法栈中的JNI(Java Native Interface,则Native方法)对象。
    • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻异常对象(NullPointException、OutOfMemeryError),还有系统类加载器。
    • 所有被同步锁(synchronized 关键字)持有的对象
    二、引用类型
    • 强引用 Strong Reference:你用到的都是强引用
      Object obj=new Object()这中关系,只要引用关系还在,就不会回收掉引用的对象
    • 软引用 Soft Reference:内存不够的时候会回收他们
    • 弱引用 Weak Reference:GC一碰到就回收它们
    • 影子引用 Phantom Reference:只能拿到影子,拿不到引用本身
    三、对象生成还是死亡?

    根据可达性分析算法中判定位不可达的对象,也不一定是非要回收的,这时候还是处于一个”缓刑“阶段,真正宣告对象死亡,至少要经历两次标记过程。

    两次标记过程是如何进行的?

    在可达性分析中没有在引用链上,标记一次。随后一次是,筛选此对象是否有必要执行 finalize() 方法,假如对象没有覆盖finalize() 方法,或者finalize() 方法已经被虚拟机调用,那么这两种情况都是“没必要执行”(Java中不建议用finalize()方法

    如果这个对象判定为有必要执行 finalize() 方法,那么该对象将会放置一个名为F-Queue的队列之中,然后虚拟机自动创建的、地调度优先级的Finalizer线程去执行他们的finalize() 方法。finalize() 方法是对象逃脱死亡最后一次机会,售后收集器堆F-Queue中的对象进行第二次小规模的标记,如果对象要逃脱死亡命运,只要重新与引用链上的任何一个对象建立关联即可,例如 this 关键字赋值给某个类变量或者对象成员变量。如果这里进行了第二次标记,那么对象真的就要被回收了。

    finalize() 方法只会被系统调用一次,第二次不会被执行。
    Java语言中是不建议使用finalize()方法,使用try-finally可以做的更好。

    四、回收方法区
    • 一般不会在方法区类进行回收,因为性价比低。

    • 一般在新生代中可以回收70%~99%的内存空间。

    方法区的回收条件苛刻。方法区回收两部分内容:

    • 废弃的常量
    • 不再使用的类型。
    回收废弃的常量:

    只要虚拟机中其他地方没有应用这个常量,将会被清理常量池(常量池中的接口,方法、字段的符号引用将都会被清理)

    判定一个类型是否属于“不再被使用的类”的条件:

    1、该类实例已经被回收,也就是不存在该类及其任何派生子类。
    2、该加载类已经被类加载器回收(除非是精心设计的类,否则是很难达到)
    3、该类对应的java.lang.Class对象没有任何地方引用,无法在任何地方通过反射访问到该类。

    以上三个条件,仅仅只是“被允许”,并不是和对象一样,没有了引用必然会回收。
    是否要对类型进行回收,HotSpot虚拟机提供了一系列配置参数


    垃圾收集算法

    1、分代收集理论

    分代收集理论是建立在两个分代假说之上

    • 弱分代假说(Weak Generational Hypothesis):绝大多少对象都是是朝生夕灭的。
    • 强分代假说(Strong Generational Hypothesis):熬过多次垃圾收集过程的对象就越难以消亡

    这两个分代假说共同奠基 了多款垃圾收集器的设计原则

    收集器应该将Java堆分划出不同的区域,然后将回收对象依据年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么就把他们集中放在一起,每次回收时,只关注如何保留少量存活,而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量的空间。如果剩下的都是难以消亡的对象,那么把它们集中在一块,虚拟机就可以用较低频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存空间有效利用

    所以就有了Minor GC,Major GC,Full GC这个的回收类型的划分,如果在商用虚拟机里,一般会划分为新生代(Young Generation)和老年代(Old Generation)

    • 新生代:每次垃圾收集时发现有大批对象死去,每次回收后存活的少量对象
    • 老年代:从新生代中存活下来的对象,晋升到老年代

    还有一个跨代引用假说:跨代引用相对于同代引用来说仅仅占极少数

    根据前面两条理论可以隐含推论:存在互相引用关系的两个对象,应该是同时生存或者消亡的。

    2、标记-清除(Mark-Sweep)算法

    算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后,统一回收所有被标记的对象,反之也可以。

    “标记-清除”算法示意图
    缺点:
    1. 执行效率不太稳定,一旦对象过多,执行效率就会贬低,效率是随着对象数量增长而降低。
    2. 内存空间碎片化问题。标记之后会产生大量不连续的内存碎片,导致以后分配大对象时无法找到足够连续的内存空间,而不得不再一次触发垃圾收集动作。
    3、标记-复制算法

    为了解决标记-清除算法面对大对象,可回收对象时执行效率低的问题。它可以把内存空间分为两块,当一块内存使用完了,将活着的对象复制到另外一块上面,然后把已使用过的清除掉。

    标记-复制算法示意图
    缺点:
    • 使得可用内存缩小了原来的一半
    • 如果内存中多数对象是存活的,那么会产生大量的内存间复制的开销。

    将内存空间分为两块,并不是一定要1:1分配,因为在研究发现,新生代中的对象有98%活不过第一轮收集。

    HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略。也叫“Appel式回收”
    把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor空间,发生垃圾收集时,将Eden和Survivor中人仍然存活的对象复制到另一块Survivor空间上,清理掉Eden和使用过的Survivor 空间。
    HotSpot虚拟机默认Eden和Survivor的大小比例为8:1
    如果Survivor空间不足以容乃一次Minor GC之后存活的对象时,就需要依赖其他内存空间(大多数是老年代)进行分配担保(Hadle Promotion)。这也是“Appel 式回收”的一个“逃生门”

    4、标记-整理(Mark-Compact)算法

    过程与”标记-清除“算法一致,但是后续不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界意外的内存。

    “标记-整理”算法示意图
    缺点:
    • 如果存活有大对象,那么移动存活对象是一个极为负重的操作,并且必须全程暂停用户应用程序才能进行。这也形成称为”Stop The World“

    移动内存对象和不移动内存对象都有弊端,但是从整个程序吞吐量来看,移动对象会更划算。

    相关文章

      网友评论

          本文标题:java的GC

          本文链接:https://www.haomeiwen.com/subject/jkgzghtx.html