JVM (八)垃圾回收概述

414 阅读10分钟

目录

1.垃圾回收概述

1.1.什么是垃圾

垃圾是指在程序运行过程中没有任何指针指向的对象,这个对象就是需要被回收的垃圾

如果不及时对这些垃圾进行回收,那么这些内存垃圾会一直存在与内存中,直到应用程序结束,被保留的空间无法被其他对象使用,可能导致内存溢出

1.2为什么要GC

1.如果不进行垃圾回收,垃圾会将内存消耗完

2.将内存中的碎片移到堆的另一端,以便jvm将整理出的内存分配给新的对象

2. 垃圾回收的相关算法

GC的两个阶段:标记阶段,清除阶段

2.1 垃圾标记阶段算法(对象存活判断)

当一个对象已经不再被任何的存活的对象继续引用,就可以宣判为已经死亡

判断对象的存活一般有两种方式:引用计数算法,可达性分析算法

2.1.1 引用计数算法

对每一个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况

优点:实现简单,垃圾对象便于辨识,判定效率高,回收没有延迟

缺点:需要单独的字段存储计数器,增加了存储的空间开销

每次赋值都需要更新计数器,增加了时间的开销

引用计数器无法处理循环引用的问题,导致java垃圾回收器没有使用这类算法

2.1.2 可达性分析算法(根搜索性算法)

能够有效的解决在引用计数算法中循环引用的问题

GC Roots:跟集合,一组必须活跃的引用

基本思路:

可达性分析算法是以跟对象集合(GC Roots)为起始点,按照从上至下的方式搜索被跟对象集合所链接的目标对象是否可达

使用可达性分析算法后,内存中的存活对象都会被跟对象集合直接或则间接的连接着,搜索走过的路经称为引用链

如果目标对象没有任何引用链相连,则是不可达的,意味着对象已经死亡,可以标记为垃圾对象

在可达性分析算法中,只有能够被跟对象集合直接或者间接链接的对象才是存活的

image-20201201195923144

GC roots包括以下几类元素 (两栈+两区+同步锁)

  • 虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数,局部变量等
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象。比如:java类的引用类型静态变量
  • 方法区中常量引用的对象,比如: 字符串常量池里的引用
  • 所有被同步锁synchronized持有的对象
  • java虚拟机内部的引用
  • 反应虚拟机内部情况的jmxbean

除了这些固定的GC roots集合以外,根据用户所选的垃圾收集器以及当前回收的内存区域的不同,还可以有其他的对象,比如:分代收集和局部回收

如果指针对java堆中的某一块区域进行回收,这个对象可能被堆区其他的元素引用,这时需要将相关联的对象也加入GC Roots集合中去考虑,才能够保证可达性分析的准确性

2.1.3 判定为GC ROOTS的条件

如果是一个指针,他保存了堆内存中的对象,但是自己又不存放在对内存里面,他就是一个GCROOT

注意:可达性分析工作必须在一个能够保证一致性的快照中进行, 这点不满足的话,分析结果的准确性就无法保证,这点也是导致GC进行时必须stop the wold 的一个重要的原因,

3. 对象的finalization机制

java语言提供了对象终止的机制(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑

当垃圾回收器发现没有引用指向一个对象,即垃圾回收此对对象之前,总会先调用这个对象的finalize()方法

finlize方法不要主动的去调用,应该交给垃圾回收来调用,理由如下:

  • 在finlize()时可能会造成对象的复活

  • finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize方法将没有机会执行

  • 一个糟糕的finalize()方法会影响GC性能

4. 虚拟机中对象的三种状态

  • 可触及的:从跟节点开始,可以到达的对象

  • 可复活的:对象的所有引用都被释放了,但是对象有可能在finalize()中复活

  • 不可触及的:对象的finalize()方法被调用,并且没有复活,那么就会进入不可触及状态,不可触及的对象不可能被复活,

5. 清除阶段的相关算法

5.1 标记-清除算法mark-sweep

执行过程:当堆中的有效内存将被耗尽,就会停止整个程序,称为stop the world 然后进行两项工作。第一项就是标记,第二项就是清除

标记:Collector从引用跟节点开始遍历,将能够遍历到的对象进行标记,一般是在对象header中记录为可达对象

清楚:collector对对内存从头到尾进行线性遍历。如果发现某一个对象在其header中没有标记为可达对象,则将其回收

image-20201202110609635

优点:实现简单,

缺点:效率不高,需要遍历堆内存空间两次

进行gc时需要将进程停止,导致用户体验差

这种回收算法产生的空闲内存不是连续的,会产生空闲内存碎片,需要维护一个空闲链表,会消耗额外的内存空间

清除的含义,清楚并不是真正的清空,而是将需要清除的对象的地址加到空闲链表之中。

5.2 复制算法(copying)

将活着的内存空间分为两块,每次只使用其中的一块内存区域,在垃圾回收时将正在使用的内存中存活的对象复制到未被使用的另一个快内存空间中,之后清除正在使用的内存中的所有对象,交换两个内存的角色,最后完成垃圾回收

image-20201202112012455

优点:没有标记与清除的过程,实现简单,运行高效

复制过程不会产生内存碎片,回收的内存空间连续

缺点:需要两倍的内存空间,会浪费内存空间

对于G1这种拆分为大量region的gc,复制而不是移动,意味着GC需要维护region之间对象引用的关系,不管是内存占用,还是时间开销都花费巨大

5.3标记-压缩(mark-compact)

第一阶段:和标记-清除算法一样,从跟节点开始标记所有被引用的对象

第二阶段:将所有的存活对象压缩到内存的一端,按顺序排放,然后清除边界外所有的空间

image-20201202113829614

优点:消除了标记-清楚算法当中,内存分散的缺点,我们需要给新对象分配内存时,只需持有内存的起始地址即可,

消除了复制算法当中,内存减半的高额代价

缺点:从效率上来说,效率较低

移动对象的同时,如果对象被其他对象引用,则还需要整理引用地址,

移动过程中,需要全程暂停用户应用程序:即STW

5.4 三种算法的对比

image-20201202200824356

5.5 分代收集算法

不同生命周期的对象使用不同的收集方法,主要根据各个年龄代的不同,使用不同的垃圾收集算法

年轻代:区域相对老年代较小,对象生命周期短,存活周期短,回收频繁

采用复制算法,速度最快,而通过两个survivor的设计得到换届

老年代,区域较大,对象较大,回收不及年轻代频繁

一般采用标记清除算法或者标记压缩算法,混合使用

mark阶段的开销与存活对象的数量成正比

sweep阶段的开销与所管理的区域大小成正比

compact阶段的开销与对象的数成正比

5.6 增量收集算法

以上算法会发生stw,如果垃圾回收时间较长,则程序被挂起较长

可以让垃圾收集线程和应用线程交替执行,每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,减少停顿时间

缺点:间断性的还执行了应用程序的代码,所以能够减少系统的停顿时间,但是,因为线程切换和上下问转换的消耗,会使得垃圾会回收的总体成本上升,造成系统吞吐量下降

5.7 分区算法

堆的空间越大,gc消耗的时间越长,可以将一个较大的内存区域分割成为多个小的模块,根据目标的停顿时间,每次合理的收集若干小的区间,而不是整个堆空间,从而减少一次gc所产生的停顿

image-20201202204117191

每个小的区域单独的使用,独立的回收,这种算法的好处是可以控制一次回收多个小的区域

6. 垃圾回收的相关概念

6.1 1.System.gc()或者Runtime.getRuntime().gc()的调用,

会显示的触发full gc,同时对老年代与新生代进行回收,尝试释放被丢弃的对象的内存

6. 2内存溢出

没有空闲内存,并且垃圾收集器也无法提供更多的内存

虚拟机内存溢出的原因

  • java虚拟机的对内存设置不够
  • 代码中创建了大量的大对象,并且长时间不能过被垃圾收集器收集(存在被引用)

6.3.内存泄露

对象不会再被程序用到了,但是GC又不能回收他们的情况,就叫做内存泄露

尽管内存泄露并不会立刻引起程序的崩溃,但是一旦发生内存泄露,程序中的可用内存就会被逐步的蚕食,直至耗尽所有的内存,最终出现Outofmemory

内存泄露的示意图

image-20201204202151903

内存泄露的例子

1.单例模式

单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能够被回收的,则会导致内存的泄露的产生

2.一些提供close的资源未关闭导致内存泄露

数据库链接,网络链接和io链接必须手动的关闭close,否则是不能够被回收

STOP THE WORLD

在发生gc的过程中,会产生应用程序的停顿,停顿产生时整个应用程序会被暂停,没有任何响应