我是靠谱客的博主 温婉电灯胆,最近开发中收集的这篇文章主要介绍JVM(四) — 垃圾回收机制一、概述二、如何判断对象是否被引用三、垃圾收集算法四、常见垃圾收集器五、对象如何进入老年代六、参考,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 一、概述
  • 二、如何判断对象是否被引用
    • 2.1 引用计数算法
    • 2.2 可达性分析算法
    • 2.3 引用类型
    • 2.4 回收方法区
  • 三、垃圾收集算法
    • 3.1 标记 - 清除算法
    • 3.2 复制算法 (解决“标记 - 清除算法”的效率问题)
    • 3.3 标记 - 整理算法
    • 3.4 分代收集算法
  • 四、常见垃圾收集器
    • 4.1 新生代收集器
    • 4.2 老年代收集器
    • 4.3 CMS收集器
  • 五、对象如何进入老年代
    • 5.1 大对象直接进入老年代
    • 5.2 新生代对象年龄到一定程度后进入老年代
    • 5.3 动态对象年龄判定
  • 六、参考

一、概述

在《JVM(三) — Java虚拟机运行时内存结构》中,我们了解了JVM的运行时内存结构,得到以下结论:

  1. 程序计数器、虚拟机栈、本地方法栈 都是线程私有的;
  2. 程序计数器、虚拟机栈、本地方法栈 随着线程的创建而创建,随着线程的销毁而回收;
  3. 程序计数器、虚拟机栈、本地方法栈 这几个区域的内存分配和回收都具备确定性,因此无需过多考虑回收的问题;
  4. Java堆、方法区 这两块内存区域在程序运行期会动态分配内存,具有不确定性;

小结: 垃圾回收主要针对 Java堆、方法区 这两块内存区域;

关联文章:

  • 《JVM(一) — Class 文件结构》
  • 《JVM(二) — 字节码指令》
  • 《JVM(三) — Java虚拟机运行时内存结构》
  • 《JVM(四) — 垃圾回收机制》
  • 《JVM(五) — 类加载机制》
  • 《JVM(六) — JVM面试问题》
  • 《JVM — 字节码文件分析》

二、如何判断对象是否被引用

判断一个对象是否被引用有两种方法:

  1. 引用计数算法。
  2. 可达性分析算法。

2.1 引用计数算法

原理: 给对象中添加一个引用计数器,分三种情况:

  • 当对象在一个地方被引用时,计数器值加1。
  • 当引用时效时,计数器减1。
  • 在任何时刻,计数器为0的对象就是没有被使用的对象,可以被回收。

优点:
方案实现简单。

缺点:
主流的JVM没有使用引用计数算法来管理内存,因为很难解决 对象之间相互循环引用 的问题。


2.2 可达性分析算法

目前主流的商用程序语言主流实现中都通过可达性分析算法来判断对象是否存活。

原理:

  1. 从一系列称为 GC Roots 的对象作为起点,开始向下搜索,搜索所走过的路径称为 引用链
  2. 当一个对象到 GC Roots 没有任何引用链相连时,就说明该对象是不可用的(可被回收的)。
  3. 下图中的 object5、object6、object7 虽然有被引用,但没有一条到 GC Roots 的引用链,所以是可回收的对象。
    这里写图片描述

Java中可以作为 GC Roots 对象的有如下几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中静态属性引用的对象。
  3. 方法区中静态常量引用的对象。
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

2.3 引用类型

为了更好描述在系统运行时,对象允许存活,JDK1.2之后,Java扩充了引用的概念,将引入分为4类。

  • 强引用: 通过new构建出来的对象,且只要强引用在存在,垃圾收集器永远不会回收该引用的对象。
  • 软引用: 描述一些还在用但并非必须的对象,当系统将要发生内存溢出前,将会对这些对象进行回收。
  • 弱引用: 描述非必要的对象,一旦触发GC操作,对象就会被回收。
  • 虚引用: 为一个对象设置虚引用关联的目的是当该对象被垃圾收集器回收时,能收到一条系统通知。

2.4 回收方法区

从 《JVM(三) — Java虚拟机运行时内存结构》 中我们知道,方法区内主要存储一些已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。由于这些数据存活时间都很长,因此一般也将方法区称为虚拟机中的永久代。

方法区的垃圾收集主要回收两部分内容:废弃常量无用的类

废弃常量的回收: 2个条件。

  1. 没有任何对象引用常量池中的这个常量。
  2. 没有任何地方引用了这个字面量。

回收无用的类: 需要同时满足如下3个条件。

  1. 该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

三、垃圾收集算法

垃圾收集算法有4种:

  1. 标记-清除算法
  2. 复制算法
  3. 标记-整理算法
  4. 分代收集算法

3.1 标记 - 清除算法

原理: 该算法分为 标记清除 两个阶段。

  1. 标记阶段:首先标记处所有需要回收的对象。
  2. 清除阶段:在标记之后统一回收所有被标记的对象。

缺点:

  • 效率问题: 标记和清除两个过程效率都不高;
  • 空间问题: 清除之后会产生大量不连续的内存碎片,当之后需要创建一个较大内存的对象时,会因为无法找到找到这么一块满足需求的内存区域而提前出发 GC 操作。

3.2 复制算法 (解决“标记 - 清除算法”的效率问题)

原理:

将内存分为大小相等的两块(A、B),每次只使用一块(A),当一块(A)内存用完时,将(A)中还存活的对象复制到另一块内存(B)中,然后清除内存(A),避免了内存回收之后的碎片化,简单、高效

缺点:

  1. 内存会缩小到原先的一半;
  2. 当内存中对象存活率较高时,要进行多次的复制操作,效率会降低。

改善: 目前商业虚拟机都采用这种收集算法来回收新生代。

为了改善这个算法对内存的浪费问题,IBM公司通过研究发现新生代的对象98%是“朝生夕死”的,因此并不需要安照1:1的比例来划分内存空间,而是将内存分为一个较大的Eden空间和两个较小的Survivor空间。HotSpot虚拟机中Eden大小和Survivor大小比例为8:1。

一个Eden和两个Survivor空间的结构如下图所示。
这里写图片描述
Eden和Survivor的使用流程:

每次使用Eden和其中一个Survivor。当触发回收时,将Eden和Survivor中存活的对象一次性复制到另一个Survivor空间上,最后清理掉Eden和刚才使用的Survivor空间,从而提升内存的使用率。

当然,Eden和Survivor的模型也会存在问题。假设Eden和Survivor大小比例是8:1,当触发GC时,Eden和Survivor内存空间中存活的对象数超过10%,则另一个Survivor空间就不够用了,此时需要依赖其他内存(老年代)进行分配担保,即:会将这些对象直接通过分配担保机制存入老年代。


3.3 标记 - 整理算法

原理:

标记 - 清除算法 类似,先标记需要清理的对象,然后将所有存活的对象都向一端移动,最后直接清理掉端边界以外的内存。


3.4 分代收集算法

目前商业虚拟机的垃圾收集都采用“分代收集”。

原理:

按照对象存活周期不同的特点,将Java堆分为新生代老年代,根据不同代的特点采用不同的垃圾收集算法。

  1. 新生代:大批量对象死亡,少量存活,使用 复制算法
  2. 老年代:对象存活率高,使用 标记 - 清除标记 - 整理 算法。

Java堆中各代分布:新生代、老年代、永久代。
这里写图片描述
说明:

上图是Java堆中的分代图:分为新生代、老年代、永久代
新生代又分为:Eden区、Survivor from区、Survivor to区 三部分;


问1: 新创建的对象,在哪里分配内存空间 ?
答: 多数情况下,对象的创建是在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。


问2: 为什么新生代需要两个 Survivor区
答: 避免存活对象复制时,碎片化的发生;
参考:《为什么新生代内存需要有两个Survivor区》


问3: 为什么新生代内存需要 Eden区Survivor区
答: 延缓新生代中的对象进入年老代的速度(主要还是新生代内存和年老代内存GC策略不同);从GC角度看,年老代GC对系统影响比新生代的大。


四、常见垃圾收集器

4.1 新生代收集器

  • Serial收集器:复制算法,单条线程进行收集
  • ParNew收集器:复制算法,多条线程进行收集(优化Serial收集器)
  • Parallel Scavenge收集器:复制算法,多条线程进行收集,与ParNew收集器区别在于它会达到一个可控的吞吐量,从而提升用户体验。

4.2 老年代收集器

  • Serial Old收集器:标记整理算法,单条线程进行收集
  • Parallel Old收集器:标记整理算法,多条线程进行收集
  • CMS收集器:标记清除算法。

4.3 CMS收集器

CMS收集器的收集过程分为4步:

  1. 初始标记:会触发Stop the world,仅标记一下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记:进行GC Roots Tracing的过程,可与用户线程一起工作。
  3. 重新标记:会触发Stop the world,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
  4. 并发清除:可与用户线程一起工作。

整个过程中,并发标记和并发清除耗时最长,但这两个部分都可以与用户线程一起工作。


五、对象如何进入老年代

5.1 大对象直接进入老年代

虚拟机提供一个 -XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。目的是避免在Eden区和两个Survivor区之间发生大量的内存复制。

5.2 新生代对象年龄到一定程度后进入老年代

虚拟机给每个对象定义了一个Age的计数器,初始值为0,每经过一次GC并且存活,这个对象的Age就会加1,当增加到一定程度(默认为15),那么就会进入老年代中。

5.3 动态对象年龄判定

如果新生代Survivor区空间中相同年龄所有对象大小的总和大于Survivor区的一半,年龄大于或等于该年龄的对象就会直接进入老年代。

六、参考

  • 《深入理解Java虚拟机》
  • 《为什么新生代内存需要有两个Survivor区》
  • 《JVM(三) — Java虚拟机运行时内存结构》

最后

以上就是温婉电灯胆为你收集整理的JVM(四) — 垃圾回收机制一、概述二、如何判断对象是否被引用三、垃圾收集算法四、常见垃圾收集器五、对象如何进入老年代六、参考的全部内容,希望文章能够帮你解决JVM(四) — 垃圾回收机制一、概述二、如何判断对象是否被引用三、垃圾收集算法四、常见垃圾收集器五、对象如何进入老年代六、参考所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(39)

评论列表共有 0 条评论

立即
投稿
返回
顶部