JVM 垃圾回收

JVM 垃圾回收

垃圾回收概述:

什么是垃圾?

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

  • 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。

为什么需要GC?

  1. 对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。
  2. 除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象
  3. 随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。

垃圾回收相关算法:

1.标记阶段–引用计数算法
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。

  1. 引用计数算法(Reference Counting)比较简单,**对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况**。
  2. 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
  3. 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  4.  缺点:
     1. 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
     2. 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
     3. **引用计数器有一个严重的问题,即无法处理循环引用的情况**。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。

2.标记阶段–可达性分析算法
* 可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
* 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)。
* 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
* 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

GC Roots:

     * 虚拟机栈中引用的对象
        1. 比如:各个线程被调用的方法中使用到的参数、局部变量等。
     * 本地方法栈内JNI(通常说的本地方法)引用的对象
     * 方法区中类静态属性引用的对象
        1. 比如:Java类的引用类型静态变量
     * 方法区中常量引用的对象
        1. 比如:字符串常量池(String Table)里的引用
     * 所有被同步锁synchronized持有的对象
     * Java虚拟机内部的引用。
        1. 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
     * 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
     * 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(PartialGC)。
        * 如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GCRoots集合中去考虑,才能保证可达性分析的准确性。

        1. 典型的只针对新生代:因为新生代除外,还有关联的老年代,所以需要将老年代也一并加入GC Roots集合中

小技巧:由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。

3.对象的finalization机制:

     * Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
     * 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize( )方法。
     * finalize( ) 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
     * 永远不要主动调用某个对象的finalize( )方法,应该交给垃圾回收机制调用。理由包括下面三点:
        1. 在finalize( )执行时可能会导致对象复活。
        2. finalize( )方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize( )方法将没有执行机会。
        3. 一个糟糕的finalize( )会严重影响GC的性能。
     * 从功能上来说,finalize( )方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize( )方法在本质上不同于C++中的析构函数。
     * 由于finalize( )方法的存在,虚拟机中的对象一般处于三种可能的状态。
           1. 可触及的:从根节点开始,可以到达这个对象。
           2. 可复活的:对象的所有引用都被释放,但是对象有可能在 finalize() 中复活。
           3. 不可触及的:对象的 finalize() 被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为 finalize() 只会被调用一次。

在这里插入图片描述

4.清除阶段 标记-清除算法

当堆中的有效内存空间(Available Memory)被耗尽的时候,就会停止整个程序(也被称为 Stop The World),然后进行两项工作,第一项则是标记,第二项则是清除。

        * 标记:Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的 Header 中记录为可达对象。
           * 标记的是引用的对象,不是垃圾!!
        * 清除:Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收。

缺点:

        * 标记清除算法的效率不算高
        * 在进行 GC 的时候,需要停止整个应用程序,导致用户体验较差
        * 这种方式清理出来的空闲内存是不连续的,产生内存碎片,需要维护一个空闲列表

何为清除?

        * 这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。
        * 关于空闲列表是在为对象分配内存的时候:
           * 如果内存规整
              1. 采用指针碰撞的方式进行内存分配
           * 如果内存不规整
              1. 虚拟机需要维护一个列表
              2. 空闲列表分配

5.清除阶段 复制算法:

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

     1. 优点:
        * 没有标记和清除过程,实现简单,运行高效
        * 复制过去以后保证空间的连续性,不会出现“碎片”问题
     2. 缺点:
        * 此算法的缺点也是很明显的,就是需要**两倍**的内存空间。
        * 对于 G1 这种拆分成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象的引用关系,不管是内存占用或者时间开销也不小

在这里插入图片描述

6.清除阶段 标记-压缩(整理)算法

     * 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
     * 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
     * 之后,清理边界外所有的空间。

在这里插入图片描述

7.分代收集算法
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
目前几乎所有的 GC 都采用分代收集算法执行垃圾回收的。
* 年轻代(Young Gen)
- [ ] 年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
- [ ] 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过 HotSpot 中的两个 Survivor 的设计得到缓解。
* 老年代(Tenured Gen)
- [ ] 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
- [ ] 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。

Mark(标记)阶段的开销与存活对象的数量成正比。

Sweep(清除)阶段的开销与所管理区域的大小成正相关。

Compact(压缩)阶段的开销与存活对象的数据成正比。

垃圾回收相关概念:

System.gc的理解:

  * 在默认情况下,通过 System.gc() 者 Runtime.getRuntime().gc() 的调用,会显式触发 Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
  * 然而 System.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用。(不能确保立即生效)
  * JVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用 System.gc()。

运行程序,不一定会触发垃圾回收,但是调用 System.runFinalization() 会强制调用失去引用对象的finalize( )方法
System.gc( ) 与System.runFinalization( ) 是一起使用的

内存溢出与内存泄露:

内存溢出:

  1. 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
  2. 由于 GC 一直在发展,所以一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现 OOM 的情况。
  3. 大多数情况下,GC 会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的 Full GC 操作,这时候会回收大量的内存,供应用程序继续使用。
  4. javadoc 中对 OutOfMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。

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

内存泄漏:

  * 也称作“存储渗漏”。严格来说,**只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏**
  * 但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM,也可以叫做宽泛意义上的“内存泄漏”。
  * 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutOfMemory 异常,导致程序崩溃。
  * 注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。

java中出现内存泄露的例子:

     * 单例模式

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

     * 一些提供 close 的资源未关闭导致内存泄漏

数据库连接(dataSourse.getConnection()),网络连接(Socket)和 IO 连接必须手动 close,否则是不能被回收的。

Stop The World

在这里插入图片描述

垃圾回收的并行与并发

并发:

  1. 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理器上运行。
  2. 并发不是真正意义上的“同时进行”,只是 CPU 把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于 CPU 处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。

并行:

  1. 当系统有一个以上 CPU 时,当一个 CPU 执行一个进程时,另一个 CPU 可以执行另一个进程,两个进程互不抢占 CPU 资源,可以同时进行,我们称之为并行(Parallel)。
  2. 其实决定并行的因素不是 CPU 的数量,而是 CPU 的核心数量,比如一个 CPU 多个核也可以并行。
  3. 适合科学计算,后台处理等弱交互场景

在这里插入图片描述

垃圾回收中的并行与并发:

     * 并行(Parallel):**指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。**

如 ParNew、Parallel Scavenge、Parallel Old;

     * 串行(Serial)

相较于并行的概念,单线程执行。

如果内存不够,则程序暂停,启动 JVM 垃圾回收器进行垃圾回收。回收完,再启动程序的线程。

     * 并发(Concurrent):**指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。**

用户程序在继续运行,而垃圾收集程序线程运行于另一个 CPU 上
如:CMS、G1

安全点和安全区域:

安全点:

在这里插入图片描述

安全区域:

在这里插入图片描述

强引用–不回收

     * 强引用可以直接访问目标对象。
     * 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象。
     * 强引用可能导致内存泄漏。

软引用–内存不足即回收

弱引用–发现即回收

弱引用对象与软引用对象的最大不同就在于,当 GC 在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC 总是进行回收。弱引用对象更容易、更快被 GC 回收。

虚引用–对象回收跟踪

垃圾回收器

评价GC的性能指标:

  * **吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)**
  * 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
  * **暂停时间:执行垃圾收集时,程序的工作线程 (用户线程) 被暂停的时间。**
  * 收集频率:相对于应用程序的执行,收集操作发生的频率。
  * **内存占用:Java 堆区所占的内存大小。**
  * 快速:一个对象从诞生到被回收所经历的时间。
  * 吞吐量、暂停时间、内存占用,这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
  * 这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而带来负面效果。
  * 简单来说,主要抓住两点:吞吐量、暂停时间

不同的垃圾回收器概述:

GC 垃圾收集器是和 JVM 一脉相承的,它是和 JVM 进行搭配使用,在不同的使用场景对应的收集器也是有区别

  * 串行回收器:Serial、Serial Old
  * 并行回收器:ParNew、Parallel Scavenge、Parallel Old
  * 并发回收器:CMS、G1

在这里插入图片描述

在这里插入图片描述

Serial回收器:串行回收

在这里插入图片描述

ParNew回收器:并行回收

  * 如果说 Serial GC 是年轻代中的单线程垃圾收集器,那么 ParNew 收集器则是 Serial 收集器的多线程版本。
        1. Par 是 Parallel 的缩写,New:只能处理的是新生代
  * ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew 收集器在年轻代中同样也是采用复制算法、"Stop-The-World"机制。
  * ParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器。

Parallel回收器:吞吐量优先

  1. HotSpot 的年轻代中除了拥有 ParNew 收集器是基于并行回收的以外,Parallel Scavenge 收集器同样也采用了复制算法、并行回收和"Stop The World"机制。
  2. 那么 Parallel 收集器的出现是否多此一举?
     * 和 ParNew 收集器不同,Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
     * 自适应调节策略也是 Parallel Scavenge 与 ParNew 一个重要区别。
  3. 高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
  4. Parallel 收集器在 JDK 1.6 时提供了用于执行老年代垃圾收集的 Parallel Old 收集器,用来代替老年代的 Serial Old 收集器。
  5. Parallel Old 收集器采用了标记-压缩算法,但同样也是基于并行回收和"Stop-The-World"机制。

CMS回收器:低延迟

  * 在 JDK 1.5 时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
  * CMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,良好的响应速度能提升用户体验。
        1. 目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。
  * CMS 的垃圾收集算法采用标记-清除算法,并且也会"Stop-The-World"。
  * 不幸的是,CMS 作为老年代的收集器,却无法与 JDK 1.4.0 中已经存在的新生代收集器 Parallel Scavenge 配合工作,所以在 JDK 1.5 中使用 CMS 来收集老年代的时候,新生代只能选择 ParNew 或者 Serial 收集器中的一个。
  * 在 G1 出现之前,CMS 使用还是非常广泛的。一直到今天,仍然有很多系统使用 CMS GC。

优点:并发收集、低延迟

缺点:

     1. 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发 Full GC。
     2. CMS 收集器对 CPU 资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
     3. CMS 收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure"失败而导致另一次 Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS 将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行 GC 时释放这些之前未被回收的内存空间。

在这里插入图片描述

G1回收器:区域化分代式

在这里插入图片描述

垃圾回收器总结:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值