JVM 和垃圾回收机制

一、JVM

1.1、JVM 运行参数

1.1.1、标准参数

JVM 的标准参数,可以通过 java -help 检索出所有的标准参数。


C:\Users\alw>java -help
用法: java [-options] class [args...]
           (执行类)
   或  java [-options] -jar jarfile [args...]
           (执行 jar 文件)
其中选项包括:
    -d32          使用 32 位数据模型 (如果可用)
    -d64          使用 64 位数据模型 (如果可用)
    -server       选择 "server" VM
                  默认 VM 是 server.

    -cp <目录和 zip/jar 文件的类搜索路径>
    -classpath <目录和 zip/jar 文件的类搜索路径>; 分隔的目录, JAR 档案
                  和 ZIP 档案列表, 用于搜索类文件。
    -D<名称>=<>
                  设置系统属性
    -verbose:[class|gc|jni]
                  启用详细输出
    -version      输出产品版本并退出
    -version:<>
                  警告: 此功能已过时, 将在
                  未来发行版中删除。
                  需要指定的版本才能运行
    -showversion  输出产品版本并继续
    -jre-restrict-search | -no-jre-restrict-search
                  警告: 此功能已过时, 将在
                  未来发行版中删除。
                  在版本搜索中包括/排除用户专用 JRE
    -? -help      输出此帮助消息
    -X            输出非标准选项的帮助
    -ea[:<packagename>...|:<classname>]
    -enableassertions[:<packagename>...|:<classname>]
                  按指定的粒度启用断言
    -da[:<packagename>...|:<classname>]
    -disableassertions[:<packagename>...|:<classname>]
                  禁用具有指定粒度的断言
    -esa | -enablesystemassertions
                  启用系统断言
    -dsa | -disablesystemassertions
                  禁用系统断言
    -agentlib:<libname>[=<选项>]
                  加载本机代理库 <libname>, 例如 -agentlib:hprof
                  另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
    -agentpath:<pathname>[=<选项>]
                  按完整路径名加载本机代理库
    -javaagent:<jarpath>[=<选项>]
                  加载 Java 编程语言代理, 请参阅 java.lang.instrument
    -splash:<imagepath>
                  使用指定的图像显示启动屏幕
有关详细信息, 请参阅 http://www.oracle.com/technetwork/java/javase/documentation/index.html。

设置系统属性

public class HelloJVM {
    
    public static void main(String[] args) {
        //获取系统属性
        String province = System.getProperty("province");
        String city = System.getProperty("city");
        String district = System.getProperty("district");
        
        //打印信息
        System.out.println(province + " " + city + " " + district);
    }
}
java -Dprovince=beijing HelloJVM

idea设置系统属性
在这里插入图片描述
虚拟机的运行模式

JVM 虚拟机的运行模式有两种,在 32 位的虚拟机中支持 client 和 server 两种模式,而 64 位的虚拟机只支持 server 模式。

两种模式的区别:

  • client

    初始堆空间比较小,使用串行的垃圾回收器,它的目标是为了让虚拟机的启动速度更快,但运行速度比 server 模式要慢一些。

  • server

    初始堆空间更大,默认使用的是并行垃圾回收器,启动速度较慢,但运行的速度要比 client 的快的多。

虚拟机的启动模式可以通过 java -version 命令查看
要更改虚拟机的运行模式可在启动虚拟机时带上 -server-client 参数,也可以通过修改配置文件来永久更改虚拟机的运行模式。

jvm.cfg

# List of JVMs that can be used as an option to java, javac, etc.
# Order is important -- first in this list is the default JVM.
# NOTE that this both this file and its format are UNSUPPORTED and
# WILL GO AWAY in a future release.
#
# You may also select a JVM in an arbitrary location with the
# "-XXaltjvm=<jvm_dir>" option, but that too is unsupported
# and may not be available in a future release.
#
#当前64位虚拟机默认使用的是server模式,client模式为IGNORE不支持
-server KNOWN
-client IGNORE

1.1.2、-X 参数

-X 参数是非标准参数,在不同版本的虚拟机中,参数可能会有所不同,可通过 java -X 查看非标准参数。

C:\Users\alw>java -X

    -Xbatch           禁用后台编译
    -Xbootclasspath/a:<; 分隔的目录和 zip/jar 文件>
                      附加在引导类路径末尾
    -Xcheck:jni       对 JNI 函数执行其他检查
    -Xcomp            在首次调用时强制编译方法
    -Xdebug           为实现向后兼容而提供
    -Xdiag            显示附加诊断消息
    -Xfuture          启用最严格的检查,预期将来的默认值
    -Xint             仅解释模式执行
    -Xinternalversion
                      显示比 -version 选项更详细的 JVM
                      版本信息
    -Xloggc:<文件>    将 GC 状态记录在文件中(带时间戳)
    -Xmixed           混合模式执行(默认值)
    -Xmn<大小>        为年轻代(新生代)设置初始和最大堆大小
                      (以字节为单位)
    -Xms<大小>        设置初始 Java 堆大小
    -Xmx<大小>        设置最大 Java 堆大小
    -Xnoclassgc       禁用类垃圾收集
    -Xrs              减少 Java/VM 对操作系统信号的使用(请参见文档)
    -Xshare:auto      在可能的情况下使用共享类数据(默认值)
    -Xshare:off       不尝试使用共享类数据
    -Xshare:on        要求使用共享类数据,否则将失败。
    -XshowSettings    显示所有设置并继续
    -XshowSettings:all
                      显示所有设置并继续
    -XshowSettings:locale
                      显示所有与区域设置相关的设置并继续
    -XshowSettings:properties
                      显示所有属性设置并继续
    -XshowSettings:vm
                      显示所有与 vm 相关的设置并继续
    -XshowSettings:system
                      (仅 Linux)显示主机系统或容器
                      配置并继续
    -Xss<大小>        设置 Java 线程堆栈大小
    -Xverify          设置字节码验证器的模式
    --add-reads <模块>=<目标模块>(,<目标模块>)*
                      更新 <模块> 以读取 <目标模块>,而无论
                      模块声明如何。
                      <目标模块> 可以是 ALL-UNNAMED 以读取所有未命名
                      模块。
    --add-exports <模块>/<程序包>=<目标模块>(,<目标模块>)*
                      更新 <模块> 以将 <程序包> 导出到 <目标模块>,
                      而无论模块声明如何。
                      <目标模块> 可以是 ALL-UNNAMED 以导出到所有
                      未命名模块。
    --add-opens <模块>/<程序包>=<目标模块>(,<目标模块>)*
                      更新 <模块> 以在 <目标模块> 中打开
                      <程序包>,而无论模块声明如何。
    --illegal-access=<>
                      允许或拒绝通过未命名模块中的代码对命名模块中的
                      类型成员进行访问。
                      <>"deny""permit""warn""debug" 之一
                      此选项将在未来发行版中删除。
    --limit-modules <模块名>[,<模块名>...]
                      限制可观察模块的领域
    --patch-module <模块>=<文件>(;<文件>)*
                      使用 JAR 文件或目录中的类和资源
                      覆盖或增强模块。
    --disable-@files  禁止进一步扩展参数文件
    --source <版本>
                      设置源文件模式中源的版本。

这些额外选项如有更改,恕不另行通知。

实战:调整堆大小

#初始堆大小512M
#最大堆大小2G
java -Xms512m -Xmx2048m HelloJVM

默认的设置

初始堆大小:机器最大内存的 1/64

最大堆大小:机器最大内存的 1/4

查看默认的堆设置

java -XX:+PrintFlagsFinal -version | findstr /i "InitialHeapSize MaxHeapSize"

根据具体的情况适当的调整堆内存大小,可以充分利用服务器资源,让程序运行的更快。

1.1.3、-XX 参数

XX 参数也是非标准参数,主要用于虚拟机的调优和 debug 操作。

-XX 参数可分为两种配置类型,一种是 boolean 类型,一种是非 boolean 类型。
boolean 类型

格式:-XX:[+-]<name> 表示启用或禁用 <name> 属性

示例:-XX:+DisableExplicitGC 表示禁用手动调用GC操作,也就是说调用 System.gc() 无效

非 boolean 类型

格式:-XX:<name>=<value> 表示将 <name> 属性的值设置为 <value>

示例:-XX:NewRatio=1 表示新生代和老年代的比值

实战:禁用手动调用 GC

java -showversion -XX:+DisableExplicitGC HelloJVM

1.1.4、查看虚拟机运行参数
查看所有的虚拟机运行参数

使用 java -XX:+PrintFlagsFinal 查看所有的虚拟机运行参数

C:\Users\alw>java -XX:+PrintFlagsFinal
[Global flags]
      int ActiveProcessorCount                     = -1   
    uintx AdaptiveSizeDecrementScaleFactor         = 4    
    uintx AdaptiveSizeMajorGCDecayTimeScale        = 10   
    uintx AdaptiveSizePolicyCollectionCostMargin   = 50   
    uintx AdaptiveSizePolicyInitializingSteps      = 20   
    uintx AdaptiveSizePolicyOutputInterval         = 0    
    uintx AdaptiveSizePolicyWeight                 = 10   
    uintx AdaptiveSizeThroughPutPolicy             = 0    
    uintx AdaptiveTimeWeight                       = 25   
     bool AggressiveHeap                           = false
     bool AggressiveOpts                           = false
     intx AliasLevel                               = 3    
     bool AlignVector                              = false
    ccstr AllocateHeapAt                           =      
     intx AllocateInstancePrefetchLines            = 1    
     intx AllocatePrefetchDistance                 = 192  
     intx AllocatePrefetchInstr                    = 3    
     intx AllocatePrefetchLines                    = 4    
     intx AllocatePrefetchStepSize                 = 64   
     intx AllocatePrefetchStyle                    = 1    
     bool AllowJNIEnvProxy                         = false
     bool AllowNonVirtualCalls                     = false
     bool AllowParallelDefineClass                 = false
     bool AllowUserSignalHandlers                  = false
...略

看正在运行的虚拟机参数

要查看正在运行的服务启用了哪些虚拟机参数,可使用 jpsjinfo 工具。

jps -l
jinfo -flags 48288

1.2、Java 内存区域

Java 虚拟机在执行 Java 代码的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

根据《Java 虚拟机规范》规定,Java 虚拟机所管理的内存将会包括以下几个运行时数据区。
在这里插入图片描述

1.2.1、程序计数器

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程时通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行一个 Java 方法,这个计数器记录的正是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为空(undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

1.2.2、虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从调用直到执行完成的过程,就对于着一个栈帧在虚拟机栈中入栈到出栈的过程。

经常有人把 Java 内存分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java 内存区域的划分实际上远比这复杂。这种划分方式的流行只能说大多数程序员最关注的、与对象内存分配关系最密切的内存区域是这两块。其中所指的“堆”后面会专门讲到。而所指的“栈”就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、short、char、int、long、float、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(slot),其余的数据类型只占用 1 个。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在 Java 虚拟机规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可以动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

1.2.3、本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过时虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

1.2.4、Java 堆

对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在 Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Colleted Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;新生代又被划分为 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快的分配内存

根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过 -Xmx 和 -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

1.2.5、方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它永固存储已被虚拟机加载的类信息、常量、静态常量、及时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在 Sun 公司的 BUG 列表中,曾出现过若干个严重的 BUG 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。

根据 Java 虚拟机规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

1.2.6、运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(Constant Pool Table)用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

Java 虚拟机对 Class 文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个区域。不过,一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于 Class 文件常量池的另一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译器才能产生,也就是并非置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

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

1.2.7、直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也可能导致 OutOfMemoryError 异常出现。

在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因此避免了在 Java 堆和 Native 堆中来回复制数据。

1.2.8、永久代

《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它,HotSpot 虚拟机使用 PermGen space 永久代对方法区进行落地实现,其他虚拟机并没有 PermGen space 的概念,从 JDK8 开始,HotSpot 虚拟机已经移除了永久代。其实,早在 JDK7 时,永久代的移除工作就已经开始,例如字符串常量池在 JDK7 时就已经从永久代移除。

1.2.9、元空间

从 JDK8 开始,HotSpot 虚拟机彻底移除了永久代,使用元空间作为方法区的实现,元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。

使用元空间作为方法区的实现后,原来永久代中的符号引用存储在元空间中,字符串常量池和静态类型变量存储在堆空间中。

1.2.10、为什么要移除永久代

官网给出了解释:http://openjdk.java.net/jeps/122

This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.

移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。

  • 字符串存在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  • 将 HotSpot 与 JRockit 合二为一。

1.3、VisualVM

VisualVM 是一个集成了性能分析和运行监控工具的可视化程序,开发人员可通过 VisualVM 来监控和分析线程信息、查看CPU和内存的使用情况、监控 GC 等等。
运行 VisualVM

在 JDK 安装目录的 bin 目录,找到 jvisualmv.exe 程序打开即可。

抽样器可以对 CPU、内存进行一段时间的抽样分析。

为了能够看到内存溢出效果,我们调整一下堆内存大小

-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

二、垃圾回收机制

2.1、概述

垃圾收集(Garbage Collection,GC)是 Java 虚拟机的重要组成部分,它能够让软件开发人员更专注于代码实现,而不用过多的关注内存释放问题。

Java 程序在运行过程中会不断的产生一些不会再被用到的实例对象,如果不对这些对象进行清理,它们将会一直占用内存,直到某个时刻发生内存溢出的异常。GC 垃圾收集器的工作就是清理垃圾对象,整个过程无需开发人员干预。

2.2、判定对象是否可回收

当一个对象无法通过任何途径使用,那这个对象就是需要清理的垃圾,最常见的垃圾判定算法是使用 “引用计数器” 或 “可达性分析”。

2.2.1、引用计数器

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就+1;当引用失效时,计数器的值就-1;任何时刻计数器为0的对象就是不可能再被使用的。

引用计数器的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法。但是,至少在主流的 Java 虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

2.2.2、可达性分析

主流的虚拟机实现都是通过可达性分析来判定对象是否存活的。这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象时不可用的。
在这里插入图片描述上图中 object 5, object 6, object 7 虽然相互有关联,但是它们到 GC Roots 是不可达的,所以它们将会被判定为可回收。

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

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

2.3、垃圾收集算法

2.3.1、标记-清除算法

算法思想

最基础的收集算法是 “标记-清除” 算法,如同它的名字一样,算法分为 “标记” 和 “清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
在这里插入图片描述

标记-清除算法的不足

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

2.3.2、复制算法

算法思想

为了解决效率问题,一种称为 “复制” 的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块内存上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂问题,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
在这里插入图片描述

复制算法的不足

  • 这种算法的代价是将内存缩小为了原来的一半,未免太高了一点

2.3.3、标记-整理算法

算法思想

“标记-整理” 算法是建立在 “标记-清理” 算法基础之上的,其标记过程仍然与 “标记-清理” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存,从而解决内存碎片化问题。

在这里插入图片描述
标记-整理算法的不足

  • 与 “标记-清理” 算法一样,”标记-整理“ 算法也需要进行标记和清理,并且还多了一步整理过程,所以效率也比较低。

2.3.4、分代收集算法

算法思想

当前商业虚拟机的垃圾收集都采用 “分代收集” 算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对象进行分配担保,就必须使用 “标记-清理” 或者 “标记-整理” 算法来进行回收。

IBM 公司的专门研究表明,新生代中 98% 的对象都是 “朝生夕死” 的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活者的对象一次性地复制到另一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存会被 “浪费”。

当然,98% 的对象可回收只是一般场景下的数据,我们没办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。
在这里插入图片描述

2.4、内存分配与回收策略

2.4.1、对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

Minor GC 和 Full GC 的区别:

  • 新生代 GC(Minor GC)指发生在新生代的垃圾收集动作,因为 Java 对象大多数都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
  • 老年代 GC(Full GC)指发生在老年代的 GC,出现了 Full GC,经常会伴随着至少一次的 Minor GC(并非绝对,Parallel Scavenge 收集器的策略就可以单独进行 Full GC)。Full GC 的速度一般会比 Minor GC 慢 10 倍以上。

2.4.2、大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是很长的字符串以及数组(例如 new byte[1024 * 1024 * 10] //10MB)。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时,就提前除触发 GC 以获得足够的连续空间来 “安置” 它们。

虚拟机提供了一个 -XX:PretenureSizeThreshold 参数(只对Serial和ParNew两个新生代收集器有用),大于这个设置值的对象直接在老年代分配。这样做的目的是避免 Eden 区及两个 Survivor 区之间发生大量的内存复制。

2.4.3、长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为1.对象在 Survivor 区每 “熬过” 一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。

2.4.4、对象年龄判断

为了能更好的适应不同程序的内存情况,虚拟机并不是永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

在这里插入图片描述

2.5、垃圾收集器

2.5.1、概述

前面我们提到了垃圾收集的算法,但还需要具体的实现,在 JVM 中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器、CMS 垃圾收集器、G1 垃圾收集器。

在这里插入图片描述图中展示了七种不同的分代垃圾收集器,如果两个收集器之间存在连线,那么它们可以搭配使用,图中收集器所处的区域表示它们属于新生代或老年代收集器。

2.5.2、Serial 串行收集器

Serial 收集器是最基础、历史最悠久的收集器,曾经是 HotSpot 虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,当收集器工作时,必须暂停其他所有的工作线程,直到它完成收集工作。当它工作时,我们的应用程序处于暂停状态,所以我们也将这个时刻称之为 “Stop The World”。

Serial 收集器会暂停应用程序,这对交互性强的应用是不可接受的,所以一般 Web 应用不会使用该收集器。
在这里插入图片描述

2.5.3、ParNew 并行收集器

ParNew 收集器实质上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一致。

ParNew 收集器与 Serial 收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的 HotSpot 虚拟机,尤其是 JDK7 之前的遗留系统中首选的新生代收集器。

在这里插入图片描述

2.5.4、Parallel 收集器

Parallel 收集器是 JDK8 默认的新生代垃圾收集器,它的工作机制与 ParNew 垃圾收集器是一样的,只是在此基础之上,增加了两个和系统吞吐量相关的参数,使得其使用起来更加的灵活和高效。

  • -XX:MaxGCPauseMillis

设置垃圾收集时最大的停顿时间,单位为毫秒。
需要注意的是,Parallel 收集器为了达到设置的停顿时间,可能会调整堆大小或其他参数,如果堆设置的比较小,可能会导致 GC 工作变得更加的频繁。

  • -XX:UseAdaptiveSizePolicy

自适应 GC 模式,垃圾收集器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡。

所谓的吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,例如用户代码加上垃圾回收总耗时100秒,其中垃圾收集花掉了1秒钟,那吞吐量就是 99%。

高吞吐量能最大效率的利用处理器资源,低停顿时间能以更快的响应速度来提升用户的体验。

2.5.5、Serial Old 收集器

Serial Old 收集器是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用 标记-整理 算法。这个收集器主要意义也是供客户端模式下的 HotSpot 虚拟机使用。

在这里插入图片描述

2.5.6、Parallel Old 收集器

Parallel Old 收集器是 Parallel 收集器的老年代版本,支持多线程并发收集,基于 标记-整理 算法实现。

Parallel Old 收集器直到 JDK6 时才开始提供,在此之前,新生代的 Parallel 收集器只能与 Serial Old 收集器配合工作,由于 Serial Old 收集器在服务端在性能上的拖累(单线程无法充分利用服务器多处理器的并行处理能力),这种组合的总吞吐量甚至没有 ParNew + CMS 的组合来的优秀。

直到 Parallel Old 收集器 出现后,“吞吐量优先” 收集器终于有了名副其实的搭配组合,在注重吞吐量的场景可以有限考虑使用 Parallel 收集器 + Parallel Old 收集器这个组合。

在这里插入图片描述

2.5.7、CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种老年代垃圾收集器,CMS 收集器的目标是获取最短的停顿时间,目前很大一部分互联网应用使用的就是 CMS 收集器,因为这些应用更注重给用户带来良好的交互体验。

CMS 收集器是基于 标记-清除 算法实现的,它的运作过程相对复杂,分为四个步骤,包括:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中初始标记、重新标记着两个步骤仍然需要 “Stop The World”。初始标记仅仅只是标记一下 GC Roots 能够直接关联到的对象,速度很快;

并发标记阶段就是从 CG Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程。

重新标记阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。这个阶段的停顿时间比初始标记阶段的耗时会稍微长一些。

最后是并发清理阶段,清理掉标记阶段已经判定为死亡的对象。

在这里插入图片描述

2.5.8、G1 收集器(重点)

概述

对于垃圾回收器来说,前面收集器要么是一次性回收新生代,要么一次性回收老年代,但现在的服务器的空间已经很大了,为了优化垃圾收集操作,出现了 G1 收集器。

G1 收集器是一款软实时、低延迟、可设定目标(最大 STW 停顿时间)的垃圾收集器,用于替代 CMS,适用于较大的堆(大于4~6G),JDK9 之后默认使用 G1 收集器,其垃圾收集的时间能控制在 10 毫秒以内。(空间换时间)

G1 收集器的设计原则就是简化 Java 虚拟机调优,开发人员只需要简单的三步即可完成调优:

  1. 开启 G1 垃圾收集器
  2. 设置堆的最大内存
  3. 设置最大停顿时间(STW)

G1 的内存布局

G1 垃圾收集器相比于其他收集器而言,最大的不同就是不再使用固定的年轻代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的年轻代、老年代区域。G1 将这些区域作为单次回收的最小单元,这样即可避免在整个堆中进行全区域的垃圾收集。

G1 能够跟踪每个区域里面的垃圾堆积价值,并在后台维护一个优先级列表,根据用户设定的允许停顿时间,优先处理回收价值收益最大的那些区域。
在这里插入图片描述可以看到出现了一个新的区域 Humongous,这个区域属于老年代区,专门用来存储大对象,当某个对象的大小超过了 Region 容量的一般则直接存储到这个区域。如果一个 H 区装步下这个大对象,G1 会寻找连续的 H 区来存储,为了能够找到连续的 H 区,有时候不得不启动一次 Full GC。

G1 能够自动的给 Region 分配最优的大小,如果需要手动设置,只能设置 1m, 2m, 4m, 8m, 16m, 32m 这几个值,最小是1m,最大是 32m,且必须是 2 的幂次方。

在 G1 的划分区域中,新生代的垃圾收集依然采用暂停所有用户线程的方式,将存活的对象拷贝到老年代或 Survivor 空间,G1 收集器通过将对象从一个区域复制到另一个区域完成了清理工作,这意味着 G1 在复制对象的过程中就可以完成堆内存的整理工作。

垃圾收集模式

G1 有三种垃圾收集模式,分别是 Young GC、Mixed GC、Full GC。

  • Young GC

发生在新生代的 GC 算法,一般对象(除了大对象)都是在 Eden Region 中分配内存,当所有的 Eden Region 被耗尽,就会触发一次 Young GC。
这种机制和之前的 Young GC 差不多,执行完一次 Young GC,活跃对象会被拷贝到 Survivor Region 或晋升到 Old Region 中。

  • Mixed GC

当越来越多的对象晋升到 Old Region 时,为了避免堆内存被耗尽,虚拟机会触发一次混合的垃圾收集,即 Mixed GC,该算法除了回收整个 Young Region 之外,还会回收一部分的 Old Region。只回收一部分 Old Region 的原因要要对垃圾回收的耗时进行控制。Mixed GC和 CMS 相同的 标记-清除 算法,运作过程也是相同的。

  • Full GC

如果对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法是单线程执行 Serial Old GC,所以开发人员需要根据情况适当对虚拟机进行调优,尽可能的避免 Full GC。

G1 最佳实践

不断地对期望最大停顿时间进行调优!

通过 -XX:MaxGCPauseMillis=x 来设置期望的最大 GC 停顿时间,G1 在运行的时候会根据这个参数的设置在不同的应用场景中取得吞吐量和延迟之间的最佳平衡,一般设置在 200ms 左右,不能设置的太低,比如设置为 20ms 就不行,因为过低的期望停顿时间意味着收集器的处理速度可能会跟不上垃圾产生的速度,导致垃圾的慢慢堆积,最终因占满堆而引发 Full GC,反而降低了性能。

不要设置新生代和老年代的大小!

G1 收集器在运行的时候会自动的调整新生代和老年代的大小,通过改变代的大小来调整对象的晋升速度和晋升年龄,从而达到我们为收集器设置的期望暂停时间的目标。设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们只需要设置整个堆内存的大小,剩下的交给 G1 自己区分配各个代的大小。

2.5.9、垃圾收集器相关参数

参数描述
UseSerialGC虚拟机运行在 Client 模式下的默认值,打开此开关后,使用 Serial + Serial Old 的收集器组合进行内存回收。
UseParNewGC使用 ParNew + Serial Old 的收集器组合进行内存回收,JDK9 之后不再支持。
UseConcMarkSweepGC使用 ParNew + CMS + Serial Old 的组合进行内存回收。Serial Old 收集器将作为 CMS 收集器出现 “Concurrent Mode Failure” 失败后的后备收集器。
UseParallelGC使用 Parallel + Serial Old 的收集器组合进行内存回收。
UseParallelOldGC使用 Parallel + Parallel Old 的收集器组合进行内存回收。
SurvivorRatio新生代中 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden:Survivor=8:1
PretenureSizeThreshold直接晋升到老年代的对象大小,大于这个设定值的对象将直接在老年代分配。
MaxThenuringThreshold晋升到老年代的对象年龄,每个对象在坚持过一次 Minor GC 之后,年龄就增加 1,当超过这个参数值时就进入老年代。
UseAdaptiveSizePolicy动态调整 Java 堆中各个区域的大小以及进入老年代的年龄。
HandlePromotionFailure是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个 Eden 和 Survivor 区的所有对象都存活的极端情况。
ParalleGCThreads设置并行 GC 时进行内存回收的线程数
GCTimeRatioGC 时间占总时间的比率,默认值为 99,即允许 1% 的 GC 时间,仅在使用 Parallel 收集器时生效。
MaxGCPauseMillis设置 GC 的最大停顿时间,仅在使用 Parallel 收集器时生效。
UseG1GC使用 G1 收集器,JDK9 之后的 Server 模式默认值。
G1HeapRegionSize=n设置 Region 大小,并非最终值。
MaxGCPauseMillis设置 G1 收集过程期望的停顿时间,默认值是 200ms。
G1NewSizePercent新生代最小值,默认值是 5%。
G1MaxNewSizePercent新生代最大值,默认值是 60%。
ParallelGCThreads用户线程冻结期间并行执行的收集器线程数。
ConcGCThreads并发标记、并发整理的执行线程数,对不同的收集器,根据其能够并发的阶段有不同的含义。
InitiatingHeapOccupancyPerccent触发 GC 周期的 Java 堆占用率阈值,默认是 45%,这里的 Java 堆是指整个堆内存,而不是某个分代区域,值为 0 则表示”一直执行GC循环”。

2.5.10、JVM 默认的垃圾收集器

JDK1.7:Parallel Scavenge (新生代)+ Parallel Old (老年代)

JDK1.8:Parallel Scavenge (新生代)+ Parallel Old (老年代)

JDK1.9:G1

通过 -XX:+PrintGCDetails 打印 GC 日志,根据新生代、老年代名称判断

名称垃圾收集器
DefNew-XX:+UseSerialGC (新生代 Serial,老年代 Serial Old)
ParNew-XX:+UseParNewGC (新生代 ParNew,老年代 Serial Old)
-XX:+UseConcMarkSweepGC (新生代 ParNew,老年代 CMS + Serial Old)
PsYoungGen-XX:+UseParallelOldGC(新生代 Parallel,老年代 Parallel Old)
-XX:+UseParallelGC(新生代 Parallel,老年代 Serial Old)
grabage-first heap-XX:+UseG1GC(G1 收集器)

2.6、可视化 GC 日志分析工具

GC Easy 是一款在线的可视化工具,易用且功能强大。

http://gceasy.io/

public class GCTest {

    public static void main(String[] args) throws InterruptedException {
        List<String> list1 = new ArrayList<>();
        List<String> list2 = new ArrayList<>();

        while (true) {
            for (int i = 0; i < 1000; i++) {
                if (System.currentTimeMillis() % 2 == 0) {
                    list1.add("Element:" + System.currentTimeMillis());
                } else {
                    list2.add("Element:" + System.currentTimeMillis());
                }
            }

            list1.clear();
            TimeUnit.MILLISECONDS.sleep(50);
        }
    }
}

设置虚拟机参数

-XX:+UseParallelOldGC -XX:+PrintGCDetails -Xms16m -Xmx16m -Xloggc:gc.log

JVM 内存使用情况
在这里插入图片描述
左侧分别展示了新生代的内存分配大小(Allocated)和新生代空间大小的最大峰值(Peek)。

然后依次是老年代(Old Generation)、元空间(Meta Space)、总大小(Young + Old + Meta space)。

关键性能指标
在这里插入图片描述- Throughput 表示吞吐量

  • Latency 表示响应时间
    • Avg Pause GC Time 表示平均 GC 时间
    • Max Pause GC Time 表示最大 GC 时间
      在这里插入图片描述
  • Heap after GC:回收后堆的内存图
  • Heap before GC:回收前堆的使用率
  • GC Duration:GC 持续时间
  • Reclaimed Bytes:GC 回收掉的垃圾对象的大小
  • Young Gen:年轻代的内存分配情况
  • Old Gen:老年代的内存分配情况
  • Meta Space:元数据的内存分配情况
  • A & P:堆内存分配和晋升情况
    在这里插入图片描述
    引发 GC 的原因统计
    在这里插入图片描述
  • Ergonomics:老年代空间不足引发的 GC
  • Allocation Failure:新生代空间不足引发的 GC
  • Others:其他
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值