JVM虚拟机-基础知识汇总

  |   0 评论   |   0 浏览

JVM 虚拟机思维导图
JVM 虚拟机总结.png

Java 引用的四种状态

强引用

 用的最广。我们平时写代码时,new 一个 Object 存放在堆内存,然后用一个引用指向它,这就是强引用。
 如果一个对象具有强引用,那垃圾回收器绝不会回收它*。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用

 如果一个对象只具有软引用,则内存空间足够时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。(备注:如果内存不足,随时有可能被回收。)
 只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

弱引用

 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
 每次执行 GC 的时候,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象

虚引用

 “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃

Java 中的内存划分

JVM 运行内存划分.png

程序计数器:(线程私有)

 每个线程拥有一个程序计数器,在线程创建时创建, 指向下一条指令的地址,执行本地方法时,其值为 undefined,说的通俗一点,我们知道,Java 是支持多线程的,程序先去执行 A 线程,执行到一半,然后就去执行 B 线程,然后又跑回来接着执行 A 线程,那程序是怎么记住 A 线程已经执行到哪里了呢?这就需要程序计数器了。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都有一个独立的程序计数器,这块儿属于“线程私有”的内存。

Java 虚拟机栈:(线程私有)

 每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。局部变量表存放的是:编译期可知的基本数据类型、对象引用类型。
 每个方法被调用直到执行完成的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
在 Java 虚拟机规范中,对这个区域规定了两种异常情况:
  (1)如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现 StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
  (2)虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现 OOM

本地方法栈

(1)本地方法栈与 Java 虚拟机栈作用非常类似,其区别是:Java 虚拟机栈是为虚拟机执行 Java 方法服务的,而本地方法栈则为虚拟机执使用到的 Native 方法服务。
(2)Java 虚拟机没有对本地方法栈的使用和数据结构做强制规定,Sun HotSpot 虚拟机就把 Java 虚拟机栈和本地方法栈合二为一。
(3)本地方法栈也会抛出 StackOverFlowError 和 OutOfMemoryError。

Java 堆:即堆内存(线程共享)

(1)堆是 Java 虚拟机所管理的内存区域中最大的一块,Java 堆是被所有线程共享的内存区域,在 Java 虚拟机启动时创建,堆内存的唯一目的就是存放对象实例几乎所有的对象实例都在堆内存分配。
(2)堆是 GC 管理的主要区域,从垃圾回收的角度看,由于现在的垃圾收集器都是采用的分代收集算法,因此 Java 堆还可以初步细分为新生代和老年代。
(3)Java 虚拟机规定,堆可以处于物理上不连续的内存空间中,只要逻辑上连续的即可。在实现上既可以是固定的,也可以是可动态扩展的。如果在堆内存没有完成实例分配,并且堆大小也无法扩展,就会抛出 OutOfMemoryError 异常。

方法区:(线程共享)

(1)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
(2)Sun HotSpot 虚拟机把方法区叫做永久代(Permanent Generation),方法区中最终要的部分是运行时常量池。

运行时常量池

(1)运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时就会抛出 OutOfMemoryError 异常。

Java 对象在内存中的状态

可达的/可触及的

 Java 对象被创建后,如果被一个或多个变量引用,那就是可达的。即从根节点可以触及到这个对象。
 其实就是从根节点扫描,只要这个对象在引用链中,那就是可触及的。

可恢复的

 Java 对象不再被任何变量引用就进入了可恢复状态。
 在回收该对象之前,该对象的 finalize()方法进行资源清理。如果在 finalize()方法中重新让变量引用该对象,则该对象再次变为可达状态,否则该对象进入不可达状态

不可达的

 Java 对象不被任何变量引用,且系统在调用对象的 finalize()方法后依然没有使该对象变成可达状态(该对象依然没有被变量引用),那么该对象将变成不可达状态。
 当 Java 对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

判断对象死亡方法

引用计数算法

  • 概念: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。
  • 主流的 Java 虚拟机并没有选用引用计数算法来管理内存,其中最主要的原因是:它很难解决对象之间相互循环引用的问题。
  • 优点: 算法的实现简单,判定效率也高,大部分情况下是一个不错的算法。很多地方应用到它
  • 缺点: 引用和去引用伴随加法和减法,影响性能 致命的缺陷:对于循环引用的对象无法进行回收
  • 图解:
    WX201911011500332x.png

根搜索算法

  • 概念: 设立若干种根对象,当任何一个根对象(GC Root)到某一个对象均不可达时,则认为这个对象是可以被回收的
  • 根(GC Roots): 说到 GC roots(GC 根),在 Java 语言中,可以当做 GC roots 的对象有以下几种: 1、栈(栈帧中的本地变量表)中引用的对象。 2、方法区中的静态成员。 3、方法区中的常量引用的对象(全局变量) 4、本地方法栈中 JNI(一般说的 Native 方法)引用的对象
  • 图解: WX201910311524282x.png

垃圾回收算法

标记-清除(mark sweep)

  • 概念:标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象; 清除阶段:清除所有未被标记的对象
  • 缺点:标记和清除的过程效率不高(标记和清除都需要从头遍历到尾) 标记清除后会产生大量不连续的碎片
  • 图解:
    WX201910311455012x.png

复制算法(新生代)

  • 概念: 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象。
  • 优点: 这样使得每次都是对整个半区进行回收,内存分配时也就不用考虑内存碎片等情况 只要移动堆顶指针,按顺序分配内存即可,实现简单,运行效率高
  • 缺点:空间的浪费 从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行。 现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象 98% 都是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块比较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的空间会被浪费。 当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。整个过程如下图所示:
  • 图解:WX201910311456352x.png

标记整理(mark compact)(老年代)

  • 概念: 标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象 整理阶段:将将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间
  • 优点: 不会产生内存碎片。
  • 缺点: 在标记的基础之上还需要进行对象的移动,成本相对较高,效率也不高。 它们的区别如下:(> 表示前者要优于后者,= 表示两者效果一样) (1)效率:复制算法 > 标记/整理算法 > 标记/清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。 (2)内存整齐度:复制算法 = 标记/整理算法 > 标记/清除算法。 (3)内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法。 注 1:标记-整理算法不仅可以弥补标记-清除算法当中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。 注 2:可以看到标记/清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。 注 3:时间与空间不可兼得
  • 图解:WX201910311457412x.png

分代收集算法

  • 当前商业虚拟机的 GC 都是采用的“分代收集算法”,这并不是什么新的思想,只是根据对象的存活周期的不同将内存划分为几块儿。一般是把 Java 堆分为新生代和老年代:短命对象归为新生代,长命对象归为老年代。 存活率低:少量对象存活,适合复制算法:在新生代中,每次 GC 时都发现有大批对象死去,只有少量存活(新生代中 98% 的对象都是“朝生夕死”),那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成 GC。 存活率高:大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行 GC。 注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次 GC 都没有被回收掉而进入老年代

Java 堆内存划分

新生代

  • 新生代中的对象 98% 都是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块比较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的空间会被浪费。 当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。同时,长期存活的对象将进入老年代(虚拟机给每个对象定义一个年龄计数器)

老年代

  • 在新生代中经历了 N 次垃圾回收后仍然存活的对象就会被放到老年代中。而且大对象直接进入老年代

图解:

永久区(PermGen) 1.8 元空间(Metaspace)

WX201910311504092x.png

Minor GC 和 Full GC

Minor GC

  • Minor GC 是发生在新生代中的垃圾收集动作,采用的是复制算法。 对象在 Eden 和 From 区出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被 to 区所容纳,那么在使用复制算法时这些存活对象就会被复制到 to 区域,然后清理掉 Eden 区和 from 区,并将这些对象的年龄设置为 1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 +1,当对象的年龄达到某个值时(默认是 15 岁,可以通过参数 –XX:MaxTenuringThreshold 设置),这些对象就会成为老年代。 但这也是不一定的,对于一些较大的对象(即需要分配一块较大的连续内存空间)则是直接进入老年代

Full GC

  • Full GC 是发生在老年代的垃圾收集动作,采用的是标记-清除/整理算法。 老年代里的对象几乎都是在 Survivor 区熬过来的,不会那么容易死掉。因此 Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比做一次 Minor GC 的时间要长。 另外,如果采用的是标记-清除算法的话会产生许多碎片,此后如果需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC。

垃圾收集器

WX202003291552192x.png

新生代

Parallel Scanvenge

类似 ParNew,但更加关注吞吐量。目标是:达到一个可控制吞吐量的收集器。
停顿时间和吞吐量不可能同时调优。我们一方买希望停顿时间少,另外一方面希望吞吐量高,其实这是矛盾的。因为:在 GC 的时候,垃圾回收的工作总量是不变的,如果将停顿时间减少,那频率就会提高;既然频率提高了,说明就会频繁的进行 GC,那吞吐量就会减少,性能就会降低。
吞吐量:CPU 用于用户代码的时间/CPU 总消耗时间的比值,即 = 运行用户代码的时间/(运行用户代码时间 + 垃圾收集时间)。比如,虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%

ParNew

它是运行在 server 模式下的首选新生代收集器,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。CMS 收集器是一个被认为具有划时代意义的并发收集器,因此如果有一个垃圾收集器能和它一起搭配使用让其更加完美,那这个收集器必然也是一个不可或缺的部分了

serial

这个收集器是一个单线程的收集器,但它的单线程的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop-The-World:将用户正常工作的线程全部暂停掉),直到它收集结束

老年代

Serial old

Serial 收集器的老年代版本
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

Parallel old

Parallel Scavenge 收集器的老年代版本
注重程序的吞吐量

CMS

流程

WX202003291601062x.png

优点

并发收集,低停顿

缺陷
  1. 导致用户的执行速度降低。
  2. 无法处理浮动垃圾。因为它采用的是标记-清除算法。有可能有些垃圾在标记之后,需要等到下一次 GC 才会被回收。如果 CMS 运行期间无法满足程序需要,那么就会临时启用 Serial Old 收集器来重新进行老年代的收集。
  3. 由于采用的是标记-清除算法,那么就会产生大量的碎片。往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 full GC

G1

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

流程
  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的 Region 中创建对象,此阶段需要停顿线程(STW),但耗时很短。
  • 并发标记:从 GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。
  • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
特点
  • 空间整合,不会产生内存碎片
  • 可预测的停顿
参数设置
  • -XX:+UseG1GC
  • -XX:MaxGCPauseMillis

ZGC

SHENANDOAH

JVM 内存分配流程图解

WX201910311518272x.png


标题:JVM虚拟机-基础知识汇总
作者:疲惫的怪神明