JVM虚拟机

简单聊聊 JVM 内存分配与回收

  • Java 的⾃动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java ⾃动内存管理最核⼼的功能是堆内存中对象的分配与回收。

  • Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新⽣代和⽼年代:再细致⼀点有:Eden 空间、From Survivor、To Survivor 空间等。进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存。

  • 堆空间的基本结构:

堆内存划分

年轻代 (Young Generation)

年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1

  • 大多数新创建的对象都位于 Eden 内存空间中
  • 当 Eden 空间被对象填充时,执行Minor GC,并将所有幸存者对象移动到一个幸存者空间中
  • Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的
  • 经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代

老年代(Old Generation)

  • 旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝

元空间

不管是 JDK8 之前的永久代,还是 JDK8 及以后的元空间,都可以看作是 Java 虚拟机规范中方法区的实现。 虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分开

堆内存中对象的分配的基本策略

对象优先在 eden 区分配

⽬前主流的垃圾收集器都会采⽤分代回收算法,因此需要将堆内存分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

⼤多数情况下,对象在新⽣代中 eden 区分配。当 eden 区没有⾜够空间进⾏分配时,虚拟机将发起⼀次 Minor GC.下⾯我们来进⾏实际测试以下。


⼤对象直接进⼊⽼年代

  • ⼤对象就是需要⼤量连续内存空间的对象(⽐如:字符串、数组)。
  • 为什么要这样呢?
  • 为了避免为⼤对象分配内存时由于分配担保机制带来的复制⽽降低效率。

⻓期存活的对象将进⼊⽼年代

  • 既然虚拟机采⽤了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新⽣代,哪些对象应放在⽼年代中。为了做到这⼀点,虚拟机给每个对象⼀个对象年龄(Age)计数器。

  • 如果对象在 Eden 出⽣并经过第⼀次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过⼀次 MinorGC,年龄就增加 1 岁,当它的年龄增加到⼀定程度(默认为 15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

动态对象年龄判定

⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到⼀定程度(默认为 15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

主要进⾏ gc 的区域

总结
针对 HotSpot VM 的实现,它⾥⾯的 GC 其实准确分类只有两⼤种:

部分收集 (Partial GC):

  • 新⽣代收集(Minor GC / Young GC):只对新⽣代进⾏垃圾收集;
  • ⽼年代收集(Major GC / Old GC):只对⽼年代进⾏垃圾收集。需要注意的是 Major GC 在
  • 有的语境中也⽤于指代整堆收集;
  • 混合收集(Mixed GC):对整个新⽣代和部分⽼年代进⾏垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和⽅法区。

如何判断对象是否死亡?(两种⽅法)

  • 堆中⼏乎放着所有的对象实例,对堆垃圾回收前的第⼀步就是要判断哪些对象已经死亡(即不能再被任何途径使⽤的对象)。

引⽤计数法

  • 给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它,计数器就加1;当引⽤失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使⽤的。

可达性分析算法

  • 这个算法的基本思想就是通过⼀系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所⾛过的路径称为引⽤链,当⼀个对象到 GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的。

简单的介绍⼀下强引⽤,软引⽤,弱引⽤,虚引⽤

  • ⽆论是通过引⽤计数法判断对象引⽤数量,还是通过可达性分析法判断对象的引⽤链是否可达,判定对象的存活都与“引⽤”有关。

  • JDK1.2之前,Java中引⽤的定义很传统:如果reference类型的数据存储的数值代表的是另⼀块内存的起始地址,就称这块内存代表⼀个引⽤。

  • JDK1.2以后,Java对引⽤的概念进⾏了扩充,将引⽤分为强引⽤、软引⽤、弱引⽤、虚引⽤四种(引⽤强度逐渐减弱)

强引⽤(StrongReference)

  • 以前我们使⽤的⼤部分引⽤实际上都是强引⽤,这是使⽤最普遍的引⽤。如果⼀个对象具有强引⽤,那就类似于必不可少的⽣活⽤品,垃圾回收器绝不会回收它。当内存空 间不⾜,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终⽌,也不会靠随意回收具有强引⽤的对象来解决内存不⾜问题。

软引⽤(SoftReference)

如果⼀个对象只具有软引⽤,那就类似于可有可⽆的⽣活⽤品。如果内存空间⾜够,垃圾回收器就不会回收它,如果内存空间不⾜了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使⽤。软引⽤可⽤来实现内存敏感的⾼速缓存。

软引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果软引⽤所引⽤的对象被垃圾回收,JAVA虚拟机就会把这个软引⽤加⼊到与之关联的引⽤队列中。

弱引⽤(WeakReference)

如果⼀个对象只具有弱引⽤,那就类似于可有可⽆的⽣活⽤品。弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。 不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。

弱引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃圾回收,Java虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中。

虚引⽤(PhantomReference)

"虚引⽤"顾名思义,就是形同虚设,与其他⼏种引⽤都不同,虚引⽤并不会决定对象的⽣命周期。如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被垃圾回收。

虚引⽤主要用来跟踪对象被垃圾回收的活动。

虚引⽤与软引⽤和弱引⽤的⼀个区别在于: 虚引⽤必须和引⽤队列(ReferenceQueue)联合使⽤。当垃 圾回收器准备回收⼀个对象时,如果发现它还有虚引⽤,就会在回收对象的内存之前,把这个虚引⽤加⼊到与之关联的引⽤队列中。程序可以通过判断引⽤队列中是 否已经加⼊了虚引⽤,来了解被引⽤的对象是否将要被垃圾回收。程序如果发现某个虚引⽤已经被加⼊到引⽤队列,那么就可以在所引⽤的对象的内存被回收之前采取必要的⾏动。

特别注意,在程序设计中⼀般很少使⽤弱引⽤与虚引⽤,使⽤软引⽤的情况较多,这是因为软引⽤可以加速JVM对垃圾内存的回收速度,可以维护系统的运⾏安全,防⽌内存溢出(OutOfMemory)等问题的产⽣。

如何判断⼀个常量是废弃常量?

运⾏时常量池主要回收的是废弃的常量。那么,我们如何判断⼀个常量是废弃常量呢?

假如在常量池中存在字符串 "abc",如果当前没有任何String对象引⽤该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发⽣内存回收的话⽽且有必要的话,"abc" 就会被系统清理出常量池。

如何判断⼀个类是⽆⽤的类?

  • ⽅法区主要回收的是⽆⽤的类,那么如何判断⼀个类是⽆⽤的类的呢?
    判定⼀个常量是否是“废弃常量”⽐较简单,⽽要判定⼀个类是否是“⽆⽤的类”的条件则相对苛刻许多。类需要同时满⾜下⾯ 3 个条件才能算是 “⽆⽤的类” :
    • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    • 加载该类的 ClassLoader 已经被回收。
    • 该类对应的 java.lang.Class 对象没有在任何地⽅被引⽤,⽆法在任何地⽅通过反射访问该类的⽅法。
      虚拟机可以对满⾜上述 3 个条件的⽆⽤类进⾏回收,这⾥说的仅仅是“可以”,⽽并不是和对象⼀样不使⽤了就会必然被回收。

虚拟机可以对满⾜上述 3 个条件的⽆⽤类进⾏回收,这⾥说的仅仅是“可以”,⽽并不是和对象⼀样不使⽤了就会必然被回收。

垃圾收集有哪些算法,各⾃的特点?

标记-清除算法

该算法分为“标记”和“清除”阶段:⾸先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不⾜进⾏改进得到。这种垃圾收集算法会带来两个明显的问题:

  • 效率问题
  • 空间问题(标记清除后会产⽣⼤量不连续的碎⽚)

复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为⼤⼩相同的两块,每次使⽤其中的⼀块。当这⼀块的内存使⽤完后,就将还存活的对象复制到另⼀块去,然后再把使⽤的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进⾏回收。

标记-整理算法

根据⽼年代的特点提出的⼀种标记算法,标记过程仍然与“标记-清除”算法⼀样,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。

分代收集算法

当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进⾏垃圾收集。

  • 延伸⾯试问题: HotSpot 为什么要分为新⽣代和⽼年代?
    根据上⾯的对分代收集算法的介绍回答。

HotSpot 为什么要分为新⽣代和⽼年代?

主要是为了提升 GC 效率。上⾯提到的分代收集算法已经很好的解释了这个问题。

常⻅的垃圾回收器有那些?

如果说收集算法是内存回收的⽅法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进⾏⽐较,但并⾮要挑选出⼀个最好的收集器。因为知道现在为⽌还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应⽤场景选择适合⾃⼰的垃圾收集器。试想⼀下:如果有⼀种四海之内、任何场景下都适⽤的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。

Serial 收集器

  • Serial(串⾏)收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( "Stop The World" ),直到它收集结束。

  • 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

  • 虚拟机的设计者们当然知道 Stop The World 带来的不良⽤户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。

  • 但是 Serial 收集器有没有优于其他垃圾收集器的地⽅呢?当然有,它简单⽽⾼效(与其他收集器的单线程相⽐)。Serial 收集器由于没有线程交互的开销,⾃然可以获得很⾼的单线程收集效率。Serial 收集器对于运⾏在 Client 模式下的虚拟机来说是个不错的选择。

ParNew 收集器

  • ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全⼀样。

  • 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。

  • 它是许多运⾏在 Server 模式下的虚拟机的⾸要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后⾯会介绍到)配合⼯作。

并⾏和并发概念补充:

  • 并⾏(Parallel) :指多条垃圾收集线程并⾏⼯作,但此时⽤户线程仍然处于等待状态。
  • 并发(Concurrent):指⽤户线程与垃圾收集线程同时执⾏(但不⼀定是并⾏,可能会交替执⾏),⽤户程序在继续运⾏,⽽垃圾收集器运⾏在另⼀个 CPU 上。

Parallel Scavenge 收集器

  • Parallel Scavenge 收集器也是使⽤复制算法的多线程收集器,它看上去⼏乎和 ParNew 都⼀样。 那么它有什么特别之处呢?
    • 新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
-XX:+UseParallelGC
 
    使⽤ Parallel 收集器+ ⽼年代串⾏
 
-XX:+UseParallelOldGC
 
    使⽤ Parallel 收集器+ ⽼年代并⾏
  • Parallel Scavenge 收集器关注点是吞吐量(⾼效率的利⽤ CPU)。CMS 等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)。所谓吞吐量就是 CPU 中⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值。 Parallel Scavenge 收集器提供了很多参数供⽤户找到最合适的停顿时间或最⼤吞吐量,如果对于收集器运作不太了解,⼿⼯优化存在困难的时候,使⽤ Parallel Scavenge 收集器配合⾃适应调节策略,把内存管理优化交给虚拟机去完成也是⼀个不错的选择。

JDK1.8 默认使⽤的是Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC参数,则默认指定了-XX:+UseParallelOldGC,可以使⽤-XX:-UseParallelOldGC来禁⽤该功能

Serial Old 收集器

  • Serial 收集器的⽼年代版本,它同样是⼀个单线程收集器。它主要有两⼤⽤途:⼀种⽤途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使⽤,另⼀种⽤途是作为 CMS 收集器的后备⽅案。

Parallel Old 收集器

Parallel Scavenge 收集器的⽼年代版本。使⽤多线程和“标记-整理”算法。在注重吞吐量以及CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

CMS 收集器

  • CMS(Concurrent Mark Sweep)收集器是⼀种以获取最短回收停顿时间为⽬标的收集器。它⾮常符合在注重⽤户体验的应⽤上使⽤。

  • CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。

  • 从名字中的Mark Sweep这两个词可以看出,CMS 收集器是⼀种 “标记-清除”算法实现的,它的运作过程相⽐于前⾯⼏种垃圾收集器来说更加复杂⼀些。整个过程分为四个步骤:

    • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
    • 并发标记: 同时开启 GC 和⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以 GC 线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。
    • 重新标记: 重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,
      远远⽐并发标记阶段时间短
    • 并发清除: 开启⽤户线程,同时 GC 线程开始对未标记的区域做清扫。

它的名字就可以看出它是⼀款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下⾯三个明显的缺点:

  • 对 CPU 资源敏感;
  • ⽆法处理浮动垃圾;
  • 它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。

G1 收集器

G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器. 以极⾼概率满⾜ GC 停顿时间要求的同时,还具备⾼吞吐量性能特征.

被视为 JDK1.7 中 HotSpot 虚拟机的⼀个重要进化特征。它具备⼀下特点:

  • 并⾏与并发:G1 能充分利⽤ CPU、多核环境下的硬件优势,使⽤多个 CPU(CPU 或者 CPU 核⼼)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执⾏的 GC 动作,G1 收集器仍然可以通过并发的⽅式让 java 程序继续执⾏。

  • 分代收集:虽然 G1 可以不需要其他收集器配合就能独⽴管理整个 GC 堆,但是还是保留了分代的概念。

  • 空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

  • 可预测的停顿:这是 G1 相对于 CMS 的另⼀个⼤优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为 M 毫秒的时间⽚段内。

G1 收集器的运作⼤致分为以下⼏个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收
    G1 收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最⼤的 Region(这也就是它的名字 Garbage-First 的由来)。这种使⽤ Region 划分内存空间以及有优先级的区域回收⽅式,保证了 G1 收集器在有限时间内可以尽可能⾼的收集效率(把内存化整为零)。

ZGC 收集器

与 CMS 中的 ParNew 和 G1 类似,ZGC 也采⽤标记-复制算法,不过 ZGC 对该算法做了重⼤改进。

posted @ 2021-07-05 21:19  BigMonster85  阅读(95)  评论(0)    收藏  举报