青年IT男

个人从事金融行业,就职过易极付、思建科技等重庆一流技术团队,目前就职于某网约车平台负责整个支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。

Java虚拟机基础-内存回收机制

Java虚拟机基础-内存回收机制

Java虚拟机整体篇幅如下:

本篇文章的内容如下:

  • 1 概述
  • 2 垃圾回收的思考
  • 3 那些内存需要回收
  • 4 垃圾收集算法
  • 5 Stop the World事件
  • 6 垃圾收集器

一、概述

在Java中,它的内存管理包括两方面:内存分配(创建Java的时候)内存回收,和C不同,这方面读是由JVM来完成的,避免了C/C++直接操作的风险,同时也降低了学习Java的难度。虽然内存管理完全由JVM负责,但是如果程序员不了解内存分配,就会导致高耗内存、内存泄露等问题。所以Java程序员还是要学习JVM的内存回收机制。才能写出更高效的代码。

由于前面的Java虚拟机基础——2JVM运行时数据区已经详细讲解了JVM运行时区域和Java虚拟机基础——3类加载机制中也讲解了new一个新对象的过程,里面涉及了”内存分配”。所以本篇文章将结合垃圾回收策略进一步讲解 JVM中内存的理解 。

二、垃圾回收的思考

Java虚拟机基础——2JVM运行时数据区提到,JVM内存模型一共有3个部分:堆(Java代码可及的Java堆和JVM自身使用的方法区)、栈(服务Java方法的虚拟机栈和服务Native的本地方法栈)和保证多线程环境下能够连续执行的程序计数器。特别低,我们当时就提到了Java堆是进行垃圾回收的主要区域。故其也被称为GC堆;而方法去也有一个不太严谨性的表述,就是永久带。总的来说Java堆是垃圾回收的主要对象。我们知道垃圾回收机制是Java语言的一个显著特点,其可以有效的防止内存泄露、保证内存的有效使用,从而使得Java程序在编写程序的死后不需要考虑内存管理的问题。Java垃圾回收机制要考虑的问题很复杂,本文阐述了其三个核心问题,包括:

  • 哪些内存需要回收?
    • 对象是否回收的两种经典算法
      - 引用计数法
      - 可达性分析算法
  • 什么时候回收?
    • 新生代回收机机制——MinorGC
    • 老年代回收机制——FullGC
  • 如何回收?
    • 3种经典垃圾回收算法
      - 标记清楚算法
      - 复制算法
      - 标记整理算法
    • 分代收集算法
    • Stop-the-world
    • 7种垃圾收集器
      - 1 标记-清除收集器
      - 2 标记-压缩收集器
      - 3 复制收集器
      - 4 增量收集器
      - 5 分代收集器
      - 6 并行收集器
      - 7 跟踪收集器

下面我们就依次来看下

三、哪些内存需要回收?

(一)、引用计数法:判断对象的引用数量

引用计数法是通过判断对象的引用数量来决定对象是否可以被回收。

引用计数算法是垃圾收集器的早期策略。在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象的实例引用计数设置为1。当任何其他变量被复制为这个对象的引用时,对象实例的引用计数加1,(a=b,则b引用的对象实例的计数器加1),当当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用减1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减1。任何引用计数为0的对象实例可以被当做垃圾收集。

引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。如下面的程序和示意图所示,对象objA和objB之间的引用计数永远不可能为 0,那么这两个对象就永远不能被回收。

img

用计数法.png

如上图对象A和对象B相互引用,导致他们的引用计数器都不为0,那么垃圾收集器就永远不会回收他们。

(二)、可达性分析算法:判断对象的引用链是否可达

可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。

可达性分析算法是从离散数学中的图论引入的,程序吧所有的引用关系看做一张图,通过一系列的名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连(用图论的话来说就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。在Java中

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的,如下图所示。

img

可达性分析算法.png

在Java中,可作为 GC Root 的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中Native引用的对象

四、垃圾收集算法

(一)、标记清除算法

标记-清楚算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象进行标记,玩标记完毕后,再扫描整个空间未被标记的对象进行回收,如下所示:

img

标记-清除算法的主要不足有两个:

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

img

标记-清除算法的不足.png

(二)、复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。该算法如下:

img

复制算法.png

事实上,现在商用的虚拟机都采用这种算法来回收新生代。因为研究发现,新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。实践中会将新生代分为一块较大的Eden空间和两块较小的Surivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着对象一次地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1:1。也就是每次新声代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被”浪费”。

img

内存模型.png

(三)、标记整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应该被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记整理算法的标记过程类似标记清楚算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代),其原来如下:

img

标记整理算法原理.png

标记整理算法与标记清除算法最显著的区别是:标记清除算法不进行对象的移动,并且仅对不存活对象进行处理;而标记整理算法会将所有的存活对象移动到一端,并对不存货对象进行处理,因此其不会产生内存碎片。标记整理算法如下:

img

标记整理算法移动.png

(四)、分代收集算法

对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对昂也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况是不一样的),故而不同声明周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高JVM的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就采用标记清楚算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久带三个模块。如下所示:

img

分代收集算法.png

1、新生代(Young Generation)

新生代的目标是尽可能快速收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代内存按照8:1:1的比例分成一个eden区和两个Survivor(s0,s1)区,大部分对象在Eden区中生成。在进行垃圾回收时,先将eden区存活对象复制到s0区,然后清空eden区,当这个s0也满了时,则将eden区和s0区存对象复制到s1区,然后清空eden和s0。此时s0区是空的,然后交换s0区和s1区的角色(即下次垃圾回收时会扫描Eden区和s1区),即保持s0区为空,如此往返。特别地,当s1区也不足以存放eden区和s0区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫MinorGC,MinorGC发生频率比较高,不一定等到Eden区满了才触发。

2、老年代(Old Generation)

老年代存放的都是一些生命周期长的对象,就像上面的所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多,大概比例是(1:2),当老年代满时会触发Major GC/Full GC,老年代对象存活时间比较长,因此Major GC/Full GC发生的频率比较低。

3、永久代(Permanent Generation)

永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、GCLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。

4、小结

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。垃圾回收有两种类型,Minor GC 和Major GC/Full GC。

  • Minor GC:对新生代进行回收,不会影响到老年代。因为新生代的Java对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
  • Major GC/Full GC:对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代要被写满、永久代被写满和System.gc()被显式调用等。

五、Stop the World事件

所有的垃圾收集都是”Stop the World”事件,因为所有的应用线程都会停下来直到操作完成(所以叫”Stop the World”)

  • 所有的Minor GC 都会触发”Stop the World”事件,停止应用程序的线程,对于绝大多数程序,停顿导致的延迟都是可以忽略不计的。因为真相就是大部分的eden区域的对象都是”临时的(short-lived)”对象,当然你可以可以理解垃圾。所以Minor GC执行的非常快,所以Minor GC的”Stop the World”对大多数应用是可以没有什么影响的。
  • 由于Major GC/Full GC会检查所有存活的对象,因此会花费更长的时间。应该尽量减少Major GC/Full GC。因为Major GC/Full GC会在垃圾回收期间让你的应用反应迟钝,所以如果你有一个需要快速响应的应用发生了多次Major GC,你会感觉你的应用很卡。

垃圾回收时间取决垃圾回收策略。这就是为什么有必要去监控垃圾收集和对垃圾收集进行调优。并且事实上,GC优化很多时候就是指减少Stop-the-world发生的时间,从而使系统具有高吞吐、低停顿的特点。

六、垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示他们可以搭配使用。

img

垃圾收集器.png

Serial

  • Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • Serial Old 收集器(标记-整理算法):老年代单线程收集器,Serial收集器的老年代版本;

ParNew

  • ParNew收集器(复制算法):新生代并并行收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。
  • Parallel Scavenge搜集器(复制算法):新生代并行收集器,最求高吞吐量,高效利用CPU。吞吐量=用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用对等交互相应要求不高的场景
  • Parallel Old收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间未目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器(标记-整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于”标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前6种收集器回收的范围仅限于新生代或者老年代。

七、内存分配与回收策略

Java技术系统中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。一般而言,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB),将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中,总的来说,内存分配规则并不是一层不变的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

1、对象分配

对象优先在eden分配,当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的eden空间和两块较小的survivor空间,每次使用eden和其中一块survivor。当进行垃圾回收时,将eden和survivor中还存活的对象一次性的复制到另一块survivor空间上,最后处理掉eden和刚才的survivor空间。当survivor空间不够时,需要依赖老年代进行分配担保。

2、大对象

大对象直接进入老年代。所谓的大对象是值,需要大量的连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

3、新生代–>老年代

长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认是15)的Minor GC后,就会晋升到老年代中。

4、动态对象年龄判定

为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

PS:
需要注意的是,Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用对象的占据的内存空间。也就是说,垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。

转载地址:[隔壁老李头](

0
青年IT男

个人从事金融行业,就职过易极付、思建科技等重庆一流技术团队,目前就职于某网约车平台负责整个支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。

评论已关闭。

This site is protected by wp-copyrightpro.com