垃圾回收与内存分配策略

时间:2020-12-10 00:02:36

在了解垃圾回收之前,我想问大家三个问题,哪些内存需要回收?什么时候可以回收?怎么回收?我相信解决了这三个问题大家对GC会有一个更全面的了解。

 

哪些内存需要回收?

  堆和方法区的内存需要被回收。因为程序计数器、虚拟机栈和本地方法栈3个区域是随着线程而生,随着线程而灭的。栈帧中分配多少内存基本上是在类结构确定下来时就已知,因此这几个区域的内存分配和回收都具备确定性。而堆和方法区则不一样,我们只能在程序运行时才知道会创建哪些对象,这部分的内存分配和回收都是动态的。

 

什么时候回收?

  对象死去的时候。如何判断对象已经死去呢?

    1.引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1。任何时刻计数器值为0的对象就是不可能再被使用的。由于其很难解决循环引用的问题,所以主流的Java虚拟机没有选用此种方法管理内存。

    2.可达性分析算法:通过一系列被称为GC Root的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Root没有引用链的时候,则说明此对象不可用了。

      有哪些对象可以被当成GC Root呢?

      1.虚拟机栈(栈帧中的本地表量表)中引用的对象,都正在被使用,肯定是活的对象吧

      2.方法区中类静态属性所引用的对象,这些是JVM本身需要长期驻留的对象

      3.方法区中常量引用的对象,这些是JVM本身需要长期驻留的对象

      4.本地方法栈中Native方法引用的对象

  注:即使在可达性分析算法中不可达的对象,他们也不会立刻判为死亡,而是处于死缓阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Root相连接的引用链,name他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()已被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行,然后对象就被判为死亡了。

 

怎么回收?

  1.标记-清除算法:算法分为标记和清除两个阶段,首先标记出需要回收的对象,标记完成后统一回收所有被标记的对象。

    优点:程序设计简单

    不足:效率低下,标记和清除两个过程效率都不高;空间问题,清除后会产生大量不连续的内存碎片。

 

  2.复制算法:将内存按容量区分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还活着的对象复制到另一块上,然后把当前使用的这块内存空间一次清理掉。

    优点:实现简单,运行高效,避免产生大量不连续的内存碎片

    不足:可用内存缩小为原来的一半,代价高昂。

    优化:新生代中的对象98%都是“朝生夕死”,所以不需要按照1:1的比例来划分。而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor大小比例是8:1。最后,没法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(老年代)来进行分配担保。

  3.标记-整理算法:前面过程与标记清除算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。老年代一般采用此种算法。

  4.分代收集算法:根据对象存活周期的不同将内存划分为几块。一般是吧Java堆分为新生代和老年代,在新生代中每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外的空间对他进行担保,就必须使用标记-清理或者标记-整理算法来进行回收。

 

  解决了一开始的三大问题之后,我们来看看虚拟机的内存分配与回收策略是什么。Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。

  1.对象优先分配在Eden区:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC(新生代GC)

    例子:JAVA堆大小为20MB,10MB分配给新生代(Eden:Suvivor=8:1),10MB分配给老年代,这时尝试按顺序分配3个2MB和1个4MB大小的对象。

    前3个对象顺利分配到Eden,当分配第4个对象时会进行一次MinorGC,因为Eden已经占了6MB,剩余空间不足以分配4MB。GC期间虚拟机又发现已有的3个2MB大小的对象无法全部放入Survivor中,所以之后通过分配担保机制提前转移到老年代中。

  2.大对象直接进入老年代

  3.长期存活的对象进入老年代:虚拟机给每个对象定义了一个年龄计数器,如果在对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survior容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中没熬过一次MinorGC,年龄就增加1岁。当他的年龄增加到一定程度(默认15),就会被晋升到老年代中。  

  优化:为了能更好地适应不同程度的内存状况,虚拟机并不是永远要求对象的年龄到了设定值才能去老年代,如果在survivor空间中相同年龄所有对象大小的综合超过了survivor空间的一半,那么年龄大于等于该年龄的对象就可以直接进入老年代。

  4.空间分配担保:在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看相应设置是否允许担保失败。如果允许那么会继续检查老年代最大可用的连续空间是否大于历次晋升代老年代对象的平均大小,如果大于,将尝试进行一次MinorGC,如果小于或者相应设置是不允许冒险,那这时要改为进行一次FullGC(老年代GC)。