专栏首页大闲人柴毛毛深入了解JVM垃圾收集器

深入了解JVM垃圾收集器

程序计数器、JVM栈、本地方法栈这三个内存区域和线程是一一对应的,并且每一个线程的这三个区域相互独立互不干扰。他们都随着线程的产生而产生,线程的灭亡而灭亡。JVM栈和本地方法栈中的栈帧都随着方法的加载而入栈,随着方法的结束而出栈。

栈帧的大小是在程序员写类的时候就确定下来的。因此这三种区域的内存大小都具备确定性,当方法结束或线程结束后,这些内存空间就会自动被回收掉,所以JVM无需考虑这些区域的内存回收问题。

堆内存和方法区的内存分配和回收就不一样了,因为一个接口中的多个实现类所需要的内存可能不一样,并且一个方法中的多个分支所需要的内存也不一样,到底要执行哪个分支,这就要看运行时的情况了。所以堆内存中的数据的分配和回收都是动态的。所以垃圾回收器只关注堆内存中的数据的分配和回收。

判断对象是否已死的理论方法

  • 引用计数算法 a)引用计数算法是什么? 给所有的对象添加一个引用计数器,每当有一个地方引用了这个对象时,就将计数器的值+1;每当引用失效时,就将计数器的值-1;当一个对象计数器的值为0时,就认为这个对象已经没用了,垃圾收集器可以把它回收。 b)引用计数算法的优点 这个算法实现简单,效率较高,大部分情况下都是个不错的算法。 c)引用计数算法的缺点 Java并没有选用引用计数算法来管理内存,因为这个算法当遇上对象的循环引用的时候就黔驴技穷了。 d)啥是“循环引用“? 这个程序中,首先创建两个有引用指向的对象obj1、obj2; 然后分别将堆内存中两个对象中的obj0引用分别指向对象,这样,堆内存中的两个对象互相指向; 最后去除JVM栈中两个引用对堆内存中两个对象的指向;此时堆内存中的两个对象已经脱离了JVM栈中引用的指向,但堆内存中的两个对象互相指向,所以这两个对象的计数器都不为0,所以垃圾收集器不会将这两个对象视作垃圾。但事实上,只要堆内存中的对象没有JVM栈中引用指向的时候,这些对象就已经没有用了,因为函数已经无法再操控这些没有引用指向的对象了。这种循环引用的对象也应当被当作垃圾回收。
public class A{
    public Object obj0 = null;
    public static void main(String[] argus){
        A obj1 = new A();
        A obj2 = new A();
        obj1.obj0 = obj2;
        obj2.obj0 = obj1;
        obj1 = null;
        obj2 = null;
    }
}
  • 根搜索算法 a)特点? 目前主流的程序语言都是使用根搜素算法来判定对象是否存活的,Java也是。 b)是啥? 根搜索算法中将GC Roots作为起始节点,然后垃圾回收器从这这些起始节点开始搜索,搜索走过的路经称为引用链,当一个对象到GC Roots不可达,则证明这个对象已经死了!垃圾收集器可以将它回收。 c)GC Roots由以下几种对象构成 1.存放在JVM栈中的栈帧中的本地变量表中的 引用的 对象可作为GC Roots 2.存放在方法区中的静态属性引用的对象,可作为GC Roots 3.方法区中的常量引用的对象可作为GC Roots 4.本地方法栈中的JNI(即Native方法)的引用的对象,可作为GC Roots

Java的四种引用

  • 强引用 我们平常创建的对象都是强引用:Person p = new Person(); 强引用只要JVM栈中引用对应堆内存中的对象指针存在,那么GC就不会回收堆内存中的这个对象。
  • 软引用 通过SoftReference类将对象声明成软引用之后,一般情况下,堆内存中的一个对象被JVM栈中的一个软引用指向,那么在垃圾回收的时候这个对象是不会被回收的;当堆内存即将发生OOM异常,此时JVM就会做出一次假设:假设堆内存中所有被软引用指向的对象被回收了,是否还会发生OOM,如果不会发生OOM,那么就将所有软引用的对象回收;如果还会发生OOM,那么就直接抛出OOM。
  • 弱引用 被弱引用指向的对象,它们的生命周期只有从创建到发生一次垃圾回收结束;也就是说,一旦发生了垃圾回收,这些被弱引用指向的对象一定会被杀死! 通过WeakReference类来声明弱引用。
  • 虚引用 也叫幽灵引用或幻影引用,虚引用虽然也指向了一个堆内存中的对象,但无法通过这个引用来对这个对象做任何的处理。一个对象使用虚引用关联的唯一目的就是希望这个对象在被垃圾收集器回收前收到一个系统通知。 虚引用使用PhantomReference类来声明。

对象的回收过程

  1. 首先,垃圾收集器寻找所有与GC Roots没有关联的对象;
  2. 查看这些对象是否重写了finalize方法,或finalize方法是否已被执行过;
  3. 若这个对象没有重写finalize方法,或finalize方法已经被执行过了,那么这个对象就被杀死了;
  4. 如果重写了finalize方法,那么执行这个方法,如果这个方法执行完后,这个对象与GC Roots发生了关联,那么这个对象就逃过一劫,否则这个对象就被杀死。 PS:Java提供finalize方法的目的是为了让那些C++程序员适应Java中不需要对象的手动释放,这个方法中可以做一些对象生命结束时的善后工作。但是我们平时千万不要使用这个方法,因为它开销很大,而且finalize方法能做的工作,try-finally也可以做。这里只是为了了解finalize方法,它在实际开发中完全不需要!

方法区中数据的回收

方法区中存放的是类信息、常量、静态变量、编译后的代码,这些数据一般需要长时间使用,所以方法区中的垃圾回收的性价比比较低。在堆内存中进行垃圾回收,一次可以回收70%-95%的空间;而在方法区中进行垃圾回收的效率远远低于堆内存的回收效率。

方法区中的垃圾回收主要回收两部分数据:废弃常量、无用的类。

  • 废弃常量的回收 废弃常量的回收和堆内存中对象的回收非常类似,加入字符串常量“abc“已经没有任何一个String类型的引用指向,那么当发生垃圾回收时,这个常量就会被内存回收。 PS:常量都存储在方法区的常量池中,变量都存储在JVM栈中的栈帧中的局部变量表中。
  • 无用类的回收 相对于废弃常量的回收,对于无用类的回收相对比较复杂。 一个类同时满足以下这三个条件才能被判定为无用类: a)堆内存中不存在该类的任何实例 b)加载该类的ClassLoader已被回收 c)该类的Class对象没有在任何地方被引用,也就是没有任何地方通过反射机制访问该类中的成员。 PS:每个类都对应着一个Class对象,记录类的相关信息。

垃圾回收算法

  • 标记-清除算法 a)是什么? 这个算法在执行过程中,首先会标记出需要清除的对象们, 标记完成后统一回收所有的对象。 b)缺点 回收和清除过程效率都不高。 清除之后会产生大量不连续的内存空间,当有一个大对象申请内存空间的时候,由于没有连续的大内存分配给它,这时又不得不发起一次垃圾收集工作。
  • 复制算法 a)是什么? 为了提升标记-清除算法的效率问题,所以出现了复制算法。它将内存分成完全相同的两块,每次只使用其中的一块,如果这一块内存用完了,就进行一次垃圾清理,然后把还存活的对象复制到另一块上去,然后把刚才的那块内存全部清理干净。由于每次垃圾回收都将对象集中到另一块上去,所以碎片就减少了很多。 b)特性 现在的商业虚拟机都是采用这种收集算法来回收新生代。 IBM的研究表明,新生代中的对象98%都是经历了一次GC之后就会死掉,所以堆内存不必按照1:1的大小来划分,而是将堆内存划分成一块较大的Eden空间和两块较小的Survivor空间,每次分配内存时只使用一块较大的Eden和Survivor。当进行垃圾回收时,大约98%的对象都被杀死了,再将Eden和Survivor空间中2%的那些活着的对象复制到另一个Survivor中,最后将Eden和Survivor中的对象全部清除。 HotSpot虚拟机默认的Eden和Survivor的大小比是8:1,也就是Eden、Survivor、Survivor的大小比例是:80%、10%、10%,也就是只有10%的空间会被浪费掉。 但是,HotSot默认的Eden和Survivor的大小比是8:1,但是我们没有办法保证经过一次GC之后能存活的对象总大小能装入第二个Survivor中。当出现存活的对象无法装入Survivor中时,此时就需要将存活的对象装入老年代中,这种方法叫做分配担保。 c)什么是分配担保? 一般情况下,第二个Survivor大小占10%即可处理大部分GC情况,但有时候GC完一次之后可能存在超过10%的对象存活,此时这些对象装不下第二个Survivor中,所以就需要老年代的空间作为发生这种情况时的担保,将超过10%的对象装入老年代中去。 d)缺点 若经过一次GC之后对象存活率很高,就需要大量的复制操作将对象复制到第二个Survivor中去,此时就会效率低。
  • 标记-整理算法 和标记-清理算法类似,首先将所有没有引用指向的对象作标记,然后清理掉这些对象,最后将所有存活的标记移向一边。这样就减少了标记-清理算法中的碎片问题。
  • 分代收集算法 当前商业虚拟机的垃圾收集都采用分代收集算法,这种算法根据对象存活时间的不同将内存分为几块,一般把堆内存分为新生代和老年代。在新生代中,每次垃圾回收都有大量的对象死去,只有少量的对象存活,就使用复制算法,只需要付出少量的复制成本就可以完成垃圾收集工作。在老年代中的对象存活率高,而且没有额外的空间给它进行分配担保,所以必须使用标记-清除 或 标记-整理 算法进行垃圾回收。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深入理解JVM(二)——揭开HotSpot对象创建的奥秘

    对象的创建过程 当虚拟机遇到一条含有new的指令时,会进行一系列对象创建的操作: 检查常量池中是否有即将要创建的这个对象所属的类的符号引用; 若常量池中没...

    大闲人柴毛毛
  • 深入理解JVM(三)——垃圾收集策略详解

    Java虚拟机的内存模型分为五个部分,分别是:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。 这五个区域既然是存储空间,那么为了避免Java虚拟机在运行...

    大闲人柴毛毛
  • 深入JVM——OOM异常解析

    JVM对象访问解析 对象访问过程的内存情况 public void function(){ Object obj = new Object(); } f...

    大闲人柴毛毛
  • 垃圾收集算法

    垃圾收集器是Java虚拟机中自带的功能,它的目的是帮助我们管理内存,正是因为有它的存在所以,我们在开发时,基本不用考虑内存溢出等问题。基本不用考虑不代表,一定不...

    吉林乌拉
  • 面试题:讲讲Java垃圾回收机制

    本文内容是基于 JDK 1.6 的,不同版本虚拟机之间也许会有些许差异,但不影响我们对JVM垃圾回收机制的整体把握和了解。

    用户1263954
  • JVM学习笔记——垃圾收集器与内存分配策略(1)

    上一篇文章介绍了java运行时内存的各个区域,其中虚拟机栈,程序计数器,本地方法栈三个区域随线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出有条不紊的执行着...

    用户1665735
  • Java虚拟机内存管理(四)—垃圾回收

    Java 虚拟机作为运行 Java 程序抽象出来的计算机,具有内存管理的能力,像内存分配、垃圾回收等这些相关的内存管理问题,Java 虚拟机都会帮我们解决,所以...

    Wizey
  • 深入理解Java虚拟机(JVM) --- 垃圾收集算法(中)

    由于方法区中存放生命周期较长的类信息、常量、静态变量. 因此方法区就像堆的老年代,每次GC只有少量垃圾被清除.

    JavaEdge
  • 疯狂Java笔记之Java的内存与回收

    对于JVM的垃圾回收机制来说,是否回收一个对象的标准在于:是否还有引用变量引用改对象?只要有引用变量引用对象,垃圾回收机制就不会回收它。

    HelloJack
  • 编程思想 之「字符串」

    字符串(String)对象是不可变的,把String对象作为方法的参数时,其实都是复制一份引用,而该引用所指的对象一直待在单一的物理位置上,从未动过。我们可以给...

    CG国斌

扫码关注云+社区

领取腾讯云代金券