深入了解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 条评论
登录 后参与评论

相关文章

来自专栏Java进阶

zookeeper的zab协议工作原理之原子广播

3166
来自专栏技术碎碎念

探究JVM——垃圾回收

垃圾回收主要考虑三件事情:哪些内存需要回收?什么时候回收?如何回收? 一、哪些内存需要回收? 堆内存:对于JVM 来说,垃圾回收主要是针对堆内存中的对象实例。 ...

32411
来自专栏Java Edge

垃圾收集策略静态内存分配和回收动态内存分配和回收1 Java堆内存的回收2 回收无效对象的过程3 方法区的内存回收4 垃圾收集算法5 Java中引用的种类

2639
来自专栏技术专栏

ElasticSearch 优化配置

711
来自专栏小勇DW3

JVM虚拟机学习一:垃圾回收算法总结

  基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所...

621
来自专栏Jimoer

JVM学习记录-对象已死吗

前言 先来回顾一下,在jvm运行时数据区,分为两部分,一个部分是线程共享区,主要包括堆和方法区,另一部是线程私有区分包括本地方法栈,虚拟机栈和程序计数器。在线程...

3246
来自专栏nnngu

03 JVM的垃圾回收机制

1、前言 理解JVM的垃圾回收机制(简称GC)有什么好处呢?作为一名软件开发者,满足自己的好奇心将是一个很好的理由,不过更重要的是,理解GC工作机制可以帮助你写...

2013
来自专栏微信公众号:Java团长

Java面试知识点解析——JVM篇

注意:跨平台的是 Java 程序,而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM

973
来自专栏风中追风

分布式进阶__zookeeper的zab协议工作原理之原子广播

paxos协议主要就是如何保证在分布式环网络环境下,各个服务器如何达成一致最终保证数据的一致性问题

38515
来自专栏JAVA高级架构

JAVA高级面试总结-JVM篇

1.Sun HotSpot VM,是JDK和Open JDK中自带的虚拟机,也是目前使用范围最广的Java虚拟机。 2.JVM内存分布 ? 程序计数器:是一块较...

4218

扫码关注云+社区