JVM学习之路(三)JVM调优实战


本系列文章:
   JVM学习之路(一)Java运行时区域、对象的创建过程、对象的内存布局、垃圾回收器
   JVM学习之路(二)类加载、Java内存模型、JVM调试命令、JVM调优案例
  JVM学习之路(三)JVM调优实战

  一般垃圾收集器跟内存大小的对应关系:

  1. Serial 几十兆
  2. PS 上百兆 - 几个G
  3. CMS - 20G
  4. G1 - 上百G
  5. ZGC - 4T - 16T(JDK13)
  • JVM调优指什么?两个目标:
  1. 尽量减少FGC;
  2. 减少STW时间。

一、JVM参数

  Java虚拟机参数类型 有三种,如图:

  常见垃圾回收器组合参数设定

  1. -XX:+UseSerialGC = Serial + Serial Old
      适用于小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器。
  2. -XX:+UseParNewGC = ParNew + SerialOld
      在一般未明确指定Old区垃圾回收器时,就代表用的是SerialOld。这个组合已经很少用(在某些版本中已经废弃)。
  3. -XX:+UseConc(urrent)MarkSweepGC = ParNew + CMS + Serial Old
      红色的字体代表在某些JDK版本上要加上,示例的在JDK1.8上不需要加红色的。
  4. -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认)
  5. -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
  6. -XX:+UseG1GC = G1

  JVM的参数分为三种:

  • 标准参数,-开头的参数, 所有版本JDK都支持,直接输入java 查看;
  • 非标准参数, -X开头的参数,输入java -X 查看;
  • 不稳定参数 -XX:(+/-), 每个版本可能不同, java -XX:+PrintFlagsFinal 查看,特别多,几百个 。

  常见的调优用的是 -X命令。
  如:java -XX:+PrintFlagsFinal命令可以查看所有的XX参数,如果在Linux环境下,可以+ |grep CMS 来过滤指定命令。
  一个测试小demo:

package Basic;

import java.util.List;
import java.util.LinkedList;

public class HelloGC {
    
    
  	public static void main(String[] args) {
    
    
    	System.out.println("HelloGC!");
    	List list = new LinkedList();
    	for(;;) {
    
    
    	  byte[] b = new byte[1024*1024];
     	  list.add(b);
    	}
  	}
}

  在Eclipse上编写此程序会,会在项目的bin目录下生成对应的HelloGC.class文件,如在"E:\WorkSpace\HelloWorld\bin\Basic"目录:
在这里插入图片描述
  在CMD窗口,切换到bin目录:
在这里插入图片描述
  然后就可以调试各种参数。当然也可以在Eclipse中设置JVM参数,此处以CMD方式为例。执行java -XX:+PrintCommandLineFlags Basic.HelloGC命令,可以看默认的虚拟机参数设置:
在这里插入图片描述
  图中的现象是:内存溢出(要区分内存泄露与内存溢出)。图中的参数:

-XX:InitialHeapSize=65738496 起始堆大小
-XX:MaxHeapSize=1051815936 最大堆大小
-XX:+UseCompressedClassPointers 之前提过的类型指针压缩
-XX:+UseCompressedOops 普通指针压缩

  假如要设置参数,可以使用类似命令:java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC Basic.HelloGC

-Xmn 新生代大小
-Xms 初始堆大小
-Xmx 最大堆大小
-XX:+PrintGC 显示GC回收信息

  在打印GC方面,除了PrintGC,还有一些类似的命令:

PrintGCDetails 打印更详细的信息
PrintGCTimeStamps 打印GC时的时间
PrintGCCauses 打印GC产生的原因

  上面命令的执行结果:
在这里插入图片描述
  发现最大堆大小、最小堆大小、年轻代大小已经设置成功。
  如果使用CMS的话,打印的信息会更详细一些,命令:java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags -XX:+PrintGC Basic.HelloGC,结果:
在这里插入图片描述
  要查看GC日志的详细意义,可以参考:
在这里插入图片描述

  一般未显示"Full GC",就代表是YGC;
  4544K->259K,分别代表回收前后的年轻代大小;6144K代表总的年轻代大小;
  后面的代表此次回收所用的时间;
  接着的4544K->4356K代表整个堆回收前后的大小;
  19840K代表整个堆的大小;
  后面的时间分别表示占了用户态多少时间、内核态多少时间、总共多少时间。

  发生OOM时的详细dump信息:
在这里插入图片描述
  total = eden + 1个survivor。

eden space 5632K, 94% used [0x00000000ff980000,0x00000000ffeb3e28,0x00000000fff00000)                    

  后面的内存地址指的是,起始地址,使用空间结束地址,整体空间结束地址。从第一个地址到第三个地址之间的长度是5632K。从第一个地址到第二个地址代表使用完的地址,占总地址的94%。
  Mataspeace,元数据区。最后一个是全元数据区所预留的全部空间,倒数第二个是已经占用的空间,倒数第一个是目前的容量是多少,第一个是真正使用的空间。

  1. 吞吐量:用户代码执行时间 /(用户代码执行时间 + 垃圾回收时间)
  2. 响应时间:STW越短,响应时间越好
  所谓调优,首先确定目的?吞吐量优先,还是响应时间优先?还是在满足一定的响应时间的情况下,要求达到多大的吞吐量?

  比如科学计算/数据挖掘,追求吞吐量优先。该种情况一般选用PS + PO
  网站、带界面的程序、对外程序的API,一般是响应时间优先。看JDK版本,优先选G1

二、具体调优实践

  调优可以简单分为三个方面:

  1. 根据需求进行JVM规划和预调优
  2. 优化JVM运行环境(慢,卡顿)
  3. 解决JVM运行过程中出现的各种问题(OOM)

2.1 调优,从规划开始

  调优,从业务场景开始,没有业务场景的调优都没有意义。
  无监控(也就是要进行压力测试,这样能看到结果),不调优。
  参考调优步骤:

  1. 熟悉业务场景(没有最好的垃圾回收器,只有最合适的垃圾回收器),根据业务场景来选择垃圾回收器。

响应时间、停顿时间 [CMS G1 ZGC] (需要给用户作响应)
吞吐量 = 用户时间 /( 用户时间 + GC时间) [PS]

  1. 选择回收器组合
  2. 计算内存需求。该步骤比较难以计算,范围较大,内存较小的话,可以回收的频繁一些。
  3. 选定CPU(越高越好)
  4. 设定年代大小、升级年龄
  5. 设定日志参数,给出两个例子:
      1)滚动日志:

-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause

    上面的命令意思是:按GC时间来生成滚动日志,最多可以生成5个,每个最大20M。
   2)或者每天产生一个日志文件

  看一些案例:

  • 案例1:垂直电商,最高每日百万订单,处理订单系统需要什么样的服务器配置?
      这个问题不太专业,因为垂直电商不可能做到每日百万订单。
      遇到这种问法时,还是需要去分析。比如考虑高峰访问量,假设一小时产生36w订单,即100订单/秒,高峰就再次假设1000订单/秒。
      接下来较考虑一个订单产生多少内存。即new出来订单对象,需要多少内存。假设一个订单对象为512k,1000订单总和是500M左右。
      这样新生代设置500M就可以,当然250M也可以,多回收几次就行。所以此时一般有响应时间要求,即在多少响应时间(比如100ms)内进行设计,然后进行压测。
      初次设定参数后,就可以进行压测,满足不了要求就扩大参数,再不行就加服务器数量
  • 案例2:12306遭遇春节大规模抢票应该如何支撑?
       12306应该是中国并发量最大的秒杀网站,号称并发量100W最高

    CDN -> LVS -> NGINX -> 业务系统 -> 每台机器1W并发(10K问题) 100台机器

  一般先从CDN开始,在全国做不同的CDN缓存,接下来是一堆的LVS,接下来就是NGINX,接下来就是Tomcat等服务器。
  Redis可以撑得住单机1w并发。
  此外,架构设计也是和业务逻辑紧密相关的。
  在商城付款流程中,普通电商订单 -> 下单 ->订单系统(IO)减库存,减库存和订单的生成应该是异步进行的,最后一步是用户付款。
  在具体的功能模块,比如订单生成,最后还会把压力压到一台服务器,可以做分布式本地库存 + 单独服务器做库存均衡。

  大流量的处理方法:分而治之

2.2 优化JVM运行环境

  • 问题1、有一个50万PV(页面浏览量)的资料类网站(从磁盘提取文档到内存)原服务器32位,1.5G的堆,用户反馈网站比较缓慢,因此公司决定升级,新的服务器为64位,16G的堆内存,结果用户反馈卡顿十分严重,反而比以前效率更低了?
  1. 原1.5G为什么慢?
      很多用户浏览数据,很多数据load到内存,内存不足,频繁GC,STW长,响应时间变慢。
  2. 为什么会更卡顿?
      内存变大,FGC时间变长
  3. 怎么解决?
      可以将PS换成PN + CMS 或者 G1(即使用响应时间优先的垃圾回收器)。

2.3 解决JVM运行过程中出现的各种问题

  • 问题1、系统CPU经常100%,如何调优?(面试高频)
      CPU100%那么一定有线程在占用系统资源。
  1. 找出哪个进程cpu高(top)
  2. 该进程中的哪个线程cpu高(top -Hp)
  3. 导出该线程的堆栈 (jstack)
  4. 查找哪个方法(栈帧)消耗时间 (jstack)

  CPU经常100%,需要考虑:工作线程占比高和垃圾回收线程占比高两种情况。

  • 问题2、系统内存飙高,如何查找问题?(面试高频)
  1. 导出堆内存 (jmap)
  2. 分析 (jhat jvisualvm mat jprofiler … )
  • 问题3、如何监控JVM
  1. jstat jvisualvm jprofiler arthas top…

2.3.1 用jstack定位锁相关问题

  用一个例子来尝试分析问题:

   package com.mashibing.jvm.gc;
   
   import java.math.BigDecimal;
   import java.util.ArrayList;
   import java.util.Date;
   import java.util.List;
   import java.util.concurrent.ScheduledThreadPoolExecutor;
   import java.util.concurrent.ThreadPoolExecutor;
   import java.util.concurrent.TimeUnit;
   
   /**
    * 从数据库中读取信用数据,套用模型,并把结果进行记录和传输
    */
   
   public class T15_FullGC_Problem01 {
    
    
   
       private static class CardInfo {
    
    
           BigDecimal price = new BigDecimal(0.0);
           String name = "张三";
           int age = 5;
           Date birthdate = new Date();
   
           public void m() {
    
    }
       }
   
       private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
               new ThreadPoolExecutor.DiscardOldestPolicy());
   
       public static void main(String[] args) throws Exception {
    
    
           executor.setMaximumPoolSize(50);
   
           for (;;){
    
    
               modelFit();
               Thread.sleep(100);
           }
       }
   
       private static void modelFit(){
    
    
           List<CardInfo> taskList = getAllCardInfo();
           taskList.forEach(info -> {
    
    
               // do something
               executor.scheduleWithFixedDelay(() -> {
    
    
                   //do sth with info
                   info.m();
   
               }, 2, 3, TimeUnit.SECONDS);
           });
       }
   
       private static List<CardInfo> getAllCardInfo(){
    
    
           List<CardInfo> taskList = new ArrayList<>();
   
           for (int i = 0; i < 100; i++) {
    
    
               CardInfo ci = new CardInfo();
               taskList.add(ci);
           }
   
           return taskList;
       }
   }

  使用java -Xms200M -Xmx200M -XX:+PrintGC com.mashibing.jvm.gc.T15_FullGC_Problem01让程序运行,输出GC信息。

  一般公司都使用网管监控软件,检测服务器,可以进行告警等操作。一般是运维团队首先受到报警信息(CPU Memory)

  接着开发才会进行下一步的定位分析。首先要先使用top命令找到内存使用和CPU占比较高的进程。
  然后用top -Hp + 进程ID查看该进程内的线程内存使用和CPU占比情况,观察进程中的线程,找到哪个线程CPU和内存占比高。示例:
在这里插入图片描述

top命令查看的是所有的进程信息,jps可以查看Java进程信息。

  图中第一列的PID就是该进程内的线程号。接下来就要用jstack命令定位具体的线程情况。"jstack + 进程ID"会把该进程的线程情况都列出来:
在这里插入图片描述
  上图中的NID是十六进制的线程号,用top -Hp + 进程ID命令看到的线程号是十进制的。
  此时就可以看到每个线程的状况,要重点关注的是线程的异常状态,如:WAITING、BLOCKED
在这里插入图片描述
  jstack中的主要线程状态:

RUNNABLE 线程运行中或I/O等待
BLOCKED 线程在等待monitor锁(synchronized关键字)
TIMED_WAITING 线程在等待唤醒,但设置了时限
WAITING 线程在无限等待唤醒

  看一段关键的日志信息:
在这里插入图片描述
  图中的"t2"是示例的用户线程名称,状态是WAITING,有这么一段:

waiting on <0x0000000088ca3310> (a java.lang.Object)

  即在等待着一把锁的释放。假如有一个进程中100个线程,很多线程都在waiting on <xx>,一定要找到是哪个线程持有这把锁,这时候一般是这个线程长期持有这把锁不释放。怎么找?搜索jstack dump的信息,找<xx> ,看哪个线程持有这把锁,状态一般是RUNNABLE。
  同时,此时也能看到出问题代码的具体位置:
在这里插入图片描述
  此时就明白阿里Java开发规范中,线程的名称(尤其是线程池)都要写有意义的名称。在使用线程池时,自定义线程名称的方式是:自定义ThreadFactory。

2.3.2 OOM问题的定位方式

  在上面的实验中,用到了jstack、top、top -Hp等命令,当然还有别的命令可以使用:

jps,查看Java进程信息
jinfo,查看一些配置信息,用法是jinfo+进程号,示例:
在这里插入图片描述
jstat,查看一些进程信息,但是内容较乱,不常用:
比如jstat -gc 动态观察gc情况;jstat -gc 4655 500 : 每过500个毫秒,动态打印GC的情况在这里插入图片描述

  远程的服务器一般是不安装图形化界面的,所以可以在本地和远程服务器建立连接,此时有个标准的协议JMX。也就是说如果要在本地和远程服务器建立连接,就需要在远程服务器上进行JMX的相关配置。一些配置的示例:

  1. 程序启动加入参数:

shell
java -Djava.rmi.server.hostname=192.168.17.11 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=11111 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false XXX

  1. 如果遭遇 Local host name unknown:XXX的错误,修改/etc/hosts文件,把XXX加入进去

192.168.17.11 basic localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

  1. 关闭linux防火墙(实战中应该打开对应端口)

service iptables stop
chkconfig iptables off #永久关闭

  1. windows上打开 jconsole远程连接 192.168.17.11:11111

  JConsole是JDK自带的图形化CPU监测工具,如果要连接远程服务器,需要连接远程进程:
在这里插入图片描述
  JConsole连接成功后的界面:
在这里插入图片描述
  类似工具JVirtualVM界面:
在这里插入图片描述
  该工具上添加远程连接,成功后界面:
在这里插入图片描述
在这里插入图片描述
  JVirtualVM能看到CPU、类、堆、线程的一些信息。
在这里插入图片描述
  下面的这张图是最直观的信息显示:有多少类,占多少个字节,有多少个实 例。了解这些信息,也大致能够进行问题定位了,因为有大量的对象未被回收,一定是相关代码出了问题。通过这种图形化界面工具,能够较简单地定位到OOM问题的原因
在这里插入图片描述
  那怎么定位OOM问题的?不是通过图形化工具。因为如果通过图形化界面定位OOM问题的话,代表在远程服务器上一直有个服务在后台运行。那么此时就会有两个问题:

  1. 已经上线的系统不用图形界面用什么来定位OOM问题?
      1)可以用arthas等命令行模拟图形化界面
      2)线上系统一般会配置一个参数:java -Xms20M -Xmx20M -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError,这个参数代表OOM的时候会自动产生堆转储文件。
      3)很多服务器备份(高可用),停掉这台服务器对其他服务器不影响
      4)在线定位(一般小点儿公司用不到)

  那要获取和JVirtualVM相似的查看对象数量、占用字节相似效果的话,需要用什么么?jmap。示例:jmap - histo 4655 | head -20,查看数量排名前20的对象信息:
在这里插入图片描述
  注意在线上系统中,尽量不使用jmap -dump:format=b,file=xxx pid类似的命令在线转储日志,jmap执行期间会对进程产生很大影响,甚至卡顿(电商不适合)。
2. 图形界面到底用在什么地方?测试!测试的时候进行监控!(压测观察)

  总结来说:先由运维团队报告问题,如CPU高、内存占用高等--->用top命令查出出问题的线程--->如果是锁相关的问题,就继续用stack进行定位到具体线程--->如果发现频繁GC,就用jmap定位到是什么对象一直占用内存,未被回收
  如果是数据库连接未释放之类的问题,不容易通过上述调试JVM的方式看出,需要看数据库连接池日志。

2.4 arthas在线排查工具

  为什么需要在线排查?在生产上我们经常会碰到一些不好排查的问题,例如线程安全问题,用最简单的threaddump或者heapdump不好查到问题原因。为了排查这些问题,有时我们会临时加一些日志,比如在一些关键的函数里打印出入参,然后重新打包发布,如果打了日志还是没找到问题,继续加日志,重新打包发布。对于上线流程复杂而且审核比较严的公司,从改代码到上线需要层层的流转,会大大影响问题排查的进度。
  线上系统一般不用图形化工具来排查问题,因为远程服务器没有装,如果本地连接远程服务器的话,还需要管理员来权限之类的。
  arthas不包含jmap功能
  arthas是阿里的开源在线分析工具。该工具的下载安装可以在github上寻找:
在这里插入图片描述
  该工具的文件目录:
在这里插入图片描述
  可以通过命令的方式,启动:
在这里插入图片描述
  此时输入"1",就可以将arthas绑定在该进程上,然后就可以使用arthas相关命令来观察该进程。绑定成功:
在这里插入图片描述
  help可以查看常用命令:
在这里插入图片描述
  jvm命令,可以查看JVM的相关信息:
在这里插入图片描述
  thread命令可以查看线程相关情况:
在这里插入图片描述
  thread + 线程号,可以查看某个线程的详细情况:
在这里插入图片描述
  dashboard命令,观察系统情况,类似于top命令效果:
在这里插入图片描述
  heapdump命令可以导出dump文件:
在这里插入图片描述

2.4.1 用jhat分析dump文件

  此时可以用jhat命令分析.hprof(dump)文件:
在这里插入图片描述
  图中表示用最多512M内存来分析,分析4244718个对象。
在这里插入图片描述
  jhat命令起了Server,7000端口,所以可以在浏览器进行访问jhat解析过的内容。示例:
在这里插入图片描述
  该界面对底部,还可以查看其它问题
在这里插入图片描述
  点击第三个、第四个可以查看对象的数量,类似于jmap:
在这里插入图片描述
  最底部的"OQL Query",可以查询特定问题对象:
在这里插入图片描述
  点某个,可以看某个对象所占用的字节数和相关的引用:
在这里插入图片描述

2.4.2 用JVirtualVM分析dump文件

  生产dump文件后,当然也可以用本地图形化工具分析,比如JVirtualVM:
在这里插入图片描述
在这里插入图片描述
  也可以使用OQL查询。
在这里插入图片描述

2.4.3 arthas的特有功能

  • 1、反编译
      jad命令可以用来反编译:
    在这里插入图片描述
      该功能可以排查动态代理相关的问题;还可以排查版本问题,即提交的代码是否被使用。
  • 2、热替换
      目前有些限制条件:只能改方法实现(方法已经运行完成),不能改方法名, 不能改属性。
      假设有这样两个文件:
    在这里插入图片描述
    在这里插入图片描述
      然后用javac命令编译这两个java文件。然后目前的情况是运行T,再随便输入,就会输出1:
    在这里插入图片描述
      此时直接改TT.java,改成输出"2",然后编译TT.java,接着:
    在这里插入图片描述
      就已经完成了热替换:
    在这里插入图片描述

三、调优案例(OOM产生的原因)

  OOM产生的原因多种多样,有些程序未必产生OOM,不断FGC(CPU飙高,但内存回收特别少)。如上面的示例代码,使用new ScheduledThreadPoolExecutor创建线程池,是有隐藏风险的。

  • 1、硬件升级系统反而卡顿的问题【需掌握】
      原因:堆内存变大,FGC的时间也会变长了。解决方法:使用合适的垃圾回收器。
  • 2、线程池不当运用产生OOM问题【需掌握】
      原因:利用Executors创建线程池(四种方式都有隐藏风险)会造成OOM问题。解决方法:使用ThreadPoolExecutor方式创建线程池。
  • 3、jira问题(排查不出真实原因的问题)
      现象是系统卡顿,从log来看不断FGC,查不出原因。像这种问题的通常解决方法:加内存 + 更换垃圾回收器(如G1)。
  • 4、tomcat http-header-size过大问题【需掌握】
      Tomcat配置中有个字数:max-http-header-size,该参数设置过大会产生问题,每来一个请求就会占用这么多内存(单位:字节),导致OOM。出问题的对象是Http11OutputBuffer,该对象产生过多:
    在这里插入图片描述
      解决方法:将该参数调小。
  • 5、直接内存溢出问题(少见)
      《深入理解Java虚拟机》P59,使用Unsafe分配直接内存,或者使用NIO的问题。
  • 6、栈溢出问题(较简单)
      java.lang.StackOverflowError 栈内存溢出。原因:-Xss设定太小。解决方法:将该参数设置大点。
      一个栈溢出的小例子:
public class StackOverFlow {
    
    
    public static void main(String[] args) {
    
    
        m();
    }

    static void m() {
    
     m(); }
}
  • 7、比较一下这两段程序的异同,分析哪一个是更优的写法【非案例】
Object o = null;
for(int i=0; i<100; i++) {
    
    
    o = new Object();
    //业务处理
}
for(int i=0; i<100; i++) {
    
    
    Object o = new Object();
}

  第一种写法较好,因为当重新创建对象时,之前创建的对象就可以回收,而第二种不能回收。

  • 8、重写finalize引发频繁GC
      现象是卡顿,原因是C++程序员写Java代码,仿照C++写法,重写了finalize方法,并且在finalize方法中进行了耗时操作,到时频繁GC。
  • 9、如果有一个系统,内存一直消耗不超过10%,但是观察GC日志,发现FGC总是频繁产生(较简单)
      原因:有人显式调用了System.gc()。

四、CMS日志

  假设执行以下命令进行测试:

java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.mashibing.jvm.gc.T15_FullGC_Problem01

  小例子:

[GC (Allocation Failure) [ParNew: 6144K->640K(6144K), 0.0265885 secs] 6585K->2770K(19840K), 0.0268035 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
ParNew:年轻代收集器
6144->640:收集前后的对比
(6144):整个年轻代容量
 6585 -> 2770:整个堆的使用情况
(19840):整个堆大小
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8511K(13696K)] 9866K(19840K), 0.0040321 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
    //CMS Initial Mark : 初始标记
	//8511 (13696) : 老年代使用(最大)
	//9866 (19840) : 整个堆使用(最大)
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.018/0.018 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
    //CMS-concurrent-mark : 并发标记
	//这里的时间意义不大,因为是并发执行
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
	//标记Card为Dirty,也称为Card Marking
[GC (CMS Final Remark) [YG occupancy: 1597 K (6144 K)][Rescan (parallel) , 0.0008396 secs][weak refs processing, 0.0000138 secs][class unloading, 0.0005404 secs][scrub symbol table, 0.0006169 secs][scrub string table, 0.0004903 secs][1 CMS-remark: 8511K(13696K)] 10108K(19840K), 0.0039567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    // CMS Final Remark  :  重新标记
	//STW阶段,YG occupancy:年轻代占用及容量
	//[Rescan (parallel):STW下的存活对象标记
	//weak refs processing: 弱引用处理
	//class unloading: 卸载用不到的class
	//scrub symbol(string) table: 
		//cleaning up symbol and string tables which hold class-level metadata and 
		//internalized string respectively
	//CMS-remark: 8511K(13696K): 阶段过后的老年代占用及容量
	//10108K(19840K): 阶段过后的堆占用及容量

[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
	//标记已经完成,进行并发清理
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
	//重置内部结构,为下次GC做准备

  对于CMS而言,在日志中更多的是,关注GC是否频繁,和耗费的时间在不在允许范围之内。

五、G1日志

  G1可以设置参数,表明每次回收建议的暂停时间,虚拟机参考这个时间,动态调整年轻代大小(以尽量拟合到设置的时间)。
  G1的调优目标:尽量不要FGC
  在G1的日志中,YGC和Mixed GC常常是混在一起的。

[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0015790 secs]
//young -> 年轻代 Evacuation-> 复制存活对象 (即表示是YGC)
//initial-mark 混合回收的阶段,这里是YGC混合老年代回收
   [Parallel Time: 1.5 ms, GC Workers: 1] //一个GC线程
      [GC Worker Start (ms):  92635.7]
      [Ext Root Scanning (ms):  1.1]
      [Update RS (ms):  0.0]
         [Processed Buffers:  1]
      [Scan RS (ms):  0.0]
      [Code Root Scanning (ms):  0.0]
      [Object Copy (ms):  0.1]
      [Termination (ms):  0.0]
         [Termination Attempts:  1]
      [GC Worker Other (ms):  0.0]
      [GC Worker Total (ms):  1.2]
      [GC Worker End (ms):  92636.9]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.0 ms]
   [Other: 0.1 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.0 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.0 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)]
//这是此次回收的一个总结:18.8M->18.8M,代表没有回收,有内存泄露。
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
//以下是混合回收其他阶段
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0000078 secs]
[GC concurrent-mark-start]
//无法evacuation(复制),进行FGC
[Full GC (Allocation Failure)  18M->18M(20M), 0.0719656 secs]
   [Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)], [Metaspace: 38
76K->3876K(1056768K)] [Times: user=0.07 sys=0.00, real=0.07 secs]

六、常用参数

  • -Xmn -Xms -Xmx -Xss
    年轻代 最小堆 最大堆 栈空间,示例:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
  • -XX:+UseTLAB
    使用TLAB,默认打开【一般不需要调整】
  • -XX:+PrintTLAB
     打印TLAB的使用情况【一般不需要调整】
  • -XX:TLABSize
     设置TLAB大小【一般不需要调整】
  • -XX:+DisableExplicitGC
     使显示调用System.gc()时不起作用 (System.gc()会产生FGC)。

System.gc()默认会触发一次Full GC,如果在代码中不小心调用了System.gc()会导致JVM间歇性的暂停。

  • -XX:+PrintGC
    打印GC信息
  • -XX:+PrintGCDetails
    打印GC详细信息
  • -XX:+PrintHeapAtGC
     GC的时候打印堆栈情况
  • -XX:+PrintGCTimeStamps
     打印GC发生时的时间戳
  • -XX:+PrintGCApplicationConcurrentTime (低)
     打印应用程序时间
  • -XX:+PrintGCApplicationStoppedTime (低)
      打印应用程序暂停时长
  • -XX:+PrintReferenceGC (重要性低)
     记录回收了多少种不同引用类型的引用
  • -verbose:class
     打印类加载详细过程
  • -XX:+PrintVMOptions
     可以在程序运行时,打印虚拟机接受的命令行显式参数
  • -XX:+PrintFlagsFinal / -XX:+PrintFlagsInitial
    打印出最终的参数 / 初始的参数【必须会用】
  • -Xloggc:opt/log/gc.log
    生成GC日志
  • -XX:MaxTenuringThreshold
    升代年龄(该参数主要是控制新生代需要经历多少次GC晋升到老年代中的最大阈值),最大值15
  • 锁自旋次数 -XX:PreBlockSpin 热点代码检测参数-XX:CompileThreshold

这些不建议更改

6.1 Parallel常用参数

  • -XX:SurvivorRatio
    Eden和S区的比例【一般不需要调整】

该值默认为8,即Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10。

  • -XX:PreTenureSizeThreshold
     大对象阈值,即大于这个值的参数直接在老年代分配。
  • -XX:MaxTenuringThreshold
  • -XX:+ParallelGCThreads
    并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同
  • -XX:+UseAdaptiveSizePolicy
     自动选择各区大小比例。
     开启:-XX:+UseAdaptiveSizePolicy;
     关闭:-XX:-UseAdaptiveSizePolicy

1)在 JDK 1.8 中,如果使用 CMS,无论 UseAdaptiveSizePolicy 如何设置,都会将 UseAdaptiveSizePolicy 设置为 false;不过不同版本的JDK存在差异;
2)UseAdaptiveSizePolicy不要和SurvivorRatio参数显示设置搭配使用,一起使用会导致参数失效;
3)由于AdaptiveSizePolicy会动态调整 Eden、Survivor 的大小,有些情况存在Survivor 被自动调为很小,比如十几MB甚至几MB的可能,这个时候YGC回收掉 Eden区后,还存活的对象进入Survivor 装不下,就会直接晋升到老年代,导致老年代占用空间逐渐增加,从而触发FULL GC,如果一次FULL GC的耗时很长(比如到达几百毫秒),那么在要求高响应的系统就是不可取的。

6.2 CMS常用参数

  • -XX:+UseConcMarkSweepGC
    使用CMS垃圾回收器
  • -XX:ParallelCMSThreads
    CMS线程数量
  • -XX:CMSInitiatingOccupancyFraction
     使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)
  • -XX:+UseCMSCompactAtFullCollection
     在FGC时进行压缩
  • -XX:CMSFullGCsBeforeCompaction
     多少次FGC之后进行压缩
  • -XX:+CMSClassUnloadingEnabled
     回收方法区不用的class
  • -XX:CMSInitiatingPermOccupancyFraction
     达到什么比例时进行Perm回收
  • -XX:GCTimeRatio
     设置GC时间占用程序运行时间的百分比
  • -XX:MaxGCPauseMillis
    停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代

6.3 G1常用参数

  • -XX:+UseG1GC
    使用GC垃圾回收器
  • -XX:MaxGCPauseMillis
    每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
  • -XX:GCPauseIntervalMillis
     GC的间隔时间
  • -XX:+G1HeapRegionSize
     一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数(1 2 4 8 16 32)。如果不设定,那么G1会根据Heap大小自动决定。。
     随着size增加,垃圾的存活时间更长,GC间隔更长,但每次GC的时间也会更长
    (ZGC对此做了改进,是动态区块大小)。
  • -XX:G1NewSizePercent
    新生代最小比例,默认为5%
  • -XX:G1MaxNewSizePercent
    新生代最大比例,默认为60%
  • -XX:GCTimeRatio
    GC时间建议比例,G1会根据这个值调整堆空间,值为0-100的整数
  • ConcGCThreads
     线程数量
  • InitiatingHeapOccupancyPercent
     启动G1的堆空间占用比例

七、纤程/协程

在这里插入图片描述

  线程和纤程的区别,一个通过内核空间,一个不通过内核空间。目前在Java中纤程可以通过第三方库 Quasar来实现。

八、常见问题

1.生产环境中,倾向于将最大堆内存和最小堆内存设置为:(为什么?)

A: 相同 B:不同

  A,好处是:
   1) 避免JVM在运行过程中向操作系统申请内存
   2)延后启动后首次GC的发生时机
   3)减少启动初期的GC次数

  1. 什么是响应时间优先?
      注重的是垃圾回收时STW的时间最短

  2. 什么是吞吐量优先?
      吞吐量是指应用程序线程用时占程序总用时的比例,也就是说尽量多让用户程序去执行。

  3. ParNew和PS的区别是什么?
      都是年轻代多线程收集器。
      ParNew 回收器是通过控制 垃圾回收 的 线程数 来进行参数调整,而 Parallel Scavenge 回收器更关心的是程序运行的吞吐量。即一段时间内,用户代码 运行时间占 总运行时间 的百分比。

  4. ParNew和ParallelOld的区别是什么?(年代不同,算法不同)
      前者是年轻代收集器,后者是老年代收集器,然后解释两者。

  5. 长时间计算的场景应该选择:吞吐量优先的收集器和策略。

  6. 大规模电商网站应该选择:停顿时间少(即响应时间快)的收集器和策略。

  7. JDK1.7 1.8 1.9的默认垃圾回收器是什么?如何查看?
      jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代);
      jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代);
      jdk1.9 默认垃圾收集器G1。
      java -XX:+PrintCommandLineFlags -version命令可以查看使用的垃圾回收器。

  8. 所谓调优,到底是在调什么?
      本人认为,是根据业务需要,在吞吐量和响应时间之间做出选择。

  9. 如果采用PS + ParrallelOld组合,怎么做才能让系统基本不产生FGC
      应该和下个问题的答案是有相通之处的。

  10. 如果采用ParNew + CMS组合,怎样做才能够让系统基本不产生FGC
      1)加大JVM内存
      2)加大Young(年轻代)的比例
      3)提高Y-O(最大值是15)的年龄
      4)提高S(survivor)区比例
      5)避免代码内存泄漏

  11. 如果G1产生FGC,你应该做什么?
      1)扩内存
      2)提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
      3)降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)。具体的参数是:

-XX:InitiatingHeapOccupancyPercent=45
  1. 问:生产环境中能够随随便便的dump吗?
      小堆影响不大,大堆会有服务暂停或卡顿(加live可以缓解),dump前会有FGC
  2. 问:常见的OOM问题有哪些?
       栈 堆 方法区

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/121504436