JVM性能监控与调优学习

JVM命令参数

  • 标准参数
    在jvm的各个版本中不会变的参数,例如:-help-server -client-version -showversion-cp -classpath
  • X参数
    非标准化参数,jvm的不同版本可能改动:-Xint(解释执行)-Xcomp(第一次使用就编译成本地代码)-Xmixed(混合模式,JVM自己来决定是否编译成本地代码)
  • XX参数
    非标准化参数,jvm的不同版本可能改动,主要是用于JVM调优和Debug。这个也是我们经常能用到的。
    参数分类:
    • Boolean类型
      格式:-XX:[±] 表示启用或者禁用name属性。
      比如:-XX:+UseConcMarkSweepGC -XX:+UseG1GC
    • 非Boolean类型
      格式:-XX:=表示name属性的值是value。
      比如:-XX:MaxGCPauseMillis = 500 -XX:GCTimeRatio=19
    • -Xms -Xms
      不是X参数,而是XX参数
      -Xms等价于 -XX:InitialHeapSize
      -Xmx等价于 -XX:MaxHeapSize

查看JVM运行时参数

  • -XX:+PrintFlagsInitial
    查看jvm初始值。
    在这里插入图片描述
    在这里插入图片描述
    = 代表默认值
    := 代表被用户或者JVM修改后的值

    这里查询是通过重新创建jvm线程来实现的。如果需要查询已经存在的jvm需要使用jinfo。

  • -XX:+PrintFlagsFinal: 查看jvm最终值。

  • -XX:+UnlockExperimentalVMOptions: 解锁实验参数,jvm中的参数并不是所有都能直接赋值的,有的需要解锁才能赋值。

  • -XX:+UnlockDiagnosticVMOptions: 解锁诊断参数。

  • -XX:+PrintCommandLineFlags: 打印命令行参数。

jps

jps是java查看进程的命令。例如:jps -l 查看所有的进程id。
在这里插入图片描述

jinfo

jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数。

  • jinfo -flag MaxHeapSize 801

    用来查询801Java进程的最大内存。
    在这里插入图片描述

  • jinfo -flags 6336

    返回的信息包含两部分,第一部分是我们手动赋值的jvm参数,第二部分是jvm的启动命令。
    在这里插入图片描述

jstat

jstat能够查看JVM统计信息,例如:类装载、垃圾收集、JIT编译。

  • jstat -class 801

    查询类加载信息:Loaded - 加载类个数 Unloaded - 卸载类的个数 Bytes - 大小
    在这里插入图片描述

  • jstat -gc 801 1000 3

    查看JVM的gc情况,每1000毫秒输出一次,总共输出3次。
    在这里插入图片描述
    在这里插入图片描述

jmap 导出内存映像文件

生产环境上如果发生了内存溢出,需要通过分析内存映像文件找出问题。有两种方式:

  • 内存溢出自动导出

    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath = ./

  • 使用jmap命令手动导出

    format = b 指定导出格式为二进制文件。
    file = heap.hprof 指定文件名称
    jmap -dump:format=b,file=heap.hprof 1153
    在这里插入图片描述

MAT分析内存溢出

MAC安装
  • 下载地址:https://www.eclipse.org/mat/downloads.php
  • 如下图所示选择mac版本在这里插入图片描述
  • 右键mat显示包内容,进入Contents->MacOS下面,会有一个MemoryAnalyzer的命令。
  • 打开终端,进入此路径找到MemoryAnalyzer,运行 ./MemoryAnalyzer -data ./dump
  • 导入上面我们dump到内存映像文件,我们就能进行分析。
使用介绍
  • 导入之后会有将内存进行分块展示。
    在这里插入图片描述
  • 我们还可以查看内存中对象到数量和占用到空间,对类名还支持模糊搜索。shallow Heap是不包含该对象内部的对象大小。
    在这里插入图片描述
  • 对一个对象我们可以查看是谁引用的,这里可以选中排除掉虚引用。
    在这里插入图片描述

jstack

jstack用来查看JVM中的线程情况,可以用来分析死锁。命令:jstack pid
在这里插入图片描述

jstack检测死锁

死锁代码

public class DeathLock {

    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();

    public static void deathLock() {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    lock1.lock();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    lock2.lock();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        t1.setName("mythread1");
        t2.setName("mythread2");
        t1.start();
        t2.start();
    }

    public static void main(String[] args) {
        deathLock();
    }
}

死锁日志

"mythread2" #12 prio=5 os_prio=0 tid=0x0000000058ef7800 nid=0x1ab4 waiting on condition [0x0000000059f8f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d610> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$2.run(DeathLock.java:34)

   Locked ownable synchronizers:
        - <0x00000000d602d640> (a java.util.concurrent.locks.ReentrantLock$Nonfa
irSync)

"mythread1" #11 prio=5 os_prio=0 tid=0x0000000058ef7000 nid=0x3e68 waiting on condition [0x000000005947f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d640> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$1.run(DeathLock.java:22)

   Locked ownable synchronizers:
        - <0x00000000d602d610> (a java.util.concurrent.locks.ReentrantLock$Nonfa
irSync)


Found one Java-level deadlock:
=============================
"mythread2":
  waiting for ownable synchronizer 0x00000000d602d610, (a java.util.concurrent.l
ocks.ReentrantLock$NonfairSync),
  which is held by "mythread1"
"mythread1":
  waiting for ownable synchronizer 0x00000000d602d640, (a java.util.concurrent.l
ocks.ReentrantLock$NonfairSync),
  which is held by "mythread2"

Java stack information for the threads listed above:
===================================================
"mythread2":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d610> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$2.run(DeathLock.java:34)
"mythread1":
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000d602d640> (a java.util.concurrent.lock
s.ReentrantLock$NonfairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInt
errupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(A
bstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Abstrac
tQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLo
ck.java:209)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)

        at DeathLock$1.run(DeathLock.java:22)

Found 1 deadlock.
jstack检测cpu高

步骤一:查看cpu占用高进程

top

Mem:  16333644k total,  9472968k used,  6860676k free,   165616k buffers
Swap:        0k total,        0k used,        0k free,  6665292k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND     
17850 root      20   0 7588m 112m  11m S 100.7  0.7  47:53.80 java       
 1552 root      20   0  121m  13m 8524 S  0.7  0.1  14:37.75 AliYunDun   
 3581 root      20   0 9750m 2.0g  13m S  0.7 12.9 298:30.20 java        
    1 root      20   0 19360 1612 1308 S  0.0  0.0   0:00.81 init        
    2 root      20   0     0    0    0 S  0.0  0.0   0:00.00 kthreadd    
    3 root      RT   0     0    0    0 S  0.0  0.0   0:00.14 migration/0 

步骤二:查看cpu占用高线程

top -H -p 17850

top - 17:43:15 up 5 days,  7:31,  1 user,  load average: 0.99, 0.97, 0.91
Tasks:  32 total,   1 running,  31 sleeping,   0 stopped,   0 zombie
Cpu(s):  3.7%us,  8.9%sy,  0.0%ni, 87.4%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:  16333644k total,  9592504k used,  6741140k free,   165700k buffers
Swap:        0k total,        0k used,        0k free,  6781620k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
17880 root      20   0 7588m 112m  11m R 99.9  0.7  50:47.43 java
17856 root      20   0 7588m 112m  11m S  0.3  0.7   0:02.08 java
17850 root      20   0 7588m 112m  11m S  0.0  0.7   0:00.00 java
17851 root      20   0 7588m 112m  11m S  0.0  0.7   0:00.23 java
17852 root      20   0 7588m 112m  11m S  0.0  0.7   0:02.09 java
17853 root      20   0 7588m 112m  11m S  0.0  0.7   0:02.12 java
17854 root      20   0 7588m 112m  11m S  0.0  0.7   0:02.07 java

步骤三:转换线程ID

printf "%x\n" 17880          
45d8

步骤四:定位cpu占用线程

jstack 17850|grep 45d8 -A 30
"pool-1-thread-11" #20 prio=5 os_prio=0 tid=0x00007fc860352800 nid=0x45d8 runnable [0x00007fc8417d2000]
   java.lang.Thread.State: RUNNABLE
        at java.io.FileOutputStream.writeBytes(Native Method)
        at java.io.FileOutputStream.write(FileOutputStream.java:326)
        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        - locked <0x00000006c6c2e708> (a java.io.BufferedOutputStream)
        at java.io.PrintStream.write(PrintStream.java:482)
        - locked <0x00000006c6c10178> (a java.io.PrintStream)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        - locked <0x00000006c6c26620> (a java.io.OutputStreamWriter)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.write(PrintStream.java:527)
        - eliminated <0x00000006c6c10178> (a java.io.PrintStream)
        at java.io.PrintStream.print(PrintStream.java:597)
        at java.io.PrintStream.println(PrintStream.java:736)
        - locked <0x00000006c6c10178> (a java.io.PrintStream)
        at com.demo.guava.HardTask.call(HardTask.java:18)
        at com.demo.guava.HardTask.call(HardTask.java:9)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745)

"pool-1-thread-10" #19 prio=5 os_prio=0 tid=0x00007fc860345000 nid=0x45d7 waiting on condition [0x00007fc8418d3000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000006c6c14178> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)

JVisualVM

VisualVM 是Netbeans的profile子项目,已在JDK6.0 update 7 中自带,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。在JDK_HOME/bin(默认是C:\Program Files\Java\jdk1.6.0_13\bin)目录下面,有一个jvisualvm.exe文件,双击打开,从UI上来看,这个软件是基于NetBeans开发的了。

VisualVM 提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序的详细信息。VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查看本地应用程序或远程主机上运行的应用程序的相关数据。此外,还可以捕获有关 JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。

相对于上面提到的命令,JVisualVM提供了可视化的界面,既可以连接本地也可以连接远程tomcat。

垃圾回收器优化

垃圾回收算法-可达性分析算法

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
在这里插入图片描述
在Java语言中,可作为GC Roots的对象包括下面几种:(京东)

a) 虚拟机栈中引用的对象(栈帧中的本地变量表);

b) 方法区中类静态属性引用的对象;

c) 方法区中常量引用的对象;

d) 本地方法栈中JNI(Native方法)引用的对象。

两点细节需要注意:

1、GC Root在对象图之外,是特别定义的“起点”,不可能被对象图内的对象所引用。不会出现对象间循环引用问题。
2、如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

常用的垃圾收集算法
  • 标记-清除算法

    这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

  • 复制算法

    为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

  • 标记-整理算法

    为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

  • 分代收集算

    分代收集算法是目前大部分JVM的垃圾收集器采用的算法。

    新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。当Eden没有足够空间的时候就会 触发jvm发起一次Minor GC。

    在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。所以年老代一般使用标记整理。

常见的垃圾收集器

下面一张图是HotSpot虚拟机包含的所有收集器
在这里插入图片描述
展示了7种不同分代的收集器,而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

  • 新生代收集器:Serial、ParNew、Parallel Scavenge;
  • 老年代收集器:Serial Old、Parallel Old、CMS;
  • 整堆收集器:G1;
并发垃圾收集和并行垃圾收集的区别

(A)并行(Parallel)

指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old;

(B)并发(Concurrent)

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS、G1(也有并行);

停顿时间 VS 吞吐量
  • 停顿时间:垃圾收集器做垃圾回收中断应用执行断时间。 通过 -XX:MaxGCPauseMillis 设置最大停顿时间。
  • 吞吐量:花在垃圾收集断时间和花在应用时间的占比。 通过 -XX:GCTimeRatio=可以设置比例,垃圾收集时间占:1/1+n。
Serial收集器

Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;

  • 特点

    针对新生代;
    采用复制算法;
    单线程收集;
    进行垃圾收集时,必须暂停所有工作线程,直到完成;即会"Stop The World";

Serial/Serial Old组合收集器运行示意图如下:
在这里插入图片描述

  • 设置参数

    “-XX:+UseSerialGC”:添加该参数来显式的使用串行垃圾收集器。

  • 总结

    串行垃圾收集器不适合web应用场景,停顿时间长,效率比较低。

ParNew收集器

ParNew垃圾收集器是Serial收集器的多线程版本。

  • 特点

    除了多线程外,其余的行为、特点和Serial收集器一样;
    在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

ParNew/Serial Old组合收集器运行示意图如下:
在这里插入图片描述

  • 设置参数

    “-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;

    “-XX:+UseParNewGC”:强制指定使用ParNew;

    “-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

  • 总结

    虽然是并行,但是还是会存在较长时间的stop world。

Parallel Scavenge收集器

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。

  • 特点

    有一些特点与ParNew收集器相似,例如:新生代收集器;采用复制算法;多线程收集。
    Parallel Scavenge收集器的目标则是达一个可控制的吞吐量。

  • 设置参数

    “-XX:MaxGCPauseMillis”: 控制最大垃圾收集停顿时间,大于0的毫秒数; MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁;

    “-XX:GCTimeRatio”:设置垃圾收集时间占总时间的比率,0<n<100的整数;GCTimeRatio相当于设置吞吐量大小;垃圾收集执行时间占应用程序执行时间的比例的计算方法是: 1 / (1 + n)。例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%–1/(1+19);默认值是1%–1/(1+99),即n=99;

    “-XX:+UseAdptiveSizePolicy”: 开启这个参数后,就不用手工指定一些细节参数,如:新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等; JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);

Serial Old收集器

Serial Old是 Serial收集器的老年代版本;

  • 特点

    针对老年代;
    采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);
    单线程收集;

Serial/Serial Old收集器运行示意图如下:
在这里插入图片描述

Parallel Old收集器

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;

  • 特点

    针对老年代;
    采用"标记-整理"算法;
    多线程收集;

Parallel Scavenge/Parallel Old收集器运行示意图如下:
在这里插入图片描述

  • 设置参数

    “-XX:+UseParallelOldGC”:指定使用Parallel Old收集器;

CMS收集器

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器;

  • 特点

    针对老年代;
    基于"标记-清除"算法(不进行压缩操作,产生内存碎片)。以获取最短回收停顿时间为目标;
    并发收集、低停顿;

  • 设置参数

    “-XX:+UseConcMarkSweepGC”:指定使用CMS收集器;

  • CMS实现机制

    根据GC的触发机制分为:周期性Old GC(被动)和主动Old GC

    周期性Old GC

    周期性Old GC,执行的逻辑也叫 BackgroundCollect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。
    在这里插入图片描述
    触发条件

    1、如果没有设置 UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(线上环境建议带上这个参数,不然会加大问题排查的难度)

    2、老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%

    3、永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled

    4、新生代的晋升担保失败(老年代是否有足够的空间来容纳全部的新生代对象或历史平均晋升到老年代的对象,如果不够的话,就提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败。)

    主动Old GC

    这个主动Old GC的过程,触发条件比较苛刻:

    1、YGC过程发生Promotion Failed,进而对老年代进行回收

    2、System.gc(),前提是添加了-XX:+ExplicitGCInvokesConcurrent参数

  • 垃圾回收流程

    采用“标记-清理”算法对老年代进行回收,过程可以说很简单,标记出存活对象,清理掉垃圾对象,但是为了实现整个过程的低延迟,实际算法远远没这么简单,整个过程分为如下几个部分:
    在这里插入图片描述

  • 总结

    CMS是一款优秀的收集器,它的主要优点是:并发收集、低停顿,但他有以下3个明显的缺点:

    1、CMS收集器对CPU资源非常敏感

    在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低。

    2、CMS处理器无法处理浮动垃圾

    CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾”

    3、 CMS是基于“标记–清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。

G1收集器

G1收集器收集器运行示意图如下:
在这里插入图片描述
一般的垃圾回收器把内存分成三类: Eden(E), Suvivor(S)和Old(O), 其中Eden和Survivor都属于年轻代,Old属于老年代,新对象始终分配在Eden里面,熬过一次垃圾回收的对象就被移动到Survisor区了,经过数次垃圾回收之后还活着的对象会被移到Old区。
在这里插入图片描述
跟其它垃圾回收器不一样的是:G1虽然也把内存分成了这三大类,但是在G1里面这三大类不是泾渭分明的三大块内存,G1把内存划分成很多小块, 每个小块会被标记为E/S/O中的一个,可以前面一个是Eden后面一个就变成Survivor了。
在这里插入图片描述
这么做给G1带来了很大的好处,由于把三块内存变成了几百块内存,内存块的粒度变小了,从而可以垃圾回收工作更彻底的并行化。

结合多种垃圾收集算法,空间整合,不产生碎片,从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法;

它回收的大致过程是这样的:

  • 在垃圾回收的最开始有一个短暂的时间段(Inital Mark)会停止应用(stop-the-world)
  • 然后应用继续运行,同时G1开始Concurrent Mark
  • 再次停止应用,来一个Final Mark (stop-the-world)
  • 最后根据Garbage First的原则,选择一些内存块进行回收。(stop-the-world)首先排序各个Region的回收价值和成本;然后根据用户期望的GC停顿时间来制定回收计划;最后按计划回收一些价值高的Region中垃圾对象;回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;由于它高度的并行化,因此它在应用停止时间(Stop-the-world)这个指标上比其它的GC算法都要好。

G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。

G1的缺点:如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个Heap的回收,那么G1要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。因此G1比较适合内存稍大一点的应用(一般来说至少4G以上),小内存的应用还是用传统的垃圾回收器比如CMS比较合适。

总结

G1通过在垃圾回收领域应用并行化的策略,把几块大内存块的回收问题,变成了几百块小内存的回收问题,使得回收算法可以高度并行化,同时也因为分成很多小块,使得垃圾回收的单位变成了小块内存,而不是整代内存,使得用户可能对回收时间进行配置,垃圾回收变得可以预期了。

GC调优步骤
  1. 开启GC日志
  2. 使用工具分析GC日志,gcviewer或者gceasy。主要观察GC发生原因,提升GC次数,吞吐量和降低停顿时间。
  3. 不断调整JVM参数,达到最优。
发布了44 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/hxyascx/article/details/103435731