您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

程序员必备:JVM核心知识点总结

时间:2022-03-30 09:52:02  来源:  作者:编程技术分享

JAVA开发的朋友,最怕的就是之一:JVM调优。实话实说,在工作中用的不是很多,只有出现问题了才会用到(也可以在项目发布时调整好相关参数,避免线上出问题)。但是在面试中,这一块就是必须掌握的了,否则和HR聊薪水都会受到限制。

主要内容:

  • 小白也能看懂的JVM运行时数据区详解。
  • 垃圾回收,到底是怎么回收的?
  • JDK8为什么要引入元空间替换永久代?
  • CMS和G1垃圾收集器介绍
  • 常见JVM参数介绍
  • 堆空间如何设置?
  • 元空间如何设置?
  • 栈相关参数如何设置?
  • 日志参数如何设置?
  • 垃圾收集器如何配置?
  • CMS和G1参数设置介绍
  • 其他参数设置介绍
  • 调优案例

适合人群:所有Java开发人员,对JVM调优感兴趣的朋友

我们先从 JVM(Java Virtual machine)的基本知识点开始聊。Java 中的一些代码优化技巧,和JVM的关系非常的大,比如逃逸分析对非捕获型 Lambda 表达式的优化。

在进行这些优化之前,你需要对 JVM 的一些运行原理有较深刻的认识,在优化时才会有针对性的方向。

JVM 内存区域划分

学习 JVM,内存区域划分是绕不过去的知识点,这几乎是面试必考的题目。如下图所示,内存区域划分主要包括堆、Java 虚拟机栈、程序计数器、本地方法栈、元空间和直接内存这五部分,我将逐一介绍。

程序员必备:JVM核心知识点总结

 

1.堆

如 JVM 内存区域划分图所示,JVM 中占用内存最大的区域,就是堆(Heap),我们平常编码创建的对象,大多数是在这上面分配的,也是垃圾回收器回收的主要目标区域。

2.Java 虚拟机栈

JVM 的解释过程是基于栈的,程序的执行过程也就是入栈出栈的过程,这也是 Java 虚拟机栈这个名称的由来。

Java 虚拟机栈是和线程相关的。当你启动一个新的线程,Java 就会为它分配一个虚拟机栈,之后所有这个线程的运行,都会在栈里进行。

Java 虚拟机栈,从方法入栈到具体的字节码执行,其实是一个双层的栈结构,也就是栈里面还包含栈。

程序员必备:JVM核心知识点总结

 

如上图,Java 虚拟机栈里的每一个元素,叫作栈帧。每一个栈帧,包含四个区域: 局部变量表 、操作数栈、动态连接和返回地址。

其中,操作数栈就是具体的字节码指令所操作的栈区域,考虑到下面这段代码:

package com.tian.utils;

public class Test {
    public int test() {
        int a = 1;
        a++;
        return a;
    }
}

JVM 将会为 test 方法生成一个栈帧,然后入栈,等 test 方法执行完毕,就会将对应的栈帧弹出。在对变量 a 进行加一操作的时候,就会对栈帧中的操作数栈运用相关的字节码指令。

我们对上面这个类进行编译成Test.class文件后,使用命令:

javap -verbose -c Test.class >test.txt

这样就会把这个类的字节码指令输出到test.txt文件中:

Classfile /E:/workspace/other/hAppy-mall/target/classes/com/tian/utils/Test.class
  Last modified 2022-3-23; size 369 bytes
  MD5 checksum 58d655b96f21dd36600ad0a8df0efa70
  Compiled from "Test.java"
public class com.tian.utils.Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // com/tian/utils/Test
   #3 = Class              #19            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/tian/utils/Test;
  #11 = Utf8               test
  #12 = Utf8               ()I
  #13 = Utf8               a
  #14 = Utf8               I
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #4:#5          // "<init>":()V
  #18 = Utf8               com/tian/utils/Test
  #19 = Utf8               java/lang/Object
{
  public com.tian.utils.Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/tian/utils/Test;

  public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iinc          1, 1
         5: iload_1
         6: ireturn
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/tian/utils/Test;
            2       5     1     a   I
}
SourceFile: "Test.java"

上面这段字节码代码,可能很多人都看不懂,建议结合:

程序员必备:JVM核心知识点总结

 

推荐博客(如果英语好的话,也可以去官网查看):

字节码指令大全

然后,再结合我们上面的Java虚拟机栈这块知识,就能轻松阅读了。

栈帧的创建是需要耗费资源的,尤其是对于 Java 中常见的 getter、setter 方法来说,这些代码通常只有一行,每次都创建栈帧的话就太浪费了。

另外,Java 虚拟机栈对代码的执行,采用的是字节码解释的方式,考虑到下面这段代码,变量 a 声明之后,就再也不被使用,要是按照字节码指令解释执行的话,就要做很多无用功。

另外,我们了解到垃圾回收器回收的目标区域主要是堆,堆上创建的对象越多,GC 的压力就越大。要是能把一些变量,直接在栈上分配,那 GC 的压力就会小一些。

3.程序计数器

既然是线程,就要接受操作系统的调度,但总有时候,某些线程是获取不到 CPU 时间片的,那么当这个线程恢复执行的时候,它是如何确保找到切换之前执行的位置呢?这就是程序计数器的功能。

和 Java 虚拟机栈一样,它也是线程私有的。程序计数器只需要记录一个执行位置就可以,所以不需要太大的空间。事实上,程序计数器是 JVM 规范中唯一没有规定 OutOfMemoryError 情况的区域。

4.本地方法栈

与 Java 虚拟机栈类似,本地方法栈,是针对 native 方法的。我们常用的 HotSpot,将 Java 虚拟机栈和本地方法栈合二为一,其实就是一个本地方法栈,大家注意规范里的这些差别就可以了。

5.元空间

元空间是一个容易引起混淆的区域,原因就在于它经历了多次迭代才成为现在的模样。关于这部分区域,我们来讲解两个面试题,大家就明白了。

  • 元空间是在堆上吗?

答案:元空间并不是在堆上分配的,而是在堆外空间进行分配的,它的大小默认没有上限,我们常说的方法区,就在元空间中。

  • 字符串常量池在那个区域中?

答案:这个要看 JDK 版本。

在 JDK 1.8 之前,是没有元空间这个概念的,当时的方法区是放在一个叫作永久代的空间中。

而在 JDK 1.7 之前,字符串常量池也放在这个叫作永久带的空间中。但在 JDK 1.7 版本,已经将字符串常量池从永久带移动到了堆上。

所以,从 1.7 版本开始,字符串常量池就一直存在于堆上。

  • 为什么使用元空间替换永久代?

表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦。

6.直接内存

直接内存,指的是使用了 Java 的直接内存 API,进行操作的内存。这部分内存可以受到 JVM 的管控,比如 ByteBuffer 类所申请的内存,就可以使用具体的参数进行控制。

需要注意的是直接内存和本地内存不是一个概念。

  • 直接内存比较专一,有具体的 API(这里指的是ByteBuffer),也可以使用 -XX:MaxDirectMemorySize 参数控制它的大小;
  • 本地内存是一个统称,比如使用 native 函数操作的内存就是本地内存,本地内存的使用 JVM 是限制不住的,使用的时候一定要小心。

GC Roots

对象主要是在堆上分配的,我们可以把它想象成一个池子,对象不停地创建,后台的垃圾回收进程不断地清理不再使用的对象。当内存回收的速度,赶不上对象创建的速度,这个对象池子就会产生溢出,也就是我们常说的 OOM。

把不再使用的对象及时地从堆空间清理出去,是避免 OOM 有效的方法。那 JVM 是如何判断哪些对象应该被清理,哪些对象需要被继续使用呢?

这里首先强调一个概念,这对理解垃圾回收的过程非常有帮助,面试时也能很好地展示自己。

垃圾回收,并不是找到不再使用的对象,然后将这些对象清除掉。它的过程正好相反,JVM 会找到正在使用的对象,对这些使用的对象进行标记和追溯,然后一股脑地把剩下的对象判定为垃圾,进行清理。

了解了这个概念,我们就可以看下一些基本的衍生分析:

  • GC 的速度,和堆内存活对象的多少有关,与堆内所有对象的数量无关;
  • GC 的速度与堆的大小无关,32GB 的堆和 4GB 的堆,只要存活对象是一样的,垃圾回收速度也会差不多;
  • 垃圾回收不必每次都把垃圾清理得干干净净,最重要的是不要把正在使用的对象判定为垃圾。

那么,如何找到这些存活对象,也就是哪些对象是正在被使用的,就成了问题的核心。

大家可以想一下写代码的时候,如果想要保证一个 HashMap 能够被持续使用,可以把它声明成静态变量,这样就不会被垃圾回收器回收掉。我们把这些正在使用的引用的入口,叫作GC Roots。

这种使用 tracing 方式寻找存活对象的方法,还有一个好听的名字,叫作可达性分析法

概括来讲,GC Roots 包括:

  • Java 线程中,当前所有正在被调用的方法的引用类型参数、局部变量、临时值等。也就是与我们栈帧相关的各种引用;
  • 所有当前被加载的 Java 类;
  • Java 类的引用类型静态变量;
  • 运行时常量池里的引用类型常量(String 或 Class 类型);
  • JVM 内部数据结构的一些引用,比如 sun.jvm.hotspot.memory.Universe 类;
  • 用于同步的监控对象,比如调用了对象的 wAIt() 方法;
  • JNI handles,包括 global handles 和 local handles。

对于这个知识点,不要死记硬背,可以对比着 JVM 内存区域划分那张图去看,入口大约有三个:线程、静态变量和 JNI 引用。

强、软、弱、虚引用

那么,通过 GC Roots 能够追溯到的对象,就一定不会被垃圾回收吗?这要看情况。

Java 对象与对象之间的引用,存在着四种不同的引用级别,强度从高到低依次是:强引用、软引用、弱引用、虚引用。

  • 强应用 默认的对象关系是强引用,也就是我们默认的对象创建方式。这种引用属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉。
  • 软引用 用于维护一些可有可无的对象。在内存足够的时候,软引用对象不会被回收;只有在内存不足时,系统则会回收软引用对象;如果回收了软引用对象之后,仍然没有足够的内存,才会抛出内存溢出异常。
  • 弱引用 级别就更低一些,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。软引用和弱引用在堆内缓存系统中使用非常频繁,可以在内存紧张时优先被回收掉。
  • 虚引用 是一种形同虚设的引用,在现实场景中用得不是很多。这里有一个冷门的知识点:Java 9.0 以后新加入了 Cleaner 类,用来替代 Object 类的 finalizer 方法,这就是虚引用的一种应用场景。

分代垃圾回收

上面我们提到,垃圾回收的速度,是和存活的对象数量有关系的,如果这些对象太多,JVM 再做标记和追溯的时候,就会很慢。

一般情况下,JVM 在做这些事情的时候,都会停止业务线程的所有工作,进入 SafePoint 状态,这也就是我们通常说的 Stop the World。所以,现在的垃圾回收器,有一个主要目标,就是减少 STW 的时间。

其中一种有效的方式,就是采用分代垃圾回收,减少单次回收区域的大小。这是因为,大部分对象,可以分为两类:

  • 大部分对象的生命周期都很短
  • 其他对象则很可能会存活很长时间

这个假设我们称之为弱代假设(weak generational hypothesis)。

如下图,分代垃圾回收器会在逻辑上,把堆空间分为两部分:年轻代(Young generation)和老年代(Old generation)。

程序员必备:JVM核心知识点总结

 

1.年轻代

年轻代中又分为一个伊甸园空间(Eden),两个幸存者空间(Survivor)。对象会首先在年轻代中的 Eden 区进行分配,当 Eden 区分配满的时候,就会触发年轻代的 GC。

此时,存活的对象会被移动到其中一个 Survivor 分区(以下简称 from);年轻代再次发生垃圾回收,存活对象,包括 from 区中的存活对象,会被移动到 to 区。所以,from 和 to 两个区域,总有一个是空的。

Eden、from、to 的默认比例是 8:1:1,所以只会造成 10% 的空间浪费。这个比例,是由参数 -XX:SurvivorRatio 进行配置的(默认为 8)。

2.老年代

对垃圾回收的优化,就是要让对象尽快在年轻代就回收掉,减少到老年代的对象。那么对象是如何进入老年代的呢?它主要有以下四种方式。

  • 正常提升(Promotion)

上面提到了年轻代的垃圾回收,如果对象能够熬过年轻代垃圾回收,它的年龄(age)就会加一,当对象的年龄达到一定阈值,就会被移动到老年代中。

  • 分配担保

如果年轻代的空间不足,又有新的对象需要分配空间,就需要依赖其他内存(这里是老年代)进行分配担保,对象将直接在老年代创建。

  • 大对象直接在老年代分配

超出某个阈值大小的对象,将直接在老年代分配,可以通过
-XX:PretenureSizeThreshold 配置这个阈值。

  • 动态对象年龄判定

有的垃圾回收算法,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。比如 G1,通过 TargetSurvivorRatio 这个参数,动态更改对象提升的阈值。

老年代的空间一般比较大,回收的时间更长,当老年代的空间被占满了,将发生老年代垃圾回收。

目前,被广泛使用的是 G1 垃圾回收器。G1 的目标是用来干掉 CMS 的,它同样有年轻代和老年代的概念。不过,G1 把整个堆切成了很多份,把每一份当作一个小目标,部分上目标很容易达成。

程序员必备:JVM核心知识点总结

 

如上图,G1 也是有 Eden 区和 Survivor 区的概念的,只不过它们在内存上不是连续的,而是由一小份一小份组成的。G1 在进行垃圾回收的时候,将会根据最大停顿时间(MaxGCPauseMillis)设置的值,动态地选取部分小堆区进行垃圾回收。

G1 的配置非常简单,我们只需要配置三个参数,一般就可以获取优异的性能:

  • MaxGCPauseMillis 设置最大停顿的预定目标,G1 垃圾回收器会自动调整,选取特定的小堆区;
  • G1HeapRegionSize 设置小堆区的大小;
  • InitiatingHeapOccupancyPercent当整个堆内存使用达到一定比例(默认是45%),并发标记阶段就会被启动。

逃逸分析

下面着重讲解一下逃逸分析,这个知识点在面试的时候经常会被问到。

我们常说的对象,除了基本数据类型,一定是在堆上分配的吗?

答案是否定的,通过逃逸分析,JVM 能够分析出一个新的对象的使用范围,从而决定是否要将这个对象分配到堆上。逃逸分析现在是 JVM 的默认行为,可以通过参数 -XX:-DoEscapeAnalysis 关掉它。

那什么样的对象算是逃逸的呢?可以看一下下面的两种典型情况。

如代码所示,对象被赋值给成员变量或者静态变量,可能被外部使用,变量就发生了逃逸。

public class EscapeAttr {
    Object attr;
    public void test() {
        attr = new Object();
    }
}

再看下面这段代码,对象通过 return 语句返回。由于程序并不能确定这个对象后续会不会被使用,外部的线程能够访问到这个结果,对象也发生了逃逸。

 public class EscapeReturn {
    Object attr;
    public Object test() {
        Object obj = new Object();
        return obj;
    }
}

那逃逸分析有什么好处呢? 1. 栈上分配

如果一个对象在子程序中被分配,指向该对象的指针永远不会逃逸,对象有可能会被优化为栈分配。栈分配可以快速地在栈帧上创建和销毁对象,不用再分配到堆空间,可以有效地减少 GC 的压力。

2. 分离对象或标量替换

但对象结构通常都比较复杂,如何将对象保存在栈上呢?

JIT 可以将对象打散,全部替换为一个个小的局部变量,这个打散的过程,就叫作标量替换(标量就是不能被进一步分割的变量,比如 int、long 等基本类型)。也就是说,标量替换后的对象,全部变成了局部变量,可以方便地进行栈上分配,而无须改动其他的代码。

从上面的描述我们可以看到,并不是所有的对象或者数组,都会在堆上分配。由于JIT的存在,如果发现某些对象没有逃逸出方法,那么就有可能被优化成栈分配。

3.同步消除

如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

注意这是针对 synchronized 来说的,JUC 中的 Lock 并不能被消除。

要开启同步消除,需要加上-XX:+EliminateLocks参数。由于这个参数依赖逃逸分析,所以同时要打开-XX:+DoEscapeAnalysis 选项。

JVM 常见优化参数

现在大家用得最多的 Java 版本是 Java 8,如果你的公司比较保守,那么使用较多的垃圾回收器就是 CMS 。但 CMS 已经在 Java 14 中被正式废除,随着 ZGC 的诞生和 G1 的稳定,CMS 终将成为过去式。

Java 9 之后,Java 版本已经进入了快速发布阶段,大约是每半年发布一次,Java 8 和 Java 11 是目前支持的 LTS 版本。

由于 JVM 一直处在变化之中,所以一些参数的配置并不总是有效的。有时候你加入一个参数,“感觉上”运行速度加快了,但通过 -XX:+PrintFlagsFinal来查看,却发现这个参数默认就是这样了。

所以,在不同的 JVM 版本上,不同的垃圾回收器上,要先看一下这个参数默认是什么,不要轻信别人的建议,命令行示例如下:

java -XX:+PrintFlagsFinal -XX:+UseG1GC 2>&1 | grep UseAdaptiveSizePolicy

还有一个与之类似的参数叫作PrintCommandLineFlags,通过它,你能够查看当前所使用的垃圾回收器和一些默认的值。

可以看到下面的 JVM 默认使用的就是并行收集器:

# java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=127905216 -XX:MaxHeapSize=2046483456 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
openjdk version "1.8.0_41"
OpenJDK Runtime Environment (build 1.8.0_41-b04)
OpenJDK 64-Bit Server VM (build 25.40-b25, mixed mode)

下面我大致罗列一些JVM调优参数:

堆相关参数

-Xms10g :JVM启动时申请的初始堆内存值
-Xmx20G :JVM可申请的最大Heap值
-Xmn3g : 新生代大小,一般设置为堆空间的1/3 1/4左右,新生代大则老年代小
-Xss :Java每个线程的Stack大小
-XX:PermSize :持久代(方法区)的初始内存大小
-XX:MaxPermSize : 持久代(方法区)的最大内存大小
-XX:SurvivorRatio : 设置新生代eden空间和from/to空间的比例关系,关系(eden/from=eden/to)
-XX:NewRatio : 设置新生代和老年代的比例老年代/新生代

日志相关参数

-XX:+PrintGC :打印GC日志
-XX:+PrintGCDetailsGC :时的详细堆信息
-XX:+PrintHeapAtGC :打印GC前后的堆信息
-XX:+PrintGCTimeStamps :输出GC发生时间,输出的时间为虚拟机启动的偏移量
-XX:+PrintGCApplicationConcurrentTime :输出应用程序执行时间
-XX:+PrintGCApplicationStoppedTime :输出应用程序由于GC产生停顿的时间
-XX:+PrintRefrenceGC :输出软引用、弱引用、虚引用和Finalize队列
-XX:+HeapDumpOnOutOfMemoryError :产生OOM时可以在内存溢出时导出整个堆信息
-XX:HeapDumpPath :导出堆文件存放路径
-XX:+TraceClassLoading :跟踪类加载信息
-XX:+TraceClassUnloading :跟:踪类卸载信息
-XX:PrintClassHitogram :查看系统中的类的分布情况(占用空间最多、实例数量空间大小)
-XX:+PrintVMOptions :打印虚拟机接收到的命令行显示参数
-XX:+PrintCommandLineFlags :打印虚拟机的显式和隐式参数
-XX:+PrintFlagsFinal :打印虚拟机的所有系统参数

GC相关参数

-XX:+UseSerialGC :新生代、老年代使用串行收集器
-XX:SurvivorRatio :设置eden区和survivor区大小的比例
-XX:PretenureSizeThreshold,:当对象大小超过此值时,直接分配到老年代
-XX:MaxTenuringThreshold :设置对象进入老年代的最大年龄
-XX:+UseParNewGC :新生代使用并行收集器
-XX:+UseParallelOldGC :老年代使用并行回收收集器
-XX:+ParallelGCThreads :设置垃圾回收线程数,一般最好与CPU数量相当,默认情况下,当CPU数量小于8个时,ParallelGCThreads的值相当于CPU数量,当CPU数量大于8个时,ParallelGCThreads的值等于3+((5*CPU_COUNT)/8
-XX:MaxGCPauseMillis :设置最大垃圾收集停顿时间
-XX:GCTimeRatio :设置吞吐量大小,它的值是一个0~100之间的整数,假设值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集
-XX:+UseAdaptiveSizePolicy :打开自适应GC策略,JVM对新生代的大小、eden和survivior的比例、晋升老年代对象年龄等参数自动调整
-XX:+UseConcMarkSweepGC :启用CMS
-XX:ParallelCMSThreads :设置CMS线程数量
-XX:CMSInitiatingOccupancyFraction :默认68当老年代的空间超过68%时会执行一次CMS回收
-XX:UseCMSCompactAtFullCollection :设置CMS结束后是否需要进行一次内存空间整理
-XX:CMSFullGCsBeforeCompaction :进行多少次CMS后进行内存空间压缩
-XX:+CMSClassUnloadingEnabled :允许对类元数据区进行回收
-XX:CMSInitiatingPermOccupancyFraction :当永久区占用率达到此值时进行CMS回收(须激活CMSClassUnloadingEnabled)
-XX:UseCMSInitiatingOccupancyOnly:只要达到阈值时进行CMS回收
-XX:+UseG1GC :使用G1
-XX:MaxGCPauseMillis :最大垃圾收集停顿时间
-XX:GCPauseIntervalMillis :最大停顿间隔时间

JVM 的参数配置繁多,但大多数不需要我们去关心。

调优案例

下面,我们通过对 ES 服务的 JVM 参数分析,来看一下常见的优化点。

ElasticSearch(简称 ES)是一个高性能的开源分布式搜索引擎。ES 是基于 Java 语言开发的,既然是Java开发,那肯定会涉及到JVM调优了,在它的 conf 目录下,有一个叫作jvm.options的文件,JVM 的配置就放在这里。

堆空间的配置

下面是 ES 对于堆空间大小的配置。

-Xms1g
-Xmx1g

JVM 中空间最大的一块就是堆,垃圾回收也主要是针对这块区域。通过 Xmx 可指定堆的最大值,通过 Xms 可指定堆的初始大小。我们通常把这两个参数,设置成一样大小的,可避免堆空间在动态扩容时的时间开销。

在配置文件中还有AlwaysPreTouch这个参数。

-XX:+AlwaysPreTouch

其实,通过 Xmx 指定了的堆内存,只有在 JVM 真正使用的时候,才会进行分配。这个参数,在 JVM 启动的时候,就把它所有的内存在操作系统分配了。在堆比较大的时候,会加大启动时间,但它能够减少内存动态分配的性能损耗,提高运行时的速度。

如下图,JVM 的内存,分为堆和堆外内存,其中堆的大小可以通过 Xmx 和 Xms 来配置。

程序员必备:JVM核心知识点总结

 

但我们在配置 ES 的堆内存时,通常把堆的初始化大小,设置成物理内存的一半。这是因为 ES 是存储类型的服务,我们需要预留一半的内存给文件缓存 ,等下次用到相同的文件时,就不用与磁盘进行频繁的交互。这一块区域一般叫作 PageCache,占用的空间很大。

对于计算型节点来说,比如我们普通的 Web 服务,通常会把堆内存设置为物理内存的 2/3,剩下的 1/3 就是给堆外内存使用的。

我们这张图,对堆外内存进行了非常细致的划分,解释如下:

  • 元空间 参数 -XX:MaxMetaspaceSize 和 -XX:MetaspaceSize,分别指定了元空间的最大内存和初始化内存。因为元空间默认是没有上限的,所以极端情况下,元空间会一直挤占操作系统剩余内存。
  • JIT 编译后代码存放 -XX:ReservedCodeCacheSize。JIT 是 JVM 一个非常重要的特性,CodeCahe 存放的,就是即时编译器所生成的二进制代码。另外,JNI 的代码也是放在这里的。
  • 本地内存 本地内存是一些其他 attch 在 JVM 进程上的内存区域的统称。比如网络连接占用的内存、线程创建占用的内存等。在高并发应用下,由于连接和线程都比较多,这部分内存累加起来还是比较可观的。
  • 直接内存 这里要着重提一下直接内存,因为它是本地内存中唯一可以使用参数来限制大小的区域。使用参数 -XX:MaxDirectMemorySize,即可设定 ByteBuffer 类所申请的内存上限。
  • JNI 内存 上面谈到 CodeCache 存放的 JNI 代码,JNI 内存就是指的这部分代码所 malloc 的具体内存。很可惜的是,这部分内存的使用 JVM 是无法控制的,它依赖于具体的 JNI 代码实现。

日志参数配置

下面是 ES 的日志参数配置,由于 Java 8 和 Java 9 的参数配置已经完全不一样了,ES 在这里也分了两份。

Java8参数设置:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime
-Xloggc:logs/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=32
-XX:GCLogFileSize=64m

Java9参数设置:

-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m

下面解释一下这些参数的意义,以 Java 8 为例。

  • PrintGCDetails 打印详细 GC 日志。
  • PrintGCDateStamps 打印当前系统时间,更加可读;与之对应的是 PrintGCTimeStamps,打印的是 JVM 启动后的相对时间,可读性较差。
  • PrintTenuringDistribution 打印对象年龄分布,对调优 MaxTenuringThreshold 参数帮助很大。
  • PrintGCApplicationStoppedTime 打印 STW 时间
  • 下面几个日志参数是配置了类似于 Logback 的滚动日志,比较简单,不再详细介绍

从 Java 9 开始,JVM 移除了 40 多个 GC 日志相关的参数,具体参见 JEP 158。所以这部分的日志配置有很大的变化,GC 日志的打印方式,已经完全不一样了,比以前的日志参数规整了许多。

参数如下所示:

-Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m

再来看下 ES 在异常情况下的配置参数:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=data
-XX:ErrorFile=logs/hs_err_pid%p.log


HeapDumpOnOutOfMemoryError、HeapDumpPath、ErrorFile是每个 Java 应用都应该配置的参数。正常情况下,我们通过 jmap 获取应用程序的堆信息;异常情况下,比如发生了 OOM,通过这三个配置参数,即可在发生OOM的时候,自动 dump 一份堆信息到指定的目录中。

拿到了这份 dump 信息,我们就可以使用 MAT 等工具详细分析,找到具体的 OOM 原因。

垃圾回收器配置

ES 默认使用 CMS 垃圾回收器,它有以下三行主要的配置。

-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

下面介绍一下这两个参数:

  • UseConcMarkSweepGC,表示年轻代使用 ParNew,老年代的用 CMS 垃圾回收器
  • -XX:CMSInitiatingOccupancyFraction 由于 CMS 在执行过程中,用户线程还需要运行,那就需要保证有充足的内存空间供用户使用。如果等到老年代空间快满了,再开启这个回收过程,用户线程可能会产生“Concurrent Mode Failure”的错误,这时会临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了(STW)。

这部分空间预留,一般在 30% 左右即可,那么能用的大概只有 70%。参数
-XX:CMSInitiatingOccupancyFraction 用来配置这个比例,但它首先必须配置 -XX:+UseCMSInitiatingOccupancyOnly 参数。

另外,对于 CMS 垃圾回收器,常用的还有下面的配置参数:

  • -XX:ExplicitGCInvokesConcurrent 当代码里显示的调用了 System.gc(),实际上是想让回收器进行FullGC,如果发生这种情况,则使用这个参数开始并行 FullGC。建议加上。
  • -XX:CMSFullGCsBeforeCompaction 默认为 0,就是每次FullGC都对老年代进行碎片整理压缩,建议保持默认。
  • -XX:CMSScavengeBeforeRemark 开启或关闭在 CMS 重新标记阶段之前的清除(YGC)尝试。可以降低 remark 时间,建议加上。
  • -XX:+ParallelRefProcEnabled 可以用来并行处理 Reference,以加快处理速度,缩短耗时。

CMS 垃圾回收器,已经在 Java14 中被移除,由于它的 GC 时间不可控,有条件应该尽量避免使用。

针对 Java10(普通 Java 应用在 Java 8 中即可开启 G1),ES 可采用 G1 垃圾回收器。

G1垃圾收集器,它可以通过配置参数 MaxGCPauseMillis,指定一个期望的停顿时间,使用相对比较简单。

下面是主要的配置参数:

  • -XX:MaxGCPauseMillis 设置目标停顿时间,G1 会尽力达成。
  • -XX:G1HeapRegionSize 设置小堆区大小。这个值为 2 的次幂,不要太大,也不要太小。如果是在不知道如何设置,保持默认。
  • -XX:InitiatingHeapOccupancyPercent 当整个堆内存使用达到一定比例(默认是45%),并发标记阶段就会被启动。
  • -XX:ConcGCThreads 并发垃圾收集器使用的线程数量。默认值随 JVM 运行的平台不同而不同。不建议修改。

JVM 支持非常多的垃圾回收器,下面是最常用的几个,以及配置参数:

  • -XX:+UseSerialGC 年轻代和老年代都用串行收集器
  • -XX:+UseParallelGC 年轻代使用 ParallerGC,老年代使用 Serial Old
  • -XX:+UseParallelOldGC 新生代和老年代都使用并行收集器
  • -XX:+UseG1GC 使用 G1 垃圾回收器
  • -XX:+UseZGC 使用 ZGC 垃圾回收器

额外配置

我们再来看下几个额外的配置。

-Xss1m

-Xss设置每个 Java 虚拟机栈的容量为 1MB。这个参数和 -XX:ThreadStackSize是一样的,默认就是 1MB。

-XX:-OmitStackTraceInFastThrow

把 - 换成 +,可以减少异常栈的输出,进行合并。虽然会对调试有一定的困扰,但能在发生异常时显著增加性能。随之而来的就是异常信息不好排查,ES 为了找问题方便,就把错误合并给关掉了。

-Djava.awt.headless=true

Headless 模式是系统的一种配置模式,在该模式下,系统缺少了显示设备、键盘或鼠标。在服务器上一般是没这些设备的,这个参数是告诉虚拟机使用软件去模拟这些设备。

-Djava.locale.providers=COMPAT
-Dfile.encoding=UTF-8
-Des.NETworkaddress.cache.ttl=60
-Des.networkaddress.cache.negative.ttl=10
-Dio.netty.noUnsafe=true
-Dio.netty.noKeySetOptimization=true
-Dio.netty.recycler.maxCapacityPerThread=0
-Dlog4j.shutdownHookEnabled=false
-Dlog4j2.disable.jmx=true
-Djava.io.tmpdir=${ES_TMPDIR}
-Djna.nosys=true

上面这些参数,通过 -D 参数,在启动一个 Java 程序时,设置系统属性值,也就是在 System 类中通过getProperties()得到的一串系统属性。

这部分自定义性比较强,不做过多介绍。

其他调优

以上就是 ES 默认的 JVM 参数配置,大多数还是比较基础的。在平常的应用服务中,我们希望得到更细粒度的控制,其中比较常用的就是调整各个分代之间的比例。

  • -Xmn 年轻代大小,默认年轻代占堆大小的 1/3。高并发快消亡场景可适当加大这个区域,对半或者更多都是可以的。但是在 G1 下,就不用再设置这个值了,它会自动调整;
  • -XX:SurvivorRatio 默认值为 8,表示伊甸区和幸存区的比例;
  • -XX:MaxTenuringThreshold 这个值在 CMS 下默认为 6,G1 下默认为 15。这个值和我们前面提到的对象提升有关,改动效果会比较明显。对象的年龄分布可以使用-XX:+PrintTenuringDistribution打印,如果后面几代的大小总是差不多,证明过了某个年龄后的对象总能晋升到老年代,就可以把晋升阈值设的小一些;
  • PretenureSizeThreshold 超过一定大小的对象,将直接在老年代分配,不过这个参数用得不是很多。


Tags:JVM   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Graalvm 替代 JVM 真的可以带来巨大的性能优势吗?
介绍Spring Boot有助于轻松开发独立的、可用于生产的 Spring 应用程序。它对 Spring 平台和第三方库采用固执己见的方法:以最少的配置简化设置过程。优势: 易于使用:Spring Boo...【详细内容】
2023-12-25  Search: JVM  点击:(137)  评论:(0)  加入收藏
理解Java虚拟机(JVM):优化代码执行效率的内部机制
Java虚拟机(Java Virtual Machine,JVM)是Java程序运行的核心组件,它负责将Java源代码编译成字节码并执行。JVM具有内部机制来优化代码的执行效率,包括即时编译(Just-In-Time Compi...【详细内容】
2023-12-14  Search: JVM  点击:(228)  评论:(0)  加入收藏
深入了解Java的GC原理,掌握JVM 性能调优!
JVM性能调优是一个复杂的过程,需要结合具体的应用程序特性和需求来进行调优。不同的应用场景可能需要不同的调优策略。对于 Java 开发人员来说,进行程序的性能优化是很有挑战...【详细内容】
2023-12-12  Search: JVM  点击:(199)  评论:(0)  加入收藏
GC是什么?为什么要GC?JVM 垃圾回收算法有哪些?
Major GC 老年代区域的垃圾回收,老年代空间不足时,会先尝试触发Minor GC。Minor GC之后空间还不足,则会触发Major GC,Major GC速度比较慢,暂停时间长。图片1 Java垃圾回收机制(GC...【详细内容】
2023-12-07  Search: JVM  点击:(231)  评论:(0)  加入收藏
JVM由那些部分组成,运行流程是什么?
思考: JVM由那些部分组成,运行流程是什么?1.JVM由那些部分组成,运行流程是什么?JVM是什么好处:一次编写,到处运行自动内存管理,垃圾回收机制思考:JVM由哪些部分组成,运行流程是什么?...【详细内容】
2023-12-06  Search: JVM  点击:(206)  评论:(0)  加入收藏
JVM的调优常用参数
调优目的JVM调优的目的是为了提高Java应用程序的性能和稳定性。通过优化JVM的配置和参数设置,可以减少内存占用、提高垃圾回收效率、优化线程管理等,从而提升应用程序的响应速...【详细内容】
2023-11-10  Search: JVM  点击:(201)  评论:(0)  加入收藏
JVM 解释和编译指南
Java 是一种跨平台的编程语言。程序源代码会被编译为 字节码bytecode,然后字节码在运行时被转换为 机器码machine code。解释器interpreter 在物理机器上模拟出的抽象计算机...【详细内容】
2023-11-07  Search: JVM  点击:(369)  评论:(0)  加入收藏
深入理解并发编程艺术之JVM内存模型
java内存模型由来我们知道不同的计算机硬件和操作系统的,所遵循的规范以及计算机内存模型是有区别的,也就意味着我们开发的程序放在某个计算机硬件和操作系统上运行是正常的,而...【详细内容】
2023-10-27  Search: JVM  点击:(441)  评论:(0)  加入收藏
OOM异常会导致JVM退出吗?
熟悉Java开发的人,应该会经常遇到的异常:OOM,那么这个异常会导致 JVM 虚拟机退出吗?结论Java虚拟机(JVM)在运行Java应用时,可能会遇到内存不足的情况,从而抛出OutOfMemoryError(OOM)。...【详细内容】
2023-10-13  Search: JVM  点击:(246)  评论:(0)  加入收藏
JVM是如何判定对象已死的?学JVM必会的知识!
大家好,我是 BookSea。作为一名Java程序员,我们每天都在程序里不停地去new对象,但是你知道这些被new出来的对象,最后是怎么被回收的吗?在堆里面存放着Java世界中几乎所有的对象实...【详细内容】
2023-10-08  Search: JVM  点击:(334)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(18)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(25)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(29)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(60)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(77)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(77)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(96)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(111)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(103)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(82)  评论:(0)  加入收藏
站内最新
站内热门
站内头条