天天看点

JAVA虚拟机详解(JVM 、堆、GC、直接内存、性能调优) 建议收藏

作者:IT技术范

一、JVM(JAVA虚拟机)

JVM(Java虚拟机):

  • 是一个抽象的计算模型。
  • 如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。
  • 目的是为构建在其上运行的应用程序提供一个运行环境,能够运行 java 字节码。
  • JVM 可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。
JAVA虚拟机详解(JVM 、堆、GC、直接内存、性能调优) 建议收藏

二、JVM 内存模型 组成元素

Java 内存模型主要包含线程私有的程序计数器、java虚拟机栈、本地方法栈和线程共享的堆空间、元数据区、直接内存。

JAVA虚拟机详解(JVM 、堆、GC、直接内存、性能调优) 建议收藏

1、Java运行时数据区域

Java 虚拟机在执行过程中会将所管理的内存划分为不同的区域,有的随着线程产生和消失,有的随着 Java 进程产生和消失。

根据 JVM 规范,JVM 运行时区域大致分为程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1.8废弃)五个部分。

2、程序计数器(PC 寄存器、计数器)

程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。

在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都有自己的程序计数器。

可以理解为一个指针,指向方法区中的方法字节码(用来存储指向下一个指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。

倘若执行的是 native 方法,则程序计数器中为空。

3、Java 虚拟机栈(JVM Stacks)

虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。

不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

每个栈帧主要包含的内容如下:

  • 局部变量表:存储着 java 基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用。

注意:这里的基本数据类型指的是方法内的局部变量

局部变量表随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变。

  • 操作数栈
  • 动态连接
  • 方法返回地址

虚拟机栈可能会抛出两种异常:

  • 栈溢出(StackOverFlowError):

若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常

  • 内存溢出(OutOfMemoryError):

若虚拟机栈的容量允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OOM 异常

3、本地方法栈(Native Method Stacks)

本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。

本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。

本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

虚拟机栈和本地方法栈的主要区别:

  • 虚拟机栈执行的是 java 方法
  • 本地方法栈执行的是 native 方法

4、Java 堆(Java Heap)

Java 堆中是 JVM 管理的最大一块内存空间。主要存放对象实例。

Java 堆是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。

Java 堆的分区:

  • 在 jdk1.8 之前,分为新生代、老年代、永久代
  • 在 jdk1.8 及之后,只分为新生代、老年代

永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代

Java 堆内存大小:

  • 堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3)。
  • 既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定)。
  • 如果堆无法扩展或者无法分配内存时报 OOM。

主要存储的内容是:

  • 对象实例;
  • 类初始化生成的对象;
  • 基本数据类型的数组也是对象实例;
  • 字符串常量池:字符串常量池原本存放在方法区,jdk8 开始放置于堆中字符串常量池存储的是 string 对象的直接引用,而不是直接存放的对象,是一张 string table
  • 静态变量

static 修饰的静态变量,jdk8 时从方法区迁移至堆中线程分配缓冲区(Thread Local Allocation Buffer)线程私有,但是不影响 java 堆的共性增加线程分配缓冲区是为了提升对象分配时的效率。

堆和栈的区别:

  • 管理方式,堆需要GC,栈自动释放
  • 大小不同,堆比栈大
  • 碎片相关:栈产生的碎片远小于堆,因为GC不是实时的
  • 分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配
  • 效率:栈的效率比堆高

5、方法区(逻辑上)

方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM)

方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区

各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。

  • Java8 以前是放在JVM 内存中的,由堆空间中的永久代实现,受JVM 内存大小参数限制
  • Java8 移除了永久代和方法区,引入了元空间

拓展:

JDK版本 方法区的实现 运行时常量池所在的位置
JDK6 PermGen space(永久代) PermGen space(永久代)
JDK7 PermGen space(永久代) Heap(堆)
JDK8 Metaspace(元空间) Heap(堆)

6、元空间(元数据区、Metaspace)

元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。

元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM

元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

  • 类元信息(Class)

类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表

常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中

  • 运行时常量池(Runtime Constant Pool)

运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法

7、直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。

分配、回收成本较高,但读写性能高。

直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。

JAVA虚拟机详解(JVM 、堆、GC、直接内存、性能调优) 建议收藏

三、Java 程序内存 = JVM 内存 + 本地内存

1、JVM 内存(JVM 虚拟机数据区)

Java 虚拟机在执行的时候会把管理的内存分配到不同的区域,这些区域称为虚拟机(JVM)内存。

JVM 内存受虚拟机内存大小的参数控制,当大小超过参数设置的大小时会报 OOM

2、本地内存(元空间 + 直接内存)

对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。

本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。

虽然不受参数的限制,如果所占内存超过物理内存,仍然会报 OOM

3、堆外内存

直接内存

直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

可通过 -XX:MaxDirectMemorySize 调整大小,默认和 Java 堆最大值一样

内存不足时抛出OutOf-MemoryError或 者OutOfMemoryError:Direct buffer memory;

线程堆栈

可通过 -Xss 调整大小

内存不足时抛出StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度)

OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)

Socket 缓存区

每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。

如果无法分配,可能会抛出 IOException:Too many open files异常

JNI 代码

如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚拟机的本地方法栈和本地内存

虚拟机和垃圾收集器

虚拟机、垃圾收集器的工作也是要消耗一定数量的内存。

四、JVM 堆及各种 GC 详解

1、结构图(新生代、老年代、永久代)

JVM 中的堆,一般分为三大部分:新生代、老年代、永久代( Java8 中已经被移除)

JAVA虚拟机详解(JVM 、堆、GC、直接内存、性能调优) 建议收藏

2、新生代、MinorGC(Young GC)

2.1、新生代:

主要是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。

新生代又分为 Eden、S0、S1(SurvivorFrom、SurvivorTo)三个区:

  • Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。
  • 当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
  • SurvivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
  • SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者。

Eden 和 S0,S1 区的比例为 8 : 1 : 1

幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区

JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90%。

当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁。

2.2、MinorGC

MinorGC 的过程(采用复制算法):

  • 首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,一般是 15,则赋值到老年代区)
  • 同时把这些对象的年龄 + 1(如果 ServicorTo 不够位置了就放到老年区)
  • 然后,清空 Eden 和 ServicorFrom 中的对象;
  • 最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。

Minor GC 触发机制:

当年轻代满(指的是 Eden 满,Survivor 满不会引发 GC)时就会触发 Minor GC(通过复制算法回收垃圾)。

对象年龄(Age)计数器

虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。

对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值) 来设置。

2.3、老年代、MajorGC(Old GC)

年代

  • 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。
  • 在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
  • 当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记-清除算法:

  • 首先扫描一次所有老年代,标记出存活的对象
  • 然后回收没有标记的对象。
  • MajorGC 的耗时比较长(速度一般会比 Minor GC 慢10倍以上,STW 的时间更长),因为要扫描再回收。
  • MajorGC 会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。
  • 当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

2.4、永久代、元数据区(元空间)、常量池

永久代(PermGen)

  • 是 JDK7 及之前, HotSpot 虚拟机基于 JVM 规范对方法区的一个落地实现,其他虚拟机如 JRockit(Oracle)、J9(IBM) 有方法区 ,但是没有永久代。
  • 在 JDK1.8 已经被移除,取而代之的是元数据区(元空间)
  • 内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域。
  • 和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。
  • 所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

元数据区(元空间、Metaspace)

  • 元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。
  • 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

  • -XX:MetaspaceSize (初始空间大小):达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 MaxMetaspaceSize时,适当提高该值。
  • -XX:MaxMetaspaceSize(最大空间)默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

  • -XX:MinMetaspaceFreeRatio :在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集;
  • -XX:MaxMetaspaceFreeRatio :在GC之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;

类的元数据放入本地内存中,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由虚拟机的 MaxPermSize 控制,而由系统的实际可用空间来控制。

元空间替换永久代的原因分析:

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 通常会使用 PermSize 和 MaxPermSize 设置永久代的大小就决定了永久代的上限,但是类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。

  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  • Oracle 可能会将HotSpot 与 JRockit 合二为一。

2.5、类常量池、运行时常量池、字符串常量池

类常量池

在类编译过程中,会把类元信息存放到元空间(方法区),类元信息其中一部分便是类常量池

主要存放字面量(字面量一部分便是文本字符)和符号引用

运行时常量池

在类加载时,会将字面量和符号引用解析为直接引用存储在运行时常量池

(文本字符会在解析时查找字符串常量池,查出这个文本字符对应的字符串对象的直接引用,将直接引用存储在运行时常量池)

  • 在 JDK6,运行时常量池 存在于 方法区
  • 在 JDK7,运行时常量池 存在于 Java 堆

字符串常量池

存储的是字符串对象的引用,而不是字符串本身

字符串常量池在 jdk7 时就已经从方法区迁移到了 java 堆中(JDK8 时,方法区就是元空间)。

2.6、Minor GC、Major GC、Full GC 的区别

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC ):只是老年代的垃圾收集
  • 整堆收集(Full GC):收集整个 java 堆(young gen + old gen)和方法区的垃圾收集

Full GC 触发机制:

  1. 调用 System.gc 时,系统建议执行 Full GC,但是不必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存
  5. 由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  6. 当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载

2.7、堆空间分成不同区的原因

  • 堆空间分为新生代和老年代的原因

根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。

  • 新生代分为了 eden、Survivor 区的原因

为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。

如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发 Full GC,而 Full GC 是非常耗时的。

将 Eden 区满了的对象,添加到 Survivor 区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即 Survivor 相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。

  • 新生代的 Survivor 区又分为 s0 和 s1 区的原因:

分两个区的好处就是解决内存碎片化。

  • 为什么一个 Survivor 区不行?

假设现在只有一个survivor区,模拟一下流程:

新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

  • GC 优化的本质,也是为什么分代的原因:减少GC次数和GC时间,避免全区扫描。

2.8、GC(垃圾回收)

  • 在默认情况下,通过 System.gc() 或者Runtime.getRuntime().gc() 的调用,会显式触发 Full GC(完整的 GC 事件),对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
  • 在 GC 完成之前,整个 JVM 将冻结(即正在运行的所有服务将被暂停),通常完整的 GC 需要很长时间才能完成。
  • 因此在不合适的时间运行 GC,将导致不良的用户体验,甚至是崩溃。
  • JVM 具有复杂的算法,该算法始终在后台运行,进行所有计算以及有关何时触发 GC 的计算。当显式调用 System.gc() 调用时,所有这些计算都将被抛掉。

2.8、GC常用算法(垃圾回收)

分代收集算法(现在的虚拟机垃圾收集大多采用这种方式)

它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。

新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。

老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记-整理 或者 标记-清除。

标记-清除算法

每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。

标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点是可以避免内存碎片。

标记-压缩(标记-整理)算法

标记-压缩法是标记-清除法的一个改进版,和标记清除算法基本相同。

不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。

复制算法

复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。

当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来 GC 线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC 线程将更新存活对象的内存引用地址指向新的内存地址。

此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

五、JVM 的性能调优

1、调优参数

配置方式

  • java [options] MainClass [arguments]
  • options :JVM 启动参数。 配置多个参数的时候,参数之间使用空格分隔。
  • 参数命名: 常见为 -参数名
  • 参数赋值: 常见为 -参数名=参数值 或 -参数名:参数值

内存参数:

  • -Xms(s 为 strating):初始堆大小,JVM启动的时候,给定堆空间大小。可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。示例:-Xms3550m :设置 JVM 初始内存为 3550M。
  • -Xmx(x 为 max):最大堆大小,JVM运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。示例:-Xmx3550m :设置 JVM 最大可用内存为 3550M。
  • -Xmn(n 为 new):新生代大小。整个堆大小 = 新生代大小 + 老年代大小 + 持久代大小(jkd1.8废弃)持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的 3/8。 示例:-Xmn2g:设置年轻代大小为2G。
  • -Xss:设置每个线程的 Java 栈大小。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

JDK5.0 以后每个线程 Java 栈大小为1M,以前每个线程堆栈大小为 256K。示例:-Xss128k :设置每个线程的堆栈大小为128k。

  • -XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值。

示例:设置为 4 :年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5

-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。

注意 Survivor 区有两个。示例:设置为 3 :表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5。

  • -XX:MaxPermSize=n:设置永久代大小 示例:-XX:MaxPermSize=16m:设置持久代大小为16m。
  • -XX:MaxTenuringThreshold=n:设置垃圾最大年龄

如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。

如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。

2、垃圾回收器参数

VM给了三种选择:串行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况。

  • -XX:+UseSerialGC: 设置串行收集器。
  • -XX:+UseParallelGC: 设置并行收集器,表示年轻代使用并行收集器。
  • -XX:+UseParNewGC: 设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。
  • -XX:+UseParallelOldGC: 设置并行年老代收集器。JDK6.0 支持对年老代并行收集。
  • -XX:+UseConcMarkSweepGC: 设置年老代并发收集器 CMS。
  • -XX:+UseG1GC: 设置G1收集器
  • -XX:ParallelGCThreads=n: 设置并行收集器收集时最大线程数使用的CPU数。并行收集线程数。
  • -XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间,单位毫秒。可以减少STW时间。
  • -XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n) 并发收集器设置
  • -XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。
  • -XX:+UseAdaptiveSizePolicy: 设置此选项后,并行收集器会自动选择年轻代区大小和相应的 Survivor 区比例,以达到目标系统规定的最低相应时间或者收集频率等。此值建议使用并行收集器时,一直打开。
  • -XX:CMSFullGCsBeforeCompaction=n: 此值设置运行多少次 GC 以后对内存空间进行压缩、整理。因为并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。
  • -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片。

3、元空间参数:

  • -XX:MetaspaceSize:初始化的 Metaspace 大小,该值越大触发 Metaspace GC 的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控 Metaspace 的大小,而上下浮动主要由 -XX:MaxMetaspaceFreeRatio 和 -XX:MinMetaspaceFreeRatio 两个参数控制。在默认情况下,这个值大小根据不同的平台在 12M 到 20M 浮动。

使用 java -XX:+PrintFlagsInitial 命令查看本机的初始化参数。

  • -XX:MinMetaspaceFreeRatio:

当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增加 MetaspaceSize 的大小(为了避免过早引发一次垃圾回收)。默认值为40,也就是40%。

设置该参数可以控制 Metaspace 的增长的速度,太小的值会导致 Metaspace 增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致 Metaspace 增长的过快,浪费内存。

  • -XX:MaxMetaspaceFreeRatio:当进行过 Metaspace GC 之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会减小 MetaspaceSize 的大小。默认值为70,也就是70%。
  • -XX:MaxMetaspaceExpansion :Metaspace 增长时的最大幅度。默认值大约为5MB。
  • -XX:MinMetaspaceExpansion :Metaspace 增长时的最小幅度。默认值大约330KB。
  • -XX:MaxMetaspaceSize:最大空间。默认是没有限制的。

指定该值可以防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。

调优建议

1、年轻代大小选择

响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达老年代的对象。

吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8 CPU 以上的应用。

2、老年代大小选择

响应时间优先的应用:老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。

如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。

最优化的方案,一般需要参考以下数据获得:

  • 并发垃圾收集信息
  • 持久代并发收集次数
  • 传统GC信息
  • 花在年轻代和年老代回收上的时间比例
  • 减少年轻代和老年代花费的时间,一般会提高应用的效率

吞吐量优先的应用:

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代。

原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。较小堆引起的碎片问题,因为老年代的并发收集器使用标记-清除算法,所以不会对堆进行压缩。

当收集器回收时,它会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记-清除方式进行回收。

如果出现“碎片”,可能需要进行如下配置:

  • -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
  • -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC 后,对老年代进行压缩。

继续阅读