JVM

JVM基础

什么是JVM?

  • 定义

    Java Virtual Machine Java程序的运行环境(Java二进制字节码的运行环境)

  • 好处

    1. 一次编译,到处运行
    2. 自动内存管理,垃圾回收功能
    3. 数组下表越界检查
    4. 多态
  • 比较JVM、JRE、JDK

    • JVM(Java Virtual Machine),Java虚拟机
    • JRE(Java Runtime Environment),Java运行环境,包含了JVM和Java的核心类库(Java API)
    • JDK(Java Development Kit)称为Java开发工具,包含了JRE和开发工具

1

JVM 在 Java 中是如何工作的

1

  • JVM主要分为三个子系统
    • 类加载器(ClassLoader)
    • JVM运行时数据区(内存结构)
    • 执行引擎

JVM内存结构

程序计数器

  • 定义

JVM中的程序计数器(Program Counter Register)是一块较小的内存空间,它用来保存当前线程下一条要执行的指令的地址。每个线程都有自己独立的程序计数器,它是线程私有的,生命周期与线程相同。程序计数器是JVM中的一种轻量级的内存区域,因为它不会发生内存溢出(OutOfMemoryError)的情况。

程序计数器的作用在于线程切换后能够恢复到正确的执行位置,也就是下一条需要执行的指令地址。

因为在Java虚拟机的多线程环境下,为了支持线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,否则就会出现线程切换后执行位置混乱的问题。

程序计数器也是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError(内存溢出)情况的区域。因为程序计数器是线程私有的,所以它所占用的内存空间非常小,一般不会导致内存溢出的问题。

程序计数器是JVM中的一种非常重要的内存区域,它是实现Java虚拟机字节码解释器的必要组成部分。

  • 小结
    • 程序计数器
      • 作用:保存当前线程下一条要执行的指令的地址
      • 特点:
        线程私有
        不存在内存溢出

虚拟机栈

  • 定义

Java虚拟机栈(Java Virtual Machine Stacks)是Java虚拟机为每个线程分配的一块内存区域,用于存储线程的方法调用和局部变量等信息。 每个线程在运行时都有自己的Java虚拟机栈,线程开始时会创建一个新的栈帧(Stack Frame),用于存储该线程的方法调用信息。当方法调用完成后,该栈帧会被弹出,回到上一次方法调用的位置。每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

  • 小结
    • Java虚拟机栈
    1. 每个线程运行是所需的内存,就称为虚拟机栈
    2. 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时占用的内存
    3. 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

本地方法栈

  • 本地方法栈,我们先来理解一下什么叫本地方法
    • 本地方法是指由非Java语言编写的代码,如C或C++,并被编译为本地二进制代码。
  • 因为JAVA没法直接和操作系统底层交互,所以需要用到本地方法栈来调用本地的C或C++的方法
  • 例如Object类的源码中就有本地方法,用native关键字修饰本地方法
    • 本地方法只有函数声明,没有函数体,因为函数体是C或C++写的,通常是通过JNI(Java Native Interface)技术来实现的

  • 定义

JVM的堆(Heap)是Java虚拟机(JVM)在内存中用来存放对象的区域,是Java程序中最大的一块内存区域。JVM的堆被所有线程共享,在JVM启动时就已经被创建,并且一直存在于JVM的整个生命周期中。

堆可以被分成两部分:新生代(Young Generation)和老年代(Old Generation)。新生代又被进一步分为Eden空间、幸存区From空间和幸存区To空间。 新生代是用来存放新创建的对象的,其中大部分对象都很快就会被垃圾回收掉。当堆空间不足时,JVM会触发垃圾回收机制(GC),对新生代的对象进行清理。清理过程一般是将存活的对象移到老年代或幸存区,而其余的对象则被回收。

老年代是用来存放生命周期较长的对象的,这些对象一般是从新生代晋升而来,或者是本身就比较大的对象。老年代的对象存活时间较长,因此垃圾回收的频率比新生代低得多。

JVM堆的大小可以通过启动JVM时的参数进行调整,如-Xms和-Xmx参数分别控制堆的初始大小和最大大小。如果应用程序需要创建大量的对象,而堆空间不足,则会抛出OutOfMemoryError异常。

  • 小结
    • Heap堆
      • 通过new关键字创建的对象都会使用堆空间
    • 特点
      • 它是线程共享的,堆空间内的对象都需要考虑线程安全的问题
      • 有垃圾回收机制

方法区

  • 定义 在JVM中,方法区是一块用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据的内存区域,它是Java虚拟机规范中的一个概念。Java SE 7及之前版本中,方法区被称为永久代,但在Java SE 8之后的版本中,永久代被废弃了,被元空间所替代。

元空间是JVM在Java SE 8之后引入的一个新的概念,它与永久代类似,都是用于存储类信息、常量、静态变量、即时编译器编译后的代码等数据的内存区域,但元空间的实现方式与永久代有所不同。

与永久代不同的是,元空间使用的是本地内存(Native Memory),而不是虚拟机内存(堆内存),这样就避免了OutOfMemoryError错误,因为在使用本地内存时,可以动态地调整大小,而且可以使用操作系统的虚拟内存机制,使得Java应用程序不会被限制在固定的内存大小中。

此外,元空间还引入了一些新的概念和机制,例如MetaspaceSize、MaxMetaspaceSize、CompressedClassSpaceSize等,这些概念和机制都是为了更好地管理元空间的内存使用和性能。

  • 组成 1

JVM垃圾回收

如何判断对象可以回收

引用计数法

当一个对象被引用时,就当引用对象的值+1,当引用对象的值为0时,则说明该对象没有被引用,那么就可以被垃圾回收器回收

这个引用计数法听起来很不错,而且实现起来也非常的简单,可是它有一个弊端,如下图所示,当两个对象循环引用时,两个对象的计数都未1,就导致这两个对象都无法被释放 1

可达性分析算法

JVM垃圾回收机制的可达性分析算法,是一种基于引用的垃圾回收算法。其基本思想是通过一系列被称为"GC Roots"的根对象作为起点,寻找所有被根对象直接或间接引用的对象,将这些对象称为"可达对象",而没有被找到的对象则被视为"不可达对象",需要被回收。

形象一点理解就是我有一串葡萄,我把这串葡萄拿起来,连在根上的葡萄就是可达对象,而掉在盘子里的葡萄就是不可达对象,需要被回收

  • 在JVM中,有四种类型的GC Roots对象:
    1. 虚拟机栈中引用的对象:虚拟机栈是用于存储方法调用和执行的栈空间。当一个方法被调用时,会在栈中创建一个栈帧,用于存储该方法的局部变量、参数和返回值等信息。如果栈帧中包含对某个对象的引用,那么这个对象就被视为GC Roots对象。
    2. 方法区中类静态属性引用的对象:方法区是用于存储类信息、常量池、静态变量等信息的内存区域。当一个类被加载到方法区时,其中的静态属性会被分配在方法区中,如果这些静态属性中包含对某个对象的引用,那么这个对象就被视为GC Roots对象。
    3. 方法区中常量引用的对象:常量池是方法区的一部分,用于存储常量。如果常量池中包含对某个对象的引用,那么这个对象就被视为GC Roots对象。
    4. 本地方法栈中JNI引用的对象:JNI是Java Native Interface的缩写,用于在Java程序中调用本地方法(即由C或C++等语言编写的方法)。当本地方法被调用时,会在本地方法栈中创建一个栈帧,如果该栈帧中包含对某个对象的引用,那么这个对象就被视为GC Roots对象。
  • 可达性分析算法基于这些GC Roots对象,通过遍历所有的引用链,找到所有可达对象,将它们标记为存活对象,而没有被找到的对象则被视为不可达对象,需要被回收。
  • 可达性分析算法的主要优点是可以处理复杂的引用结构,例如循环引用、交叉引用等情况,能够识别出所有可达对象,从而准确地进行垃圾回收。但是,它也有一些缺点,例如需要耗费较多的时间进行垃圾回收、可能会出现漏标和误标等问题。为了解决这些问题,JVM中还采用了其他的垃圾回收算法,如标记-清除算法、复制算法、标记-整理算法等,以提高垃圾回收的效率和准确性。

引用类型

在Java中,对象不仅可以被正常引用,还可以被特殊的引用类型引用。这些引用类型决定了垃圾回收器如何对对象进行回收。

JVM中共有五种引用类型,它们分别是

  1. 强引用(Strong Reference):是最常见的引用类型,也是默认的引用类型。如果一个对象具有强引用,那么即使内存空间不足,垃圾回收器也不会回收它。只有当该对象的所有强引用都失效时,对象才会被回收
  2. 软引用(Soft Reference):是一种比强引用弱一些的引用类型。如果一个对象只具有软引用,那么当内存空间不足时,垃圾回收器可能会回收它。软引用通常用于实现内存敏感的缓存 可以配合引用队列来释放软引用自身
  3. 弱引用(Weak Reference):是一种比软引用更弱一些的引用类型。如果一个对象只具有弱银用,那么垃圾回收器在下一次运行时,无论内存空间是否足够,都会回收该对象。若引用通常用于实现在对象可用时进行操作的场景 可以配合引用队列来释放软引用自身
  4. 虚引用(Phantom Reference):是最弱的一种引用类型。如果一个对象只具有虚引用,那么在任何时候都可能被垃圾回收器回收。虚引用通常用于追踪对象被垃圾回收的状态 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
  5. 终结器引用(Final Reference):是一种特殊的弱引用类型,它只在对象被回收时被添加到引用队列中。当垃圾回收器准备回收一个对象时,会先执行对象的finallize()方法,如果finalize()方法中没有重新让对象与其他对象建立联系,那么这个对象就会被回收,并且它的Final引用会被加入到引用队列中。Final引用通常用于对象回收后的清理工作

分代垃圾回收

JVM(Java虚拟机)的分代垃圾回收是一种优化内存回收的技术。它利用对象的生命周期来将堆(heap)分为不同的区域,然后针对不同区域的特点采用不同的垃圾回收算法。 1

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄+1并且交换from和to
  • minor gc会引发stop the world(砸瓦鲁多!!),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15
    • Java中的对象头中确实分配了一定的字节用来记录对象的年龄,而这个字节的位数是4,因此其二进制最大值为1111,即十进制的15
  • 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,STW的时间更长

垃圾回收器

串行收集器(Serial收集器)

这是最简单的垃圾回收器,它通过一个单线程进行垃圾回收,因此它的优点是简单高效,但缺点是在大型应用程序中可能会出现停顿时间过长的问题。

串行收集器的缺点主要是单线程执行垃圾回收操作,不能充分利用多核CPU的计算能力,同时垃圾回收操作会阻塞应用程序的运行,可能会导致长时间的停顿。因此,在大型的、多线程的应用程序中,通常不适合使用串行收集器进行垃圾回收。

对应的VM参数:-XX:+UseSerialGC = Serial + SerialOld

下图中,其他CPU需要等待CPU 2执行完垃圾回收后,才能继续运行 1

吞吐量优先收集器(Parallel收集器)

JDK 1.8 默认采用的就是这种垃圾回收器

它是一种基于多线程并行执行的垃圾回收器,它的主要目标是提高应用程序的吞吐量,即在单位时间内处理更多的请求。

1

CMS收集器(响应时间优先)

  • 概述

CMS(Concurrent Mark Sweep)是一种垃圾回收算法,它的设计目标是在最短的停顿时间内回收垃圾。它通过在一个线程中进行垃圾回收并在应用程序线程中同时运行,从而减少停顿时间。下面详细介绍CMS垃圾收集器的工作原理和优缺点。

  • 工作流程

CMS垃圾收集器的工作可以分为以下几个阶段:

  1. 初始标记阶段(Initial Marking):在这个阶段中,CMS垃圾收集器会暂停所有应用程序线程,并且在内存中标记出所有被直接引用的对象。这个过程由一个线程来完成,因此它的停顿时间比较短。
  2. 并发标记阶段(Concurrent Marking):在这个阶段中,CMS垃圾收集器会在应用程序线程运行的同时标记出所有被间接引用的对象。这个过程由多个线程并发执行,因此它的停顿时间比较短。
  3. 重新标记阶段(Remark):在这个阶段中,CMS垃圾收集器会暂停所有应用程序线程,并且重新标记出在并发标记阶段中有变化的对象。这个过程由一个线程来完成,因此它的停顿时间比较短。
  4. 并发清除阶段(Concurrent Sweeping):在这个阶段中,CMS垃圾收集器会在应用程序线程运行的同时清除所有标记为垃圾的对象。这个过程由多个线程并发执行,因此它的停顿时间比较短。
  5. 最终标记阶段(Final Remark):在这个阶段中,CMS垃圾收集器会暂停所有应用程序线程,并且重新标记出在并发清除阶段中有变化的对象。这个过程由一个线程来完成,因此它的停顿时间比较短。 1
  • 优缺点

    • 优点
    1. 可以在最短的停顿时间内回收垃圾,不会出现长时间的停顿现象,因此适合响应时间比较敏感的应用程序。
    2. 采用并发的垃圾收集方式,能够与应用程序并发执行,不会阻塞应用程序的执行,因此对于需要保证应用程序响应性能的场景非常适用。
    • 缺点
    1. 对 CPU 的使用比较敏感,在高负载情况下可能会影响应用程序的性能。
    2. 由于并发清除阶段无法整理内存,可能会出现内存碎片化的问题,导致后续垃圾回收过程需要更多的时间。
    3. 在处理大量的垃圾时可能会导致应用程序的性能下降,因为需要占用一定的 CPU 资源。

JVM内存模型

很多人将Java内存结构和Java内存模型傻傻分不清,Java内存模型是Java Memory Model(JMM)的意思

简单地说,JMM定义了一套在多线程读写共享数据时(成员变量、数组),对数据的可见性、有序性和原子性的规则和保障

JVM面试题

简述一下JVM内存模型

JVM内存模型(JVM Memory Model,JMM)是Java虚拟机用来描述多线程程序中各个线程之间以及线程和内存之间的交互关系的规范。JMM定义了线程的工作内存和主内存之间的交互方式,并规定了在何时如何把工作内存中的数据同步回主内存,或者如何从主内存中读取数据到工作内存中。JMM的设计目的是为了保证在多线程程序中,无论运行在什么平台和处理器架构上,Java程序都能达到一致的内存访问效果。Java开发人员在编写多线程时必须遵守JMM规范来保证程序的正确性

说说堆和栈的区别
  1. 内存管理方式:栈采用静态内存分配,而堆采用动态内存分配。栈的大小在编译时就已经确定,而堆的大小可以在运行时动态调整
  2. 存储内容:栈主要用于存储基本数据类型(int、float、boolean等)以及对象的引用,而堆主要负责存储对象实例
  3. 内存分配方式:栈的内存分配方式是后进先出,而堆是随意分配、随意释放
  4. 存储效率:由于栈采用静态内存分配,因此它的存取速度比堆更快,而堆则因为需要进行动态内存分配和垃圾回收等操作,因此存取速度相对较慢
  5. 内存回收机制:栈的内存由JVM自动管理,当方法结束时,栈中的内存会自动被释放;而堆内存则由JVM的垃圾回收机制进行管理,当对象没有被引用时,垃圾回收机制会自动回收该对象占用的内存空间
Java内存结构
  • 程序计数器(Program Counter Register):每个线程都有一个程序计数器,它是线程私有的,用于记录当前线程正在执行的字节码指令的地址或者下一条指令的地址。如果执行的是native方法,则计数器为空。
  • Java虚拟机栈(Java Virtual Machine Stack):Java虚拟机栈也是线程私有的,用于存储每个县城的方法调用栈。每当一个方法被调用时,JVM都会为该方法分配一个栈帧,该栈帧包含了该方法的参数、局部变量以及操作数栈等信息。方法在返回时,JVM会弹出该栈帧。栈的大小可以是固定的,也可以是动态扩展的。栈的大小决定了方法调用的可达深度(递归调用次数,或者嵌套调用层数等,可以使用-Xss参数设置虚拟机栈的大小)。如果请求的栈深度大于最大可用深度,则会抛出StackOverFlowError。如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutOfMemoryError
  • 本地方法栈(Native Method Stack):本地方法栈也是线程私有的,与虚拟机栈类似,不同之处在于它为JVM执行本地方法(Native Method)服务
  • 方法区(Method Area):方法区用于存储已被加载的类的信息、常量池、静态变量、即时编译器编译后的代码等信息。方法区也是由垃圾回收器进行管理和回收的
  • 堆(Java Heap):堆是JVM中最大的内存区域,用于存储对象实例以及数组等数据。Java堆的内存空间是由垃圾回收器进行管理和回收的
说说对象分配规则
  • 对象优先分配在Eden区,如果Eden区没有足够的空间,虚拟机执行一次Minor GC
  • 大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区(from和to)之间发生大量的内存拷贝(新生代采用复制算法收集内存,避免产生大量内存碎片)
  • 长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了一次Minor GC,那么对象就会进入Survivor区,之后每经过一次Minor GC,对象年龄加一,直到达到阈值,对象进入老年区
  • 动态判断对象的年龄,如果Survivor区中相同年龄的虽有对象大小总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象可以直接进入老年代。
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小
    • 如果这个值大于老年区的剩余值,则进行一次FULL GC,
    • 如果小于,则检查HandlePromotionFailure设置,如果为true,则只进行Monitor GC,如果为false,则进行FULL GC
说说Java对象创建过程

Java对象的创建过程可以概括为以下几个步骤

  1. 类加载:JVM会先检查类是否已经被加载了,如果没有则通过类加载器加载类的class文件,并将类的信息存储到方法区中
  2. 内存分配:当类被加载后,JVM会为该类的对象分配内存,根据Java对象的特点,内存大小是在编译时就已经确定的,因此内存分配可以通过一些简单的算法来实现,例如指针碰撞和空闲列表等
  3. 初始化:内存分配完成后,JVM会对对象进行默认初始化,即将对象的成员变量赋上默认值。基本类型的默认值是0或false,引用类型的默认值是null
  4. 构造函数:默认初始化后,JVM会调用该对象的构造函数,进行对象的属性初始化和一些其他操作
  5. 返回地址:构造函数执行完毕后,JVM会将对象的引用返回给调用者,此时对象创建过程完毕
你知道哪些垃圾回收算法

GC最基础的算法有三种:标记-清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法

  1. 标记-清除算法:首先标记出所有仍然使用的对象,在标记完成后清理掉未被标记的垃圾对象。它的主要缺点是会产生内存碎片。
  2. 复制算法:将堆内存分为两个区域,每次只使用其中一块。当着一块用完之后,将所有存活的对象复制到另一块未使用的区域,同时将这一块整个清空。这种算法的缺点是需要耗费两倍的内存空间,适用于对象存活率比较低的情况
  3. 标记-压缩算法:标记过程与标记清除算法一样,但后续的步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向同一端移动,然后清理掉端边界以外的内存。它的主要缺陷是需要大量的对象移动操作,影响性能
  4. 分代收集算法:根据对象的特征和存活周期,将堆内存划分为不同的区域,一般分为新生代和老年代。不同的区域使用不同的垃圾收集算法和参数设置。例如年轻代通常采用复制算法,老年代采用标记整理算法或分块整理等
  5. G1收集器:它是一个面向服务端应用的垃圾收集器,采用分代收集算法,将堆内存划分为多个region,并动态做垃圾回收。G1适用于大型、多核、高并发应用下的垃圾回收
说一下JVM有哪些垃圾回收器
  • Serial收集器(复制算法):

新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

  • ParNew收集器 (复制算法):

新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

  • Parallel Scavenge收集器 (复制算法):

新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

  • Serial Old收集器 (标记-整理算法):

老年代单线程收集器,Serial收集器的老年代版本;

  • Parallel Old收集器 (标记-整理算法):

老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法):

老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

  • G1(Garbage First)收集器 (标记-整理算法):

Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

  • ZGC (Z Garbage Collector):

是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。在 JDK 11 新加入,还在实验阶段,主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。优点:低停顿,高吞吐量,ZGC 收集过程中额外耗费的内存小。缺点:浮动垃圾。