JVM优化00

JVM优化

0.目标

  • 了解下我们为什么要学习JVM优化
  • 掌握jvm的运行参数以及参数的设置
  • 掌握jvm的内存模型(堆内存)
  • 掌握jmap命令的使用以及通过MAT工具进行分析
  • 掌握定位分析内存溢出的方法
  • 掌握jstack命令的使用
  • 掌握VisualJVM工具的使用

1.为什么学习JVM优化

在本地开发环境中我们很少会遇到需要对jvm进行优化的需求,但是到了生产环境,我们可能将有下面的需求:

  • 运行的应用“卡住了”,日志不输出,程序没有反应
  • 服务器CPU突然负载很高
  • 在多线程应用下,如何分配线程的数量?

我们不仅要让程序能跑起来,而且是可以跑的更快!可以分析解决在生产环境中所遇到的各种“棘手”的问题

2.jvm的运行参数

2.1 jvm的参数分三类

  • 标准参数

    • -help
    • -version
  • -X参数(非标准参数)

    • -Xint
    • -Xcomp
  • -XX参数(使用率较高)

    • -XX:newSize
    • -XX:+UseSerialGC

2.2 标准参数

jvm的标准参数,一般都是很稳定的,在未来的JVM版本中不会改变,可以使用java -help检索出所有的标准参数

案例1:-help和-version参数

[root@localhost ~]# java -help
[root@localhost ~]# java -version    ###查看jvm的版本

# -showversion参数是表示,先打印版本信息,再执行后面的命令,在调试时非常有用,后面会使用到。

案例2:通过-D设置系统属性参数

public class test{
    public static void main(String[] args) {
        String str = System.getProperty("str");
        if (str == null) {
            System.out.println("woniu");
        } else {
            System.out.println(str);
        }
    }
}

进行编译、测试

[root@node01 test]# javac test.java
[root@node01 test]# java test
[root@node01 test]# java -Dstr=zhangsanfeng test

案例3:-server和-client参数

可以通过-server或-client设置jvm的运行参数

它们的区别是Server VM的初始堆空间会大一些,默认使用的是并行垃圾回收器,启动慢运行快,Client VM相对来讲会保守一些,初始堆空间会小一些,使用串行的垃圾回收器,它的目标是为了让JVM的启动速度更快,但运行速度会比Server VM模式慢些,JVM在启动的时候会根据硬件和操作系统自动选择使用Server还是Client类型的JVM

在32位操作系统中,如果是Windows系统,不论硬件配置如何,都默认使用Client类型的JVM;如果是其他操作系统上,机器配置有2GB以上的内存同时有2个以上CPU的话默认使用server模式,否则使用client模式

在64位操作系统中,只有server类型,不支持client类型。

测试:

[root@localhost ~]# java -client  -showversion test
结果:
java version "1.8.0_301"
Java(TM) SE Runtime Environment (build 1.8.0_301-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.301-b09, mixed mode)

woniu
[root@localhost ~]# java -server  -showversion test  ###结果和上面的一样
#由于机器是64位系统,所以不支持client模式

2.3 -X参数

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

[root@localhost ~]# java -X
    -Xmixed           混合模式执行(默认)
    -Xint             仅解释模式执行
    -Xbootclasspath:<: 分隔的目录和 zip/jar 文件>
                      设置引导类和资源的搜索路径
    -Xbootclasspath/a:<: 分隔的目录和 zip/jar 文件>
                      附加在引导类路径末尾
    -Xbootclasspath/p:<: 分隔的目录和 zip/jar 文件>
                      置于引导类路径之前
    -Xdiag            显示附加诊断消息
    -Xnoclassgc        禁用类垃圾收集
    -Xincgc           启用增量垃圾收集
    -Xloggc:<file>    将 GC 状态记录在文件中(带时间戳)
    -Xbatch           禁用后台编译
    -Xms<size>        设置初始 Java 堆大小
    -Xmx<size>        设置最大 Java 堆大小
    -Xss<size>        设置 Java 线程堆栈大小
    -Xprof            输出 cpu 分析数据
    -Xfuture          启用最严格的检查,预计会成为将来的默认值
    -Xrs              减少 Java/VM 对操作系统信号的使用(请参阅文档)
    -Xcheck:jni       对 JNI 函数执行其他检查
    -Xshare:off       不尝试使用共享类数据
    -Xshare:auto      在可能的情况下使用共享类数据(默认)
    -Xshare:on        要求使用共享类数据,否则将失败。
    -XshowSettings    显示所有设置并继续
    -XshowSettings:system
                      (仅限 Linux)显示系统或容器
                      配置并继续
    -XshowSettings:all
                      显示所有设置并继续
    -XshowSettings:vm 显示所有与 vm 相关的设置并继续
    -XshowSettings:properties
                      显示所有属性设置并继续
    -XshowSettings:locale
                      显示所有与区域设置相关的设置并继续

-X 选项是非标准选项。如有更改,恕不另行通知。

案例1:-Xint、-Xcomp、-Xmixed

  • 在解释模式(interpreted mode)下,-Xint标记会强制JVM执行所有的字节码,当然这会降低运行速度,通常低10倍或更多
  • -Xcomp(compile)参数与它(-Xint)正好相反,JVM在第一次使用时会把所有的字节码编译成本地代码,从而带来最大程度的优化。然而,很多应用在使用-Xcomp也会有一些性能损失,当然这比使用-Xint损失的少,原因是-Xcomp没有让JVM启用JIT编译器的全部功能。JIT编译器可以对是否需要编译做判断,如果所有代码都进行编译的话,对于一些只执行一次的代码就没有意义了
  • -Xmixed是混合模式,将解释模式与编译模式进行混合使用,由jvm自己决定,这是jvm默认的模式,也是推荐使用的模式
#强制设置为解释模式 
[root@node01 test]# java -showversion -Xint test  

#强制设置为编译模式 
[root@node01 test]# java -showversion -Xcomp test
#注意:编译模式下,第一次执行会比解释模式下执行慢一些,注意观察

#默认的混合模式 
[root@node01 test]# java -showversion test

2.4 -XX参数

案例1:-XX参数也是非标准参数,主要用于jvm的调优和debug操作

XX参数的使用有2种方式,一种是boolean类型,一种是非boolean类型:

  • boolean类型
    • 格式: -XX:[±]表示启用或者禁用属性
    • 如:-XX:+DisableExplicitGC 表示禁用手动调用gc操作,也就是说调用System.gc()无效
  • 非boolean类型
    • 格式:-XX:=表示属性的值为
    • 如:-XX:NewRatio=1 表示新生代和老年代的比值
[root@node01 test]# java -showversion -XX:+DisableExplicitGC test

案例2:-Xms和-Xmx参数:-Xms与-Xmx分别是设置jvm的堆内存的初始大小和最大大小。

-Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。

-Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。

适当的调整jvm的内存大小,可以充分利用服务器资源,让程序跑的更快。

[root@node01 test]# java -Xms512m -Xmx2048m test 

###运行一个jar包
[root@node01 test]#nohup java -jar -Xms512m -Xmx2048m -XX:PermSize=64m -XX:MaxPermSize=128m  xx.jar &

**案例3:**运行java命令时打印出运行参数 -XX:+PrintFlagsFinal

[root@node01 test]#java -XX:+PrintFlagsFinal -version
[Global flags]
intx ActiveProcessorCount                      = -1                                  {product}
uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {product}
uintx AdaptiveSizePausePolicy                   = 0                                   {product}
uintx AdaptiveSizePolicyCollectionCostMargin    = 50                                  {product}
uintx AdaptiveSizePolicyInitializingSteps       = 20                                  {product}
uintx AdaptiveSizePolicyOutputInterval          = 0                                   {product}
uintx AdaptiveSizePolicyWeight 
..............................
 bool PrintFlagsFinal                          := true                                {product}
 bool PrintFlagsInitial                         = false                               {product}
 bool PrintGC                                   = false                               {manageable}
 bool PrintGCApplicationConcurrentTime          = false                               {product}
 bool PrintGCApplicationStoppedTime             = false                               {product}
 bool PrintGCCause                              = true                                {product}
 bool PrintGCDateStamps                         = false                               {manageable}
 bool PrintGCDetails                            = false                               {manageable}
 bool PrintGCID                                 = false                               {manageable}
 bool PrintGCTaskTimeStamps                     = false                               {product}
 bool PrintGCTimeStamps                         = false                               {manageable}
 bool PrintHeapAtGC                             = false                               {product rw}
 bool PrintHeapAtGCExtended                     = false                               {product rw}
 bool PrintHeapAtSIGBREAK                       = true                                {product}
 bool PrintJNIGCStalls                          = false 
 ..........................

由上述的信息可以看出,参数有boolean类型和数字类型,值的操作符是=或:=,分别代表默认值和被修改的值

[root@node01 test]#java -XX:+PrintFlagsFinal -XX:+VerifySharedSpaces -version
[root@localhost ~]# java -XX:+PrintFlagsFinal -XX:+VerifySharedSpaces -version | grep PrintFlagsFinal

**案例4:**查看正在运行的jvm参数

在linux上启动tomcat

[root@localhost root]#jps    #通过jps 或者 jps -l 查看java进程
[root@localhost root]#jps -l
[root@localhost root]#jinfo -flags 8392  ####查看所有的参数,用法:jinfo -flags <jar的进程id>

3.jvm的内存模型

jvm的内存模型在1.7和1.8有较大的区别,我们以1.8为例进行讲解,但是我们也是需要对1.7的内存模型有所了解,所以接下里,我们将先学习1.7再学习1.8的内存模型

3.1 JDK1.7的堆内存情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5uMGwVTF-1687316984595)(assets/image-20220321171747053.png)]

  • Young区(年轻代区):Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间

  • Tenured 年老区:Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。

  • Perm 永久区:Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。

  • Virtual区:最大内存和初始内存的差值,就是Virtual区。

3.2 JDK1.8的堆内存情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-45b4mxiW-1687316984597)(assets/image-20220321172319640.png)]

由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。

年轻代:Eden + 2*Survivor

年老代:OldGen

在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。

需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MMHqVeFX-1687316984598)(assets/image-20220321172405394.png)]

3.3 为什么要废弃1.7的永久区

官网给出了解释: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没有永久代,不需要配置永久代。
现实使用中,由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。

3.4 jstat命令查看堆内存情况

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下

jstat [-命令选项] [进程id] [间隔时间/毫秒] [查询次数]

案例1:查看class类加载统计

[root@localhost ~]# jps
[root@localhost ~]# jstat -class 8392   ##8392为进程id

Loaded  Bytes  Unloaded  Bytes     Time   
  2858  5691.3        0     0.0     265.38

说明:

Loaded:加载class的数量,加载了2858个类
Bytes:所占用空间大小,占用了5691.3个字节,差不多5m
Unloaded:未加载数量
Bytes:未加载占用空间
Time:时间

案例2:查看编译统计

[root@localhost ~]# jstat -compiler 8392

Compiled Failed Invalid   Time   FailedType FailedMethod
    1753      1       0     2.18          1 org/apache/tomcat/util/IntrospectionUtils setProperty

说明:
Compiled:编译数量。
Failed:失败数量
Invalid:不可用数量
Time:时间
FailedType:失败类型
FailedMethod:失败的方法

案例3:垃圾回收统计

[root@localhost ~]# jstat -gc 8392

 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
2048.0 2048.0 2048.0  0.0   16384.0  14615.8   40960.0    10140.7   17408.0 16779.3 2048.0 1879.2      4    0.033   0      0.000    0.033

#也可以指定打印的间隔和次数,每1秒中打印一次,共打印5次
[root@node01 ~]# jstat -gc 8392 1000 5

说明:
S0C:第一个Survivor区的大小(KB)
S1C:第二个Survivor区的大小(KB)
S0U:第一个Survivor区的使用大小(KB)
S1U:第二个Survivor区的使用大小(KB)
EC:Eden区的大小(KB)
EU:Eden区的使用大小(KB)
OC:Old区大小(KB)
OU:Old使用大小(KB)
MC:方法区大小(KB)
MU:方法区使用大小(KB)

CCSC:压缩类空间大小(KB)

CCSU:压缩类空间使用大小(KB)

YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

4. jmap的使用以及内存溢出分析

前面通过jstat可以对jvm堆的内存进行统计分析,而jmap可以获取到更加详细的内容,如:内存使用情况的汇总、对内存溢出的定位与分析。

4.1 查看内存使用情况

[root@localhost ~]# jmap -heap 8392
Attaching to process ID 8392, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.301-b09

using thread-local object allocation.
Mark Sweep Compact GC

Heap Configuration:   #堆内存的配置信息
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 994050048 (948.0MB)
   NewSize                  = 20971520 (20.0MB)
   MaxNewSize               = 331350016 (316.0MB)
   OldSize                  = 41943040 (40.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:    # 堆内存的使用情况
New Generation (Eden + 1 Survivor Space):  #年轻代
   capacity = 18874368 (18.0MB)
   used     = 4340856 (4.139762878417969MB)
   free     = 14533512 (13.860237121582031MB)
   22.998682657877605% used
Eden Space:
   capacity = 16777216 (16.0MB)
   used     = 3042816 (2.90185546875MB)
   free     = 13734400 (13.09814453125MB)
   18.1365966796875% used
From Space:
   capacity = 2097152 (2.0MB)
   used     = 1298040 (1.2379074096679688MB)
   free     = 799112 (0.7620925903320312MB)
   61.89537048339844% used
To Space:
   capacity = 2097152 (2.0MB)
   used     = 0 (0.0MB)
   free     = 2097152 (2.0MB)
   0.0% used
tenured generation:#年老代
   capacity = 41943040 (40.0MB)
   used     = 11353328 (10.827377319335938MB)
   free     = 30589712 (29.172622680664062MB)
   27.068443298339844% used

15693 interned Strings occupying 1516912 bytes.

4.2 查看内存中对象数量及大小

#查看所有对象,包括活跃以及非活跃的
[root@localhost ~]# jmap -histo pid | more
#查看活跃对象
[root@localhost ~]# jmap -histo:live 8392 |more

num     #instances         #bytes  class name
----------------------------------------------
   1:         30459        3098384  [C
   2:          2075        2814168  [B
   3:          4476        1957032  [I
   4:         29962         719088  java.lang.String
   5:         15304         489728  java.util.HashMap$Node
   6:          4497         395736  java.lang.reflect.Method
   7:          3151         357264  java.lang.Class
   8:          3939         217352  [Ljava.lang.Object;
   9:          6467         206944  java.util.concurrent.ConcurrentHashMap$Node
  10:           918         169536  [Ljava.util.HashMap$Node;
  11:          1486          71328  java.util.HashMap
  12:          4145          66320  java.lang.Object
  13:            89          60016  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  14:          2497          52912  [Ljava.lang.Class;
  15:          1598          51136  java.util.Hashtable$Entry
  16:           823          49456  [Ljava.lang.String;
  17:          1202          48080  java.lang.ref.Finalizer
  18:           908          43584  org.apache.tomcat.util.modeler.AttributeInfo
  19:           926          37040  java.util.TreeMap$Entry
  20:           602          28896  java.util.concurrent.locks.ReentrantReadWriteLock$NonfairSync
  21:          1175          28200  java.util.ArrayList
  22:           375          27000  java.util.logging.Logger
  23:           645          25800  java.lang.ref.SoftReference
  24:           761          24352  java.util.concurrent.locks.ReentrantLock$NonfairSync
  25:          1001          24024  java.util.LinkedList$Node

对象说明:
B byte :[B表示byte数组有2075个,占用字节2814168
C char :[C表示有char类型的数组30459个,占用字节3098384
D double
F float
I int: [I表示有Integer类型的数组4476个,赵勇字节1957032
J long
Z boolean
[ 数组,如[I表示int[]
[L+类名 其他对象

4.3 将内存使用情况dump到文件中

有些时候我们需要将jvm当前内存中的情况dump到文件中,然后对它进行分析,jmap也是支持dump到文件中的

#语法
jmap -dump:format=b,file=dumpFileName <pid>     #b 表示二进制文件
[root@localhost ~]# jmap -dump:format=b,file=/tmp/dump.bat 8392

可以看到已经在/tmp下生成了dump.dat的文件。

4.4 jhat命令对dump文件进行分析

在上一小节中,我们将jvm的内存dump到文件中,这个文件是一个二进制的文件,不方便查看,这时我们可以借助于jhat工具进行查看。

#语法
jhat -port <port> file
[root@localhost tmp]# jhat -port 9999 /tmp/dump.bat

打开浏览器进行访问:http://192.172.0.5:9999/

显示的内容如下:

All Classes (excluding platform)

Package

在最后面有Execute Object Query Language (OQL) query功能

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QRnfFLyc-1687316984602)(assets/image-20220321192944341.png)]

点击进入,执行查询语句

select s from java.lang.String s where s.value.length>=1000

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aErJB5RA-1687316984603)(assets/image-20220321193050972.png)]

4.5 通过MAT工具对dump文件进行分析

1.MAT工具介绍

MAT(Memory Analyzer Tool),一个基于Eclipse的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。

官网地址:https://www.eclipse.org/mat/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-InIjz0qd-1687316984605)(assets/image-20220321193449603.png)]

2.下载安装

下载地址:https://www.eclipse.org/mat/downloads.php

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ty9ZYFhh-1687316984605)(assets/image-20220321193600998.png)]

将下载得到的MemoryAnalyzer-1.8.0.20180604-win32.win32.x86_64.zip进行解压:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dEy0EpQS-1687316984606)(assets/image-20220321194706698.png)]

3.使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bs1XYlvn-1687316984608)(assets/image-20220321194732713.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FzEqT60j-1687316984610)(assets/image-20220321194750650.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7lNhOZpp-1687316984610)(assets/image-20220321195018313.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y3wPuPur-1687316984611)(assets/image-20220321195258758.png)]

如:直接输入char回车

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jWhAmNab-1687316984612)(assets/image-20220321195416409.png)]

查看对象以及它的依赖:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lrQT4mTB-1687316984613)(assets/image-20220321195504775.png)]

Shallow Heap:表示对象本身占用内存的大小,也就是对象头加成员变量(不是成员变量的值)的总和

retained heap:如果这个对象被删除了(GC回收掉),能节省出多少内存

查看可能存在内存泄露的分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uQRxVLqV-1687316984613)(assets/image-20220321195812436.png)]

The classloader/component “java.net.URLClassLoader @ 0xd897cca0” occupies 895,696 (12.40%) bytes. The memory is accumulated in one instance of “java.lang.Object[]” loaded by “”.

类加载器/组件“java.net.URLClassLoader@0xd897cca0”占用895696(12.40%)字节。内存累积在“”加载的“java.lang.Object[]”的一个实例中。

2,525 instances of “java.lang.Class”, loaded by “” occupy 1,936,248 (26.81%) bytes.

可疑的泄露2,大对象

Biggest instances:
•class sun.util.calendar.ZoneInfoFile @ 0xd89a85f8 - 161,936 (2.24%) bytes.
•class sun.security.util.CurveDB @ 0xd8d5c9c8 - 101,160 (1.40%) bytes.
•class sun.nio.cs.ext.EUC_KR @ 0xd8d57a08 - 97,752 (1.35%) bytes.
•class sun.nio.cs.ext.Big5 @ 0xd8d57530 - 91,552 (1.27%) bytes.
•class sun.util.resources.TimeZoneNames @ 0xd9044488 - 87,456 (1.21%) bytes.
•class java.lang.System @ 0xd89b8260 - 86,480 (1.20%) bytes.
•class java.io.File @ 0xd89b08c8 - 82,152 (1.14%) bytes.

5.实战:内存溢出的定位与分析

内存溢出在实际的生产环境中经常会遇到,比如,不断的将数据写入到一个集合中,出现了死循环,读取超大的文件等等,都可能会造成内存溢出。如果出现了内存溢出,首先我们需要定位到发生内存溢出的环节,并且进行分析,是正常还是非正常情况,如果是正常的需求,就应该考虑加大内存的设置,如果是非正常需求,那么就要对代码进行修改,修复这个bug。首先,我们得先学会如何定位问题,然后再进行分析。如何定位问题呢,我们需要借助于jmap与MAT工具进行定位分析。

接下来,我们模拟内存溢出的场景。

5.1 内存溢出场景

编写代码,向List集合中添加100万个字符串,每个字符串由1000个UUID组成。如果程序能够正常执行,最后打印ok。

import java.util.*;
public class TestJvmOutOfMemory {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        for (int i = 0; i < 10000000; i++) {
            String str = "";
            for (int j = 0; j < 1000; j++) {
                str += UUID.randomUUID().toString();
            }
            list.add(str);
        }
        System.out.println("ok");
    }
}

为了演示效果,我们将设置执行的参数,这里使用的是Idea编辑器。添加add VM options参数

#参数如下: -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

运行一段时间,结果如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid10864.hprof …
Heap dump file created [8410644 bytes in 0.036 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.woniu.TestJvmOutOfMemory.main(TestJvmOutOfMemory.java:13)

可以看到,当发生内存溢出时,会自动dump文件到项目的根目录,自动生成一个文件java_pid10864.hprof。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9FFNoDkU-1687316984614)(assets/image-20220321201358463.png)]

5.2 导入到MAT工具中进行分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-82Iq6b4D-1687316984615)(assets/image-20220321201605434.png)]

可以看到,有83.77%的内存由Object[]数组占有,所以比较可疑。

分析:这个可疑是正确的,因为已经有超过80%的内存都被它占有,这是非常有可能出现内存溢出的

查看详情:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UfbHluOi-1687316984616)(assets/image-20220321201958188.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-US3vblek-1687316984616)(assets/image-20220321202034427.png)]

可以看到集合中存储了大量的uuid字符串

6.jstack的使用

有些时候我们需要查看下jvm中的线程执行情况,比如,发现服务器的CPU的负载突然增高了、出现了死锁、死循环等,我们该如何分析呢?由于程序是正常运行的,没有任何的输出,从日志方面也看不出什么问题,所以就需要看下jvm的内部线程的执行情况,然后再进行分析查找出原因。这个时候,就需要借助于jstack命令了,jstack的作用是将正在运行的jvm的线程情况进行快照,并且打印出来:

#用法:jstack <pid>
[root@localhost ~]# jstack 8392

6.1线程的状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QBjVDdHX-1687316984617)(assets/image-20220321202428535.png)]

在Java中线程的状态一共被分成6种:

  • 初始态(NEW):创建一个Thread对象,但还未调用start()启动线程时,线程处于初始态。

  • 运行态(RUNNABLE):在Java中,运行态包括 就绪态 和 运行态。

    • 就绪态:该状态下的线程已经获得执行所需的所有资源,只要CPU分配执行权就能运行,所有就绪态的线程存放在就绪队列中
    • 运行态:获得CPU执行权,正在执行的线程,由于一个CPU同一时刻只能执行一条线程,因此每个CPU每个时刻只有一条运行态的线程
  • 阻塞态(BLOCKED):

    • 当一条正在执行的线程请求某一资源失败时,就会进入阻塞态。
    • 而在Java中,阻塞态专指请求锁失败时进入的状态。
    • 由一个阻塞队列存放所有阻塞态的线程。
    • 处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。
  • 等待态(WAITING)

    • 当前线程中调用wait、join、park函数时,当前线程就会进入等待态。
    • 也有一个等待队列存放所有等待态的线程。
    • 线程处于等待态表示它需要等待其他线程的指示才能继续运行。
    • 进入等待态的线程会释放CPU执行权,并释放资源(如:锁)
  • 超时等待态(TIMED_WAITING)

    • 当运行中的线程调用sleep(time)、wait、join、parkNanos、parkUntil时,就会进入该状态;
    • 它和等待态一样,并不是因为请求不到资源,而是主动进入,并且进入后需要其他线程唤醒;
    • 进入该状态后释放CPU执行权 和 占有的资源。
    • 与等待态的区别:到了超时时间后自动进入阻塞队列,开始竞争锁。
  • 终止态(TERMINATED)

    • 线程执行结束后的状态

6.2 死锁的问题

如果在生产环境发生了死锁,我们将看到的是部署的程序没有任何反应了,这个时候我们可以借助jstack进行分析,下面我们实战下查找死锁的原因

1、代码

public class TestDeadLock {
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    public static void main(String[] args) {
        new Thread(new Thread1()).start();
        new Thread(new Thread2()).start();
    }
    private static class Thread1 implements Runnable {
        @Override
        public void run() {
            synchronized (obj1) {
                System.out.println("Thread1 拿到了 obj1 的锁!");
                try {
                    // 停顿2秒的意义在于,让Thread2线程拿到obj2的锁
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj2) {
                    System.out.println("Thread1 拿到了 obj2 的锁!");
                }
            }
        }
    }
    private static class Thread2 implements Runnable {
        @Override
        public void run() {
            synchronized (obj2) {
                System.out.println("Thread2 拿到了 obj2 的锁!");
                try {
                    // 停顿2秒的意义在于,让Thread1线程拿到obj1的锁
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1) {
                    System.out.println("Thread2 拿到了 obj1 的锁!");
                }
            }
        }
    }
}

2、在linux上运行

[root@localhost ~]# javac TestDeadLock.java
[root@localhost ~]# java TestDeadLock

###结果
Thread1 拿到了 obj1 的锁!
Thread2 拿到了 obj2 的锁!

6.3 使用jstack进行分析

[root@localhost ~]# jstack 9516

从输出的日志信息中大家重点看后面的内容

Found one Java-level deadlock:

“Thread-1”:
waiting to lock monitor 0x00007fe0dc0062c8 (object 0x00000000c4c5b5f8, a java.lang.Object),
which is held by “Thread-0”
“Thread-0”:
waiting to lock monitor 0x00007fe0dc004e28 (object 0x00000000c4c5b608, a java.lang.Object),
which is held by “Thread-1”

Java stack information for the threads listed above:

“Thread-1”:
at TestDeadLock$Thread2.run(TestDeadLock.java:40)

  • waiting to lock <0x00000000c4c5b5f8> (a java.lang.Object)
  • locked <0x00000000c4c5b608> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:748)
    “Thread-0”:
    at TestDeadLock$Thread1.run(TestDeadLock.java:22)
  • waiting to lock <0x00000000c4c5b608> (a java.lang.Object)
  • locked <0x00000000c4c5b5f8> (a java.lang.Object)
    at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

可以清晰的看到:

Thread1获取了 <0x00000000c4c5b5f8> 的锁,等待获取 <0x00000000c4c5b608> 这个锁;Thread0获取了 <0x00000000c4c5b608> 的锁,等待获取 <0x00000000c4c5b5f8> 这个锁。由此可见,发生了死锁

7.VirtualVM工具使用

VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。

  • 内存信息
  • 线程信息
  • Dump堆(本地进程)
  • Dump线程(本地进程)
  • 打开堆Dump。堆Dump可以用jmap来生成。
  • 打开线程Dump
  • 生成应用快照(包含内存信息、线程信息等等)
  • 性能分析
  • CPU分析(各个方法调用时间,检查哪些方法耗时多)
  • 内存分析(各类对象占用的内存,检查哪些类占用内存多)
  • ……

7.1 启动VirtualVM

在jdk的安装目录的bin目录下,找到jvisualvm.exe,双击打开即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8jvNzRJv-1687316984618)(assets/image-20220322114730513.png)]

7.2 查看本地jvm进程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xzuBjdt3-1687316984619)(assets/image-20220322115116829.png)]

查看用idea工具编写的死锁案例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aL6erpWQ-1687316984619)(assets/image-20220322115313017.png)]

7.3 查看CPU、内存、类、线程运行信息

1、查看cpu、内存、类情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ik8YgeF6-1687316984620)(assets/image-20220322115543948.png)]

2、查看线程运行情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V71P33CH-1687316984621)(assets/image-20220322115708090.png)]

也可以点击右上角Dump按钮,将线程的信息导出,其实就是执行的jstack命令,截取部分图片

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WlD4DspD-1687316984622)(assets/image-20220322120019174.png)]

发现,显示的内容是一样的

7.4 抽样器

抽样器可以对CPU、内存在一段时间内进行抽样,以供分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OwDsqBil-1687316984622)(assets/image-20220322120136012.png)]

7.5 监控远程的JVM

VisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现。

1.jmx的概念

JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用,简单说,jmx就是一个通信协议

想要监控远程的tomcat,就需要在远程的tomcat进行对JMX配置,方法如下

#在tomcat的bin目录下,修改catalina.sh(脚本catalina.sh用于启动和关闭tomcat服务器,是最关键的脚本,另外的脚本startup.sh和shutdown.sh都是使用不同的参数调用了该脚本),添加如下的参数:

JAVA_OPTS="-Djava.rmi.server.hostname=192.172.0.5 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false" 

#这几个参数的意思是: 
-Dcom.sun.management.jmxremote :允许使用JMX远程管理 
-Dcom.sun.management.jmxremote.port=9999 :JMX远程连接端口 
-Dcom.sun.management.jmxremote.authenticate=false :不进行身份认证,任何用户都可以连接 
-Dcom.sun.management.jmxremote.ssl=false :不使用ssl

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hh4VelqW-1687316984623)(assets/image-20220322145019974.png)]

保存退出,启动tomcat。

2.使用VirtualVM工具远程连接

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lARoGUTk-1687316984624)(assets/image-20220322121546378.png)]

在一个主机下可能会有很多的jvm需要监控,所以接下来要在该主机上添加需要监控的jvm:右键单击

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5PDPrhy9-1687316984625)(assets/image-20220322142818880.png)]

连接成功。使用方法和前面就一样了,就可以和监控本地jvm进程一样,监控远程的tomcat进程

3.jmx监控springboot的jar
java -jar -Djava.rmi.server.hostname="192.172.0.5" -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port="9999" -Dcom.sun.management.jmxremote.authenticate="false" -Dcom.sun.management.jmxremote.ssl="false" aaa.jar

其它方式都一样,略

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gTBs5RBF-1687316984625)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20230209133139871.png)]

image-20230209135310364

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mknk16DY-1687316984627)(C:\Users\ThinkPad\AppData\Roaming\Typora\typora-user-images\image-20230209135825718.png)]

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值