java垃圾回收机制及其回收算法 - Go语言中文社区

java垃圾回收机制及其回收算法


GC垃圾回收:

    jvm按照对象的生命周期,将内存按“代”划分(将堆划分为多个地址池):新生代、老年代和持久代(jdk1.8后移除持久代);

    在JVM中程序(PC)计数器、JAVA栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而堆和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

    java中新创建的对象会先被放在新生代区域,该区域对象使用频繁,jvm会在该区域使用不同算法回收一定的短期对象,如果某些对象使用次数达到一定限制后,那么该对象就会被放入老年代区域,老年代区域要比新生代区域更大一些(堆内存大部分分配给了老年代区域),而持久代保存的是类的元数据、常量、类静态变量等。  

方法区和永久代的区别:

   对于方法区和永久代的区别的话,人们一直将它们看作一个部件,其实永久代实现了方法区,比作java中类的话,永久代就是接口实现类,方法区就是接口。

堆内存模型分析:

   根据jvm内存模型来看,jvm将堆分为新生代和老年代,而在新生代中又可以分为Eden与Survivor两部分,两者默认呈4:1的比例分配内存,而在survivor中又可分为两部分;

何时进行GC操作呢?

    首先何时进行回收并不是人为就能控制的,这是由jvm控制的,我们只能说什么样的对象是符合GC的,那么最容易想到的就是当对象没有引用的时候(赋值为null),而具体的过程就要提到两种gc方式:minor gc(在新生代时)和full gc(在老年代时);当新生代中的Eden区满了的时候,执行minor gc;当进入老年代的对象大于老年代剩余空间时执行full gc(老年代快满时)。为避免在Eden区和两个Survivor区发生大量的内存拷贝大对象,而让大对象直接进入老年代,如很长的字符串数组,虚拟机提供一个-XX:PretenureSizeThreadhold参数,令大于这个参数值的对象直接在老年代中分配。

回收的是什么呢?(如何判断一个对象可回收)

    当然是没用的对象,那具体怎么就能判断一个对象没用呢?这就引入了根搜索算法,从根节点GC Roots向下寻找,只要是能寻找到的对象均是有用对象,其余的对象均是可回收对象(如果不能有效回收对象则会造成内存泄漏,这个时候我们可以通过GC Roots来查看对象路径,来分析如何解决)

finalize()和System.gc()方法介绍:

    提到GC就要提到finalize()方法,该方法是在jvm确定了一个对象符合GC的条件下执行的,用于对一些外部资源的释放等操作,但是何时对这个对象回收我们就不知道了;需要注意的是在jvm调用了该方法后,这个符合GC的对象也不一定最后就被回收了,因为在执行了finalize()方法后由于在方法体给对该方法进行了一些操作,使得该对象不符合GC的条件,例如将一个引用指向这个对象,最终导致该对象不会被GC,但这也只能求这个对象依次。

    同样还有System.gc()方法,这个方法的调用,jvm也不会立即执行对对象的回收,gc()仅仅是提醒jvm可以回收该方法了,但实际上要根据jvm内存需求来确定何实回收这个可以回收的对象。

那么gc()和finalize()的区别是什么呢?

    首先finalize()方法是jvm调用的,但是在回收期间不一定每个对象都会调用这个方法进行收尾工作,这也是这个方法不被提倡使用的原因。而System.gc()方法可以人为调用进行标记一个对象可以被回收。

最后我们从何时回收对象比较,finalize()标记的对象是在被标记后的第二次回收时进行回收,而System.gc()方法没有这种规定,它只是被标记,何时回收由jvm决定。

代码示例:

public class Test {

    @Override

    protected void finalize() throws Throwable {

    super.finalize();

    System.out.println("调用");

    }

    public static void main(String[] args){
    
    Test test = new Test();

    test=null;

    System.gc();

    }

}

分析:

    我们这里创建了Test类并重写了finalize()方法,然后我在主方法里创建了一个Test对象,并使其引用为空(此时符合回收条件)我们先调用System.gc()

结果:

    调用

    我们发现执行了finalize()方法,OK,我们现在将System.gc()注释掉,我们会发现并没有输出“调用”,也就是没有调用finalize()方法,这就是不一定每个垃圾对象jvm都会自动调用finalize()方法。

 

在Java语言中,可作为GC Roots的对象包括下面几种:

  a) Java栈中引用的对象(栈帧中的本地变量表);

  b) 方法区中类静态属性引用的对象;

  c) 方法区中常量引用的对象;

  d) 本地方法栈中JNI(Native方法)引用的对象。

常见的回收算法分析:

  1. 标记-清除算法(Mark-Sweep)

从根节点开始向下扫描,将能够查询到的对象进行标记,然后回收未标记处即可;会产生大量内存碎片;

 

  1. 复制算法(Copying)

将整个内存分为两部分,一部分用来为对象分配内存,另一部分用来将存活的对象复制过去,方便回收可回收对象(复制过去后,直接回收另一部分的所有对象即可);适用于存活对象少,大量短期对象的情况。(因为复制大量存活的对象很花费时间)

 

 

  1. 标记-整理算法(Mark-Compact)

与标记-清除算法相同,都是先标记从GC Roots向下查询到的对象,然后对未标记的对象进行回收,但是不同的是标记-整理算法最后会将所有存活对象整理到一端,减少了内存碎片

 

 

  1. 分代收集算法(Generational Collection)

    jvm根据对象的生命周期将堆内存按"代"分为新生代和老年代,所有jvm可以在对象的不同生命周期可以根据实际情况使用不同的回收算法,这样有利于提高回收效率和成本

  • 在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,则使用复制算法;如果对堆内存有一定的了解的话,我们知道新生代内存被分为一个较大的Eden区和两个较小的Survivor区,每次只使用Eden区和一个Survivor区,当回收时将Eden区和Survivor还存活着的对象一次性的拷贝到另一个Survivor区上,最后清理掉Eden区和刚才使用过的Survivor区,Eden和一个Survivor的默认比例是8:1。
  • 老年代中因为对象存活率高、没有足够大的额外空间对存活的对象进行复制,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。

 

未完待续...

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/a602519773/article/details/82529240
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-07 18:46:24
  • 阅读 ( 754 )
  • 分类:算法

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢