深入理解Java虚拟机 【垃圾收集器】

概述

  • Java虚拟机的内存模型分为五个部分:程序计数器、Java虚拟机栈、本地方法栈、堆、方法区。

  • 这五个区域既然是存储空间,那么为了避免Java虚拟机在运行期间内存存满的情况,就必须有一个垃圾收集者的校色,不定期的回收一些无效内存,以保障Java虚拟机能够健康的持续的运行。

  • 程序计数器、Java虚拟机栈、本地方法栈都是线程私有的,也就是每条线程都拥有这三块区域,而且随着线程的创建而创建,线程的结束而销毁。

  • 此外,Java虚拟机栈、本地方法栈中的栈帧会随着方法的开始而入栈,方法的结束而结束,并且每个栈帧的本地变量表都是在类被加载的时候确定的。因此以上三个区域的垃圾手机工作具有确定性,垃圾收集器能够清楚的知道何时清扫这三块区域中的哪些数据。

  • 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,内存的分配和回收都是动态的。方法区中存放类信息、静态成员变量、常量。类的加载是在程序运行过程中,当需要创建这个类的对象时才会加载这个类。因此,JVM究竟要加载多少个类也需要在程序运行期间确定。

堆内存的回收

判断对象是否需要回收?
  • 在垃圾收集器在堆进行回收前,首先需要确定哪些对象是否存活,一般有两种方式:
    • 引用记数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就不能再使用的。
    • 可达性分析法:这个算法的基本思路是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索的所有走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连时,则证明次对象不可用。

两者对比:

引用记数法虽然简单,但存在一个严重的问题,它无法解决循环引用的问题。因此,目前主流语言均使用可达性分析方法来判断对象是否有效。

生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的这时候他们暂时处于缓刑阶段,真正宣告一个对象的死亡,至少经历两次标记过程:

  • 判断该对象是否覆盖了finalize()方法

    • 若已覆盖该方法,并该对象的finallize()方法还没执行过,那么就会将finalize()扔到F-Queue队列中;
    • 若未覆盖方法,则直接释放对象内存
  • 执行F-Queue队列中的finalize()方法

    • 虚拟机会以较低的优先级执行这些finalize()方法们,也不会确保所有的finalize()方法都会执行结束。如果finalize()方法中出现耗时的操作,虚拟机就直接停止执行,将该对象清楚。
  • 对象重生或死亡,如果在执行finalize()方法时,将this赋给了某一个引用,那么该对象就重生了。如果哦没有,那么就会被垃圾收集器清楚。

注意:如需释放资源,就使用try-finally,不建议使用finalize()函数进行其他的操作。

谈引用

  • 强引用:
    • 程序代码中普遍存在,类似“Object obj = new Object()”
    • 垃圾收集器永远不会回收掉被引用的对象。
  • 软引用
    • 只有当系统将要发生内存溢出(OOM)之前,将会把这些对象列进回收范围之中进行第二次回收,JVm才会回收软引用所指向的对象。
    • 软引用通过哦SoftReference类实现。
    • 软引用的生命周期比强引用短一些。
  • 弱引用
    • 只要垃圾收集器运行,软引用所指向的对象就会被回收。
    • 弱引用通过WeakReference类实现
    • 弱引用的生命周期比软引用短
  • 虚引用
    • 也称为“幽灵引用”或者“幻影引用”,为对象设置虚引用的目的就是能在这个的对象被收集器回收时收到一个系统通知,
    • 通过PhantomReference类实现。

回收方法区

如果使用复制算法实现堆的内存回收,堆就会被分为新生代和老年代。新生代中,常规的应以哦那个进行一次垃圾手机一般可回收70%-95%的空间,而永久代的垃圾收集效率远低于此。

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

方法区的主要清楚两部分垃圾:

  • 废弃常量

    • 如何判断废弃常量?
    • 清楚废弃的常量和清楚对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。
  • 无用的类

    • ”无用的类“需满足以下条件:
    • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过访问该类的方法。

垃圾收集算法

垃圾-清除算法
  • 首先,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

不足之处:一个是效率问题,标记和清楚两个过程的效率都不高;另外是空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一垃圾收集动作。

复制算法

它将可用的内存按容量划分为大小相等的两块,每次只使用一块。当一块内存用完了,就将还存活的对象复制到另外一块上面。当需要回收垃圾时,也是首先标记出废弃的数据,然后将有用的数据复制到另一块内存上,最后将第一块内存全部清除。

不足之处:内存缩小一半,代价较高,每次都需要将有用的数据全部复制到另一片内存上去,效率不高。

空间效率问题:

在新生代中,由于大量的对象都是“朝生夕死”,也就是一次垃圾收集后只有少量对象存活,因此我们可以将内存划分成三块:Eden、Survior1、Survior2,内存大小分别是8:1:1。分配内存时,只使用Eden和一块Survior1。当发现Eden+Survior1的内存即将满时,JVM会发起一次MinorGC,清除掉废弃的对象,并将所有存活下来的对象复制到另一块Survior2中。那么,接下来就使用Survior2+Eden进行内存分配。

通过这种方式,只需要浪费10%的内存空间即可实现带有压缩功能的垃圾收集方法,避免了内存碎片的问题。

但是,当一个对象要申请内存空间时,发现Eden+Survior中剩下的空间无法放置该对象,此时需要进行Minor GC,如果MinorGC过后空闲出来的内存空间仍然无法放置该对象,那么此时就需要将对象转移到老年代中,这种方式叫做“分配担保”

分配担保

当JVM准备为一个对象分配内存空间时,发现此时Eden+Survior中空闲的区域无法装下该对象,那么就会触发MinorGC,对该区域的废弃对象进行回收。但如果MinorGC过后只有少量对象被回收,仍然无法装下新对象,那么此时需要将Eden+Survior中的所有对象都转移到老年代中,然后再将新对象存入Eden区。这个过程就是“分配担保”。

标记-整理算法

在回收垃圾前,首先将所有废弃的对象坐上标记,然后将所有未被标记的对象移动到一边,最后清空另一边区域即可。

分代收集算法

将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。然后在不同的区域使用不同的垃圾收集算法。

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/qq_38386316/article/details/81974788
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞