垃圾回收
1.垃圾回收概述
1.概述
1.Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语
言没有垃圾收集技术,需要程序员手动的收集。
2.垃圾收集,不是 Java 语言的伴生产物。早在 1960 年,第一门开始使用内存
动态分配和垃圾收集技术的 Lisp 语言诞生。
3.关于垃圾收集有三个经典问题:
哪些内存需要回收?
什么时候回收?
如何回收?
4.垃圾收集机制是 Java 的招牌能力,极大地提高了开发效率。如今,垃圾收集
几乎成为现代语言的标配。
2.什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃
圾。
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一
直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出。
3.为什么需要 GC?
1.对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
2.除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。
3.随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有 GC就不能保证应用程序的正常进行。
2.垃圾回收机制
自动内存管理
优点
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险. 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发.
担忧
1.对于 Java 开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化 Java 开发人员在程序出现内存溢出时定位问题和解决问题的能力。
2.此时,了解 JVM 的自动内存分配和内存回收原理就显得非常重要,只有在真正了解 JVM 是如何管理内存后,我们才能够在遇见 OutofMemoryError 时,快速地根据错误异常日志定位问题和解决问题。
3.当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
应该关心哪些区域的回收?
垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收,其中,Java 堆是垃圾收集器的工作重点
从次数上讲:
频繁收集 Young 区
较少收集 Old 区
基本不收集元空间(方法区)
3.垃圾回收相关算法
1.垃圾标记阶段算法
1.标记阶段的目的
主要目的是为了判断对象是否存活
- 在堆中GC执行垃圾回收前,首先会判断哪些对象是死亡或者存活;只有死亡的对象才会被标记回收,GC执行垃圾回收后会释放其内存.(垃圾标记阶段)
- 当一个对象不再被任何存活对象引用时,那么这个对象就是死亡对象.
- 如何判断对象是否存活: 引用计数算法 可达性算法
2.引用计数算法
- 它会对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的次数.
- 对于一个对象A,如果被引用了,计数器就+1 ,引用失效时 会 - 1 ;计数器显示为0时,则表示可以被回收.
- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
- 1.它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
2.每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3.引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在.Java 的垃圾回收器中没有使用这类算法。
3.可达性分析算法
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
与引用计数算法的区别
1.是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
2.相较于引用计数算法,这里的可达性分析就是 Java、C#选择的。
可达性分析实现思路
1.是以根对象集合为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
2.使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
3.如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
4.在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象
GC Roots 可以是哪些元素?
1.虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。
2.本地方法栈内 JNI(通常说的本地方法)引用的对象
3.方法区中类静态属性引用的对象
比如:Java 类的引用类型静态变量
4.方法区中常量引用的对象
比如:字符串常量池(StringTable)里的引用
5.所有被同步锁 synchronized 持有的对象
6.Java 虚拟机内部的引用。
基 本 数 据 类 型 对 应 的 Class 对 象 , 一 些 常 驻 的 异 常 对 象 ( 如 :NullPointerException、OutofMemoryError),系统类加载器。
4.对象的 finalization 机制
-
finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。
-
垃圾回收此对象之前,总会先调用这个对象的 finalize()方法。
-
Java 语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
永远不要主动调用某个对象的 finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
1.在 finalize()时可能会导致对象复活。
2.finalize()方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize()方法将没有执行机会。
3.一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。
5.生存还是死亡?
由于 finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
-
可触及的:从根节点开始,可以到达这个对象。
-
可复活的:对象的所有引用都被释放,但是对象有可能在 finalize()中复活。
-
对象的 finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次。以上 3 种状态中,是由于 finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
2.垃圾回收阶段算法
标记-清除算法
当堆中的有效内存耗尽的时候,就会停止整个程序,然后进行标记 清楚.
标记:Collector 从引用根节点开始遍历,标记所有被引用的对象。
清除:Collector 对堆内存进行线性的遍历,如果未被标记,则会被回收.
缺点:效率不高,在进行 GC 的时候,需要停止整个应用程序,用户体验较差
复制算法
为了解决标记-清除算法在垃圾收集效率方面的缺陷,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或者时间开销也不小
应用场景:
- 如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大,效率较高
- 老年代大量的对象存活,那么复制的对象将会有很多,效率会很低
标记-压缩算法
执行过程:
- 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法与标记-清除算法的比较:
-
标记-压缩算法可以说是在标记-清除算法之后加上压缩阶段.
-
二者的本质差异在于标记-清除算法是一种非移动式的回收算法(空闲列表记录位置),标记-压缩是移动式的。是
优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价
缺点:
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址移动过程中,需要全程暂停用户应用程序。即:STW
垃圾回收算法小结
-
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
-
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
即:STW
垃圾回收算法小结
-
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
-
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
[外链图片转存中…(img-brZ4yjlT-1617962280808)]