Java虚拟机(JVM)

Posted 头发都哪去了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java虚拟机(JVM)相关的知识,希望对你有一定的参考价值。

JVM概念

虚拟机简介

JVM(Java Virtual Machine的简称。意为Java虚拟机。

虚拟机:
指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。
常见的虚拟机:
JVM、VMwave、Virtual Box

JVM和其他两个虚拟机的区别

  1. VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器。
  2. JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了裁剪。

JVM 是一台被定制过的现实当中并不存在的计算机。

JVM 布局(HotSpot)(JDK 1.8)

在这里插入图片描述

所有的对象都是存放在此区域的,此区域也是 JVM 中最大的一块区域。
JVM 的垃圾回收就是针对此区域的。

JVM 栈(Java 虚拟机栈)

  1. 局部变量表:8大基础数据类型,对象的引用。
  2. 操作栈:每个方法都会对应一个操作栈。
  3. 动态连接:执行常量池的方法引用。
  4. 方法返回址:PC 寄存器的地址。

本地方法栈

它与 JVM 栈比较类似,只不过 JVM 栈是给 Java 和 JVM 使用,而本地方法栈是为本地方法(c/c++)服务的。

程序计数器

用来记录线程执行的行号。

元空间(JDK 1.8)

JDK 1.7 此区域为 方法区(使用永久代来实现),属于 JVM ,存放运行时常量信息、字符串常量池、类的元信息。
JDK 1.8 元空间 ,属于JVM,但是在 本地内存上执行,并且将字符串常量池移动到堆。

小结

在这里插入图片描述

堆划分:

  1. 新生代:第一次创建的对象都会分配到此区域
  2. 经历了一定的垃圾回收之后,依然存活的对象会移动到老年代;大对象在创建时候也会直接进入老年代。

PS:HotSpot 默认的执行次数是 15,经历 15 GC 就会从新生代转移到老年代。

新生代 区域划分:

  1. Eden:80%内存。
  2. S0:10%内存。
  3. S1:10%内存。

新生代内存的利用率就可以达到 90%

为什么大对象会直接进入老年代?
核心原因是因为大对象的初始化比较耗时,如果频繁的创建和销毁会带来一定的性能开销,因此最好的实现方式是将它存入 GC 频率更低的老年代。

JVM 参数调优

-Xmx:堆最大容量的设置
-Xms:堆最小容量的设置

Tip:通常情况下,可以将 Xmx 和 Xms 的大小设置相同,这样可以防止堆扩容所造成的抖动。

JVM 调优的时候可以设置的参数类型:
-X:非标准的参数设置,它只能针对特殊 Hotspot 生效。
-XX:标准的参数设置,它只能针对所有 Hotspot 都生效。
-D:设置应用程序的参数。

JVM类加载机制(Class Loading)

在这里插入图片描述

加载:

  1. 根据类路径全名加载二进制流。
  2. 将静态的存储结构转换成运行时的数据结构。
  3. 在内存中生成一个此类的方法入口。

验证:

  1. 文件格式进行验证
  2. 字节码校验

准备:
将类中的静态变量在在内存中进行分配
比如:public static int count = 123;
注意:此时只会在内存中生成一个 count = 0的变量。(对变量类型的初始化)

解析:
初始化 final 修饰的常量

初始化 :
此步骤开始将执行权从 JVM 转移到自己写的程序,开始执行构造函数

Tips:
字面量:字符串,final修饰变量,基本类型数据的值
符号引用:类、方法的完全限定名(全路径名称)
直接引用:将符号引用加载到内存中,根据引用指向内存中的对象。

JVM双亲委派模型

站在 Java 虚拟机的⻆度来看,只存在两种不同的类加载器:⼀种是启动类加载器(Bootstrap ClassLoader),这个类加载器使⽤ C++ 语⾔实现,是虚拟机⾃身的⼀部分;另外⼀种就是其他所有的类加载器,这些类加载器都由Java语⾔实现,独⽴存在于虚拟机外部,并且全都继承⾃抽象类<java.lang.ClassLoader>。
站在 Java 开发⼈员的⻆度来看,类加载器就应当划分得更细致⼀ 些。⾃ JDK 1.2 以来,Java ⼀直保持着三层类加载器、双亲委派的类加载架构器。

什么是双亲委派模型
如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆ 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。
简单的来说:当加载一个类的时候,这个类不会直接加载,而是将这个加载任务交给父类。当找不到父类的时候,自己才尝试去加载。

在这里插入图片描述

双亲委派模型优点:
唯一性:父类执行加载一次
安全性:类会往上找,而上层的类是系统提供的类,避免加载自定义的类,从而一定程度上保证了安全性

破坏双亲委派模型(3次):

  1. JDK 1.2 提供的双亲委派模型,为了兼容老代码,因此在 JDK 1.2 的时候已经出现了破坏双亲委派模型的场景。
  2. 双亲委派模型自身的缺点而导致的,比如在父类当中要调用子类的方法是无法实现的。
  3. 人们对于热更新的追求,导致了双亲委派模型的又一次破坏。

垃圾回收

判别死亡对象(垃圾)

Java堆中存放着几乎所有的对象实例,垃圾回收器在对堆进行垃圾回收前,首先要判断这些对象哪些还存活,哪些已经"死去"。

引用计数器算法

给每个对象创建一个计数器,当有程序引用此类的时候计数器 +1 ,引用失效时候计数器 -1 ,当计数器为 0 时,则表示此对象没有使用,就可以将它判定为死亡对象,等待垃圾回收器的回收。
但是,在主流的JVM中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题

HotSpot 默认的垃圾回收器判定死亡对象的方法不是引用计数器算法。

可达性分析算法(HotSpot判定死亡对象默认使用的算法)

此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的。以下图为例:
在这里插入图片描述

对象 Object5 - Object7 之间虽然彼此还有关联,但是它们到 GC Roots 是不可达的,因此他们会被判定为可回收对象。

在Java语言中,可作为GC Roots的对象包含下面几种:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(Native方法)引用的对象

Tips:

  1. 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"Object obj = new Object()"这类 的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
  2. 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在 系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来 实现软引用。
  3. 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的 对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后提供了 WeakReference 类来实现弱引用。
  4. 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在 JDK1.2 之后,提供了 PhantomReference 类来实现虚引用

回收方法区

方法区(永久代)的垃圾回收主要收集两部分内容 : 废弃常量和无用的类。

回收废弃常量和回收Java堆中的对象十分类似。以常量池中字面量(直接量)的回收为例,假如一个字符串"abc"已经进入了常量池中,但是当前系统没有任何一个String对象引用常量池的"abc"常量,也没有在其他地方引用这个字面量,如果此时发生GC并且有必要的话,这个"abc"常量会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
判定一个类是否是"无用类"则相对复杂很多。类需要同时满足下面三个条件才会被算是"无用的类" :

  1. 该类所有实例都已经被回收(即在Java堆中不存在任何该类的实例)。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的Class对象没有在任何其他地方被引用,无法在任何地方通过反射访问该类的方法。

JVM可以对同时满足上述3个条件的无用类进行回收,也仅仅是"可以"而不是必然。在大量使用反射、动态代理等场景都需要JVM具备类卸载的功能来防止永久代的溢出。

垃圾回收算法

标记-清除算法

"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象(标记过程见3.1.2章节)。后续的收集算法都是基于这种思路并对其不足加以改进而已。
在这里插入图片描述
"标记-清除"算法的不足主要有两个 :

  1. 效率问题 : 标记和清除这两个过程的效率都不高。
  2. 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。

复制算法(新生代回收算法)

"复制"算法是为了解决"标记-清理"的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。算法的执行流程如下图 :
在这里插入图片描述
此算法虽然大大提升了性能,但是内存的利用率却大大降低了。

标记-整理算法(老生代回收算法)

复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。
针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。流程图如下:
在这里插入图片描述
不会产生内存碎片。

分代收集算法

当前JVM垃圾收集都采用的是"分代收集(Generational Collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。

一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法(多用"标记-整理"算法)。

Minor GC和Full GC,这两种GC有什么不一样

  1. Minor GC又称为新生代GC : 指的是发生在新生代的垃圾收集。因为Java对象大多都具备朝生夕灭的特性,因此Minor GC(采用复制算法)非常频繁,一般回收速度也比较快。
  2. Full GC 又称为 老年代GC或者Major GC : 指发生在老年代的垃圾收集。出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器中就有直接进行Full GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

垃圾回收器(垃圾收集器)

在这里插入图片描述

Serial收集器(新生代收集器,串行GC)

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
Serial:单线程串行的垃圾回收器
在这里插入图片描述

特性:
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束(Stop The World)。
应用场景:
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
优势:
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 实际上到现在为止 : 它依然是虚拟机运行在Client模式下的默认新生代收集器

ParNew收集器(新生代收集器,并行GC)

它是 Serial 的多线程版本。
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
在这里插入图片描述

特性 :
Serial收集器的多线程版本
应用场景 :
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
对比分析:
ParNew 收集器在单 CPU 的环境中绝对不会有比Serial收集器更好的效果,但是,随着可以使用的 CPU 的数量的增加,它对于 GC 时系统资源的有效利用还是很有好处的。

Parallel Scavenge收集器(新生代收集器,并行GC)

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
Parallel Scavenge收集器使用两个参数控制吞吐量:

  1. XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间。
  2. XX:GCRatio 直接设置吞吐量的大小。

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量
和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。

应用场景:
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器 VS CMS等收集器: Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。

Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。

Serial Old收集器(老年代收集器,串行GC)

特性:
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
应用场景:

  1. Client 模式:
    Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
  2. Server 模式:
    如果在 Server 模式下,那么它主要还有两大用途:一种用途是在 JDK 1.5 以及之前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
    在这里插入图片描述

Parallel Old收集器(老年代收集器,并行GC)

特性:
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。
应用场景:
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

在这里插入图片描述

CMS收集器(老年代收集器,并发GC)

在这里插入图片描述
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些

整个过程分为4个步骤:

  1. 初始标记(CMS initial mark)
    初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  2. 并发标记(CMS concurrent mark)
    并发标记阶段就是进行GC Roots Tracing的过程。
  3. 重新标记(CMS remark)
    重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的
    时间短,仍然需要“Stop The World”。
  4. 并发清除(CMS concurrent sweep)
    并发清除阶段会清除对象。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

优点:
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。

缺点:

  1. CMS收集器对CPU资源非常敏感
  2. CMS收集器无法处理浮动垃圾
  3. CMS收集器会产生大量空间碎片

G1收集器(化整为零思想)

(JDK 11 时成为默认的垃圾回收器)

G1(Garbage First)垃圾回收器是用在 heap memory 很大的情况下,把 heap 划分为很多很多的 region 块,然后并行的对其进行垃圾回收。
G1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
G1垃圾回收器回收 region 的时候基本不会STW,而是基于 most garbage优先回收(整体来看是基于"标记-整理"算法,从局部(两个region之间)基于"复制"算法) 的策略来对region进行垃圾回收的。

无论如何,G1收集器采用的算法都意味着一个region有可能属于Eden,Survivor或者Tenured内存区域。图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象。
在这里插入图片描述

Java内存模型(JMM)

JVM定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C/C++直接使用物理硬件和操作系统的内存模型,因此,会由于不同平台下的内存模型的差异,有可能导致程序在一套平台上并发完全正常,而在另一套平台上并发访问经常出错。

主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但不包括局部变量和方法参数,因为后两者是线程私有的,不会被线程共享。

Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。线程、主内存、工作内存三者的交互关系如下所示 :
在这里插入图片描述

内存间交互操作

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存中拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了如下8种操作来完成。JVM实现时必须保证下面提及的每一种操作的原子的、不可再分的。

  • lock(锁定) : 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
  • unlock(解锁) : 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取) : 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
  • load(载入) : 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用) : 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎。
  • assign(赋值) : 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
  • store(存储) : 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便后续的 write 操作使用。
  • write(写入) : 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

Java内存模型的三大特性

  • 原子性 : 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和read。大致可以认为,基本数据类型的访问读写是具备原子性的。如若需要更大范围的原子性,需要synchronized关键字约束。
  • 可见性 : 可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile、synchronized、final三个关键字可以实现可见性。
  • 有序性 : 如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的操作都是无序的。前半句是指"线程内表现为串行",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象。

volatile型变量的特殊规则

关键字 volatile 可以说是JVM提供的最轻量级的同步机制,但是它并不容易完全被正确理解和使用。 JVM 内存模型对 volatile 专门定义了一些特殊的访问规则。
当一个变量定义为 volatile 之后,它将具备两种特性。
第一:保证此变量对所有线程的可见性。
第二:使用volatile变量的语义是禁止指令重排序。

以上是关于Java虚拟机(JVM)的主要内容,如果未能解决你的问题,请参考以下文章

认识JVM虚拟机

Java虚拟机|JVM适合初学者入门

Java虚拟机|JVM适合初学者入门

JVM(java 虚拟机)

Java虚拟机(JVM)-- JVM的体系结构

Java虚拟机JVM组成