Linux
1.常用的命令
1.1文件与目录的基本操作
列出文件或者目录的信息,目录的信息就是其中包含的文件。
ls [-aAdfFhilnrRSt] file|dir
-a : 列出全部的文件
-d : 仅列出目录本身
-l : 以长数据串行列出,包含文件的属性与权限等等数据
更换当前目录。
cd [相对路径或绝对路径]
创建目录。
mkdir [-mp] 目录名称
-m : 配置目录权限
-p : 递归创建目录
删除,必须为空。
rmdir [-p] 目录名称
-p : 递归删除目录
rm [-fir] 文件或目录
-r : 递归删除
更新文件时间或者建立新文件。
touch [-acdmt] filename
-a : 更新 atime
-c : 更新 ctime,若该文件不存在则不建立新文件
-m : 更新 mtime
-d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间"
-t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm]
复制文件。
如果源文件有两个以上,则目的文件一定要是目录才行。
cp [-adfilprsu] source destination
-a : 相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明
-d : 若来源文件为链接文件,则复制链接文件属性而非文件本身
-i : 若目标文件已经存在时,在覆盖前会先询问
-p : 连同文件的属性一起复制过去
-r : 递归持续复制
移动文件。
mv [-fiu] source destination
-f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖
1.2修改权限
可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。
chmod [-R] xyz dirname/filename
#也可以使用符号来设定权限。
chmod [ugoa] [+-=] [rwx] dirname/filename
- u: 拥有者
- g: 所属群组
- o: 其他人
- a: 所有人
- +: 添加权限
- -: 移除权限
- =: 设定权限
1.3链接
ln [-sf] source_filename dist_filename
-s : 默认是 hard link,加 -s 为 symbolic link
-f : 如果目标文件存在时,先删除目标文件
1.4获取文件内容
cat [-AbEnTv] filename
-n : 打印出行号,连同空白行也会有行号,-b 不会
more:和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。
less:和 more 类似,但是多了一个向前翻页的功能。
head [-n number] filename
-n : 后面接数字,代表显示几行的意思
tail是 head 的反向操作,只是取得是后几行。
1.5指令与文件搜索
which [-a] command
-a : 将所有指令列出,而不是只列第一个
whereis [-bmsu] dirname/filename
locate [-ir] keyword
-r: 正则表达式
find [basedir] [option]
example: find . -name "shadow*"
1.6压缩与打包
tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩
tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看
tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩
-z : 使用 zip;
-j : 使用 bzip2;
-J : 使用 xz;
-c : 新建打包文件;
-t : 查看打包文件里面有哪些文件;
-x : 解打包或解压缩的功能;
-v : 在压缩/解压缩的过程中,显示正在处理的文件名;
-f : filename: 要处理的文件;
-C 目录 : 在特定目录解压缩。
1.7管道指令
将一个命令的标准输出作为另一个命令的标准输入。
ls -al /etc | less
1.8正则表达式
grep [-acinv] [--color=auto] 搜寻字符串 filename
-c : 统计个数
-i : 忽略大小写
-n : 输出行号
-v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行
--color=auto : 找到的关键字加颜色显示
1.9查看进程
查看自己的进程:
ps -l
查看系统所有进程:
ps aux
查看特定的进程
ps aux | grep threadx
实时显示进程信息
top
查看特定端口的进程
netstat -anp | grep port
2.内核
Linux内核的任务:
1.从技术层面讲,内核是硬件与软件之间的一个中间层。作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
2.从应用程序的层面讲,应用程序与硬件没有联系,只与内核有联系,内核是应用程序知道的层次中的最底层。在实际工作中内核抽象了相关细节。
3.内核是一个资源管理程序。负责将可用的共享资源(CPU时间、磁盘空间、网络连接等)分配得到各个系统进程。
4.内核就像一个库,提供了一组面向系统的命令。系统调用对于应用程序来说,就像调用普通函数一样。
内核实现策略:
1.微内核。最基本的功能由中央内核(微内核)实现。所有其他的功能都委托给一些独立进程,这些进程通过明确定义的通信接口与中心内核通信。
2.宏内核。内核的所有代码,包括子系统(如内存管理、文件管理、设备驱动程序)都打包到一个文件中。内核中的每一个函数都可以访问到内核中所有其他部分。目前支持模块的动态装卸(裁剪)。Linux内核就是基于这个策略实现的。
哪些地方用到了内核机制?
1.进程(在cpu的虚拟内存中分配地址空间,各个进程的地址空间完全独立;同时执行的进程数最多不超过cpu数目)之间进行通 信,需要使用特定的内核机制。
2.进程间切换(同时执行的进程数最多不超过cpu数目),也需要用到内核机制。
进程切换也需要像FreeRTOS任务切换一样保存状态,并将进程置于闲置状态/恢复状态。
3.进程的调度。确认哪个进程运行多长的时间。
3.基本组件
- 内核(Kernel):
Linux 内核是操作系统的核心部分,负责管理和控制硬件资源,并提供基本的系统功能。它处理进程管理、内存管理、设备驱动程序、文件系统、网络协议栈等重要任务。Linux 内核具有模块化的设计,使得用户可以根据需要添加或删除特定的模块。
- Shell:
Shell 是用户与操作系统交互的命令行解释器。它接受用户输入的命令,并将其传递给操作系统进行执行。Shell 还提供了脚本编程的能力,允许用户编写一系列的命令以自动化任务。常见的 Linux Shell 包括 Bash、Zsh 和 Fish 等,它们提供了丰富的命令和功能。
- GNU 工具:
GNU 工具是一组由 GNU 项目开发的实用工具集合,用于完成各种任务。这些工具包括常见的命令行工具,如文本编辑器(例如 Emacs 和 Vim)、文件操作工具(例如 ls、cp 和 rm)、文本处理工具(例如 grep 和 sed)等。GNU 工具是 Linux 系统的重要组成部分。
- 系统库:
Linux 提供了广泛的系统库,用于应用程序开发。最常用的是 GNU C 库(glibc),它提供了 C 语言标准函数和系统调用的封装。此外,还有其他库,如 libstdc++(C++ 的标准库)、libpthread(线程库)、libm(数学函数库)等,它们为开发者提供了丰富的函数和功能。
- X Window System:
X Window System 是 Linux 中常用的图形窗口系统,它提供了图形界面环境以及与图形硬件和输入设备的交互。X Window System 使用客户端-服务器模型,其中 X 服务器负责图形显示和输入设备控制。用户可以通过 X 客户端连接到 X 服务器,并在其上运行图形化应用程序。
- 桌面环境:
Linux 上有多个桌面环境可供选择,每个桌面环境都具有自己的外观、特性和工具集。
例如:GNOME 和 KDE 是两个最受欢迎的桌面环境,它们提供了完整的图形用户界面和一系列应用程序,包括文件管理器、文本编辑器、终端模拟器等。
- 文件系统:
Linux 支持多种文件系统,用于组织和管理存储设备上的文件和目录。常见的文件系统包括 EXT4、XFS 等。文件系统负责维护文件的元数据以及文件数据的物理存储位置。它还提供了对文件的访问和操作的接口。
- 网络协议栈:
Linux 内核支持各种网络协议,如 TCP/IP、UDP、HTTP、FTP 等。网络协议栈是在内核中实现的协议和算法的集合,它使得 Linux 能够进行网络通信。Linux 提供了丰富的网络工具和命令,如 ifconfig、ping、netstat 等,用于配置网络接口、测试连接和监控网络状态。
4.进程间通信方式
- 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 共享内存( shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
这里主要比较一下高级通信的这三种方式的特点。
管道通信(PIPE): 两个进程利用管道进行通信时.发送信息的进程称为写进程.接收信息的进程称为读进程。管道通信方式的中间介质就是文件.通常称这种文件为管道文件.它就像管道一样将一个写进程和一个读进程连接在一起,实现两个进程之间的通信。写进程通过写入端(发送端)往管道文件中写入信息;读进程通过读出端(接收端)从管道文件中读取信息。两个进程协调不断地进行写和读,便会构成双方通过管道传递信息的流水线。 利用系统调用PIPE()可以创建一个无名管道文件,通常称为无名管道或PIPE;利用系统调用MKNOD()可以创建一个有名管道文件.通常称为有名管道或FIFO。无名管道是一种非永久性的管道通信机构.当它访问的进程全部终止时,它也将随之被撤消。无名管道只能用在具有家族联系的进程之间。有名管道可以长期存在于系统之中.而且提供给任意关系的进程使用,但是使用不当容易导致出错.所以操作系统将命名管道的管理权交由系统来加以控制管道文件被创建后,可以通过系统调用WRITE()和READ()来实现对管道的读写操作;通信完后,可用CLOSE()将管道文件关闭。
消息缓冲通信(MESSAGE) 多个独立的进程之间可以通过消息缓冲机制来相互通信.这种通信的实现是以消息缓冲区为中间介质.通信双方的发送和接收操作均以消息为单位。在存储器中,消息缓冲区被组织成队列,通常称之为消息队列。消息队列一旦创建后即可由多进程共享.发送消息的进程可以在任意时刻发送任意个消息到指定的消息队列上,并检查是否有接收进程在等待它所发送的消息。若有则唤醒它:而接收消息的进程可以在需要消息的时候到指定的消息队列上获取消息.如果消息还没有到来.则转入睡眠状态等待。 共享内存通信(SHARED MEMORY)
共享内存
这种通信方式允许多个进程在外部通信协议或同步,互斥机制的支持下使用同一个内存段(作为中间介质)进行通信.它是一种有效的数据通信方式,其特点是没有中间环节.直接将共享的内存页面通过附接.映射到相互通信的进程各自的虚拟地址空间中.从而使多个进程可以直接访问同一个物理内存页面.如同访问自己的私有空间一样(但实质上不是私有的而是共享的)。因此这种进程间通信方式是在同一个计算机系统中的诸进程间实现通信的快捷的方法.而它的局限性也在于此.即共享内存的诸进程必须共处同一个计算机系统.有物理内存可以共享才行。
三种方式的特点(优缺点): 无名管道简单方便.但局限于单向通信的工作方式.并且只能在创建它的进程及其子孙进程之间实现管道的共享:有名管道虽然可以提供给任意关系的进程使用.但是由于其长期存在于系统之中,使用不当容易出错。
消息缓冲可以不再局限于父子进程.而允许任意进程通过共享消息队列来实现进程间通信.并由系统调用函数来实现消息发送和接收之间的同步.从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题.使用方便,但是信息的复制需要额外消耗CPU的时间.不适宜于信息量大或操作频繁的场合。
共享内存针对消息缓冲的缺点改而利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。但是共享内存的通信方式是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的.因此,这些进程之间的读写操作的同步问题操作系统无法实现。必须由各进程利用其他同步工具解决。另外,由于内存实体存在于计算机系统中.所以只能由处于同一个计算机系统中的诸进程共享。不方便网络通信。
5.目录结构
Linux 使用一种称为目录树的层次结构来组织文件和目录。目录树由根目录(/)作为起始点,向下延伸,形成一系列的目录和子目录。每个目录可以包含文件和其他子目录。结构层次鲜明,就像一棵倒立的树。
inode 具体包含以下信息:
- 权限 (read/write/excute);
- 拥有者与群组 (owner/group);
- 容量;
- 建立或状态改变的时间 (ctime);
- 最近一次的读取时间 (atime);
- 最近修改的时间 (mtime);
- 定义文件特性的旗标 (flag),如 SetUID...;
- 该文件真正内容的指向 (pointer)。
inode 具有以下特点:
- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes);
- 每个文件都仅会占用一个 inode。
inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块记录引用信息。
建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。
可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。
最基础的三个目录如下:
- / (root, 根目录)
- /usr (unix software resource): 所有系统默认软件都会安装到这个目录;
- /var (variable): 存放系统或程序运行过程中的数据文件。
文件属性:
用户分为三种: 文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。
使用 ls 查看一个文件时,会显示一个文件的信息,例如 drwxr-xr-x. 3 root root 17 May 6 00:14 .config
,对这个信息的解释如下:
- drwxr-xr-x: 文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段
- 3: 链接数
- root: 文件拥有者
- root: 所属群组
- 17: 文件大小
- May 6 00:14: 文件最后被修改的时间
- .config: 文件名
常见的文件类型及其含义有:
- d: 目录
- -: 文件
- l: 链接文件
9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。
6.排查CPU占用过高
如果我们项目在linux上部署成功并正常运行,但是发现linux的cpu飙升。这时就需要对cpu飙升问题进行排查。以下是排查方法。
- Java 进程cpu飙升问题
- Mysql 进程cup飙升
6.1Java 进程cpu飙升问题
6.1.1使用 top 命令
示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。 top命令可以动态地持续监听进程运行情况,默认情况下是按照cpu的使用率倒叙排序。所以我们直接看排在最前面的java进程就可以。
top
通过结果可以看出第一行的java进程cpu使用率最高,占比75.5%.这样我们就可以得到cpu占比高的进程ID。PID=13369. 我们也可以通过grep筛选java进程。
6.1.2使用ps命令查看cpu占比高的PID
我们通过ps命令查看该进程对应的哪个线程占用cpu资源比较高。
ps -mp 13369 -o THREAD,tid,time
6.1.3将对应的TID转换为16进制
我们分析java线程会使用到jdk自带的jstack工具,该工具需要16进制的线程ID进行筛选,所以我们需要把需要分析的线程ID转换为16进制。
printf "%x\n" 13370
343a
6.1.4使用jdk自带的命令jstack,拉出指定线程的堆栈信息
jstack 13369 |grep 343a -A150
"main" #1 prio=5 os_prio=0 tid=0x00007f5dc0009800 nid=0x343a runnable [0x00007f5dc8457000]
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 <0x00000006c7c27f50> (a java.io.BufferedOutputStream)
at java.io.PrintStream.write(PrintStream.java:482)
- locked <0x00000006c7c0c380> (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 <0x00000006c7c0b060> (a java.io.OutputStreamWriter)
at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
at java.io.PrintStream.newLine(PrintStream.java:546)
- locked <0x00000006c7c0c380> (a java.io.PrintStream)
at java.io.PrintStream.println(PrintStream.java:737)
- locked <0x00000006c7c0c380> (a java.io.PrintStream)
at WileDemo.main(WileDemo.java:8)
"VM Thread" os_prio=0 tid=0x00007f5dc0081000 nid=0x3443 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f5dc001e800 nid=0x343b runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f5dc0020800 nid=0x343c runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f5dc0022800 nid=0x343d runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f5dc0024000 nid=0x343e runnable
"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x00007f5dc0026000 nid=0x343f runnable
"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x00007f5dc0028000 nid=0x3440 runnable
"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x00007f5dc0029800 nid=0x3441 runnable
"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x00007f5dc002b800 nid=0x3442 runnable
"VM Periodic Task Thread" os_prio=0 tid=0x00007f5dc00d1000 nid=0x344c waiting on condition
JNI global references: 202
该命令中 13369 代表着进程ID,343a代表着线程ID的16进制,-A150代表着列出最多150行堆栈信息。
6.1.5对显示的堆栈信息进行分析
这一步是最难的一步,也是对研发人员要求最高的一部,需要研发人员有大量的经验。
每一个项目出现cpu占比过高的原因都不尽相同,在这里总结了常见的cpu飙升的原因
- while死循环,或者循环次数超大的循环。
- 超大的运算,如超大的浮点运算。
- 频繁的Young GC。如大量的json数据转实体对象。
- 多线程连接池,线程频繁地切换。我们使用top命令时可以看到CPU(s)一行,如果us用户空间占比过高,则说明是程序线程计算高引起的,通过堆栈分析可以找到原因;如果sy内核空间占比过高,那么一般是由于线程上下文切换引起的。
6.2Mysql 执行过程
6.2.1使用 top 命令
如 Java 执行过程一样,先使用 top 命令查看占比高的进程是不是 mysqld,如果是 mysqld 则表示 mysql 问题。
6.2.2使用 SHOW PROCESSLIST 语句
当 Mysql 的 CPU 使用率 过高的时候,不要盲目开启慢日志,开启慢日志可能会让 Mysql 推向崩溃。 一般是使用 SHOW PROCESSLIST 语句,定位大量存在的相同的 SQL 语句。对这些 SQL 语句进行针对性的分析。可能引起问题的原因通常是索引、锁、查询大量字段、大表等。
SHOW PROCESSLIST;
7.线上项目卡顿有延迟如何找到哪地方的异常?
7.1CPU 飙高
造成 CPU 飙高的原因主要有两种: (1)内存溢出,然后引起系统频繁的进行 Full GC,而每次 Full GC 的停顿时间都较长,所以系统出现卡顿; (2)代码中有比较耗 CPU 的操作,导致 CPU 过高,所以系统变慢。
不管是哪个原因,都可以通过下文中介绍的两种排查思路去解决。两者的区别在于,如果是因为内存溢出导致的频繁 Full GC,那么通过 jstack(下文中有介绍)得到的线程栈信息中会包含大量的 VM Thread(代表垃圾回收线程);而如果是因为代码中有比较耗 CPU 的计算,那么得到的就是一个业务线程的具体堆栈信息,包含用户自定义的类,从中可以直接找到问题代码的行数。
解决 CPU 飙高这个问题大致有两个思路:
7.1.1 第一个思路
(1)先找到占用 CPU 高的进程; (2)再找到占用 CPU 高的线程; (3)最后找到占用 CPU 高的线程对应的业务代码。
(1)先找到占用 CPU 高的进程
执行「top」或「top -c」命令,显示进程运行信息列表,再键入大写 P,列表会按 CPU 使用率降序排列:
top
(2)再找到占用 CPU 高的线程
一个进程内有多个线程,执行「top -Hp 6」,显示进程 PID 为 6 的线程列表,键入大写的 P 后,列表会按 CPU 使用率降序排列:
top -Hp 6
(3)最后找到占用 CPU 高的线程对应的业务代码
这里要进行一个转换,将线程的 PID(14)转为十六进制,因为在 jstack 打印出的线程栈信息中,线程的 PID 是用十六进制显示的。转换方法:
printf "%x\n" 14
获取到 14 的十六进制表示为 e。最后通过 jstack 检索进程(进程 PID = 6)中最耗 CPU 资源的线程(线程 PID = e)的线程栈信息,执行 jstack 命令:
jstack 6 | grep "0xe" -C5 --color
通过在线程栈信息中高亮显示的 0xe,我们可以快速找到对应的线程名称。比如在上图中,如果线程的十六进制 PID 是 531,那我们就可以找到线程 nid=0x531 对应的线程名称为 grpc-default-executor-200,这个线程名称是我们在业务代码中给线程取的名称。**因此,给线程取一个与业务处理相关的名称,对快速定位问题尤为重要。**如果只通过 jdk 生成的线程名称,以及只包含 jdk 代码的线程栈信息,是无法定位到业务代码的。
如果发现信息中包含文本「Full GC (System.gc())」,说明代码或第三方依赖包中有显式的 System.gc() 调用。
7.1.2 第二个思路
(1)生成堆转储文件(也就是生成 heap dump,它是一个二进制文件,保存了某一时刻 JVM 堆内存中的对象使用情况;与之相似的还有 thread dump,用于记录 CPU 信息); (2)分析堆转储文件。
(1)生成堆转储文件
生成堆内存转储文件主要用到命令 jmap, jmap 命令是 JDK 提供的用于查看或生成堆内存信息的工具。生成命令:
# 生成命令: jmap -dump:live,format=b,file=heap.hprof <pid>
jmap -dump:live,format=b,file=heap.bin 6
**live:**仅打印具有活动引用的对象,丢弃那些准备垃圾回收的对象,可选参数; **format=b:**以二进制格式转储; **file:**转储文件路径,文件后缀通常为 hprof 或 bin; **pid:**进程 id。
使用 jmap 命令有一点不好的地方是,内存溢出是某个时间点发生的事情,跟执行 jmap 命令的时候获取到的转储文件相比,存在时间差问题。所以这里再介绍一个 Java 虚拟机参数:
-XX:+HeapDumpOnOutOfMemoryError
设置了这个 Java 虚拟机参数之后,它就会在程序发生内存溢出的时候,自动生成一个二进制的堆转储文件,这个转储文件的后缀为 hprof,而且这个文件几乎是在发生内存溢出的同时生成的,故而会更准确些。这个文件的存储路径也可以通过参数指定:(如果不指定,则默认在当前工作目录下生成 java_pid.hprof 文件)
# 指定相对路径、绝对路径都可以,但要注意,就是必须确保文件路径事先已创建好,如果路径不存在,它不会自动创建
# -XX:HeapDumpPath=./
# -XX:HeapDumpPath=/data/tmp/
-XX:HeapDumpPath=/data/tmp/heapdump.hprof
建议提前添加 -XX:+HeapDumpOnOutOfMemoryError 参数,而不是用 jmap。有几点要注意,内存分析可能需要比对多个堆转储文件,除了在发生内存溢出时自动生成的堆转储文件外,我们在获取其他时间点的堆转储文件时一定要找准时机,避免得到一个没用的快照。另外,每次执行堆转储,都会对 JVM 进行「冻结」,所以在生产环境中不能执行太多次的 dump 操作,一旦系统缓慢或者卡死,麻烦就大了。(快照中也可能含有机密信息,例如密码等,这时候如果企业有安全限制,还需先获得在生产服务器上执行堆转储的权限)
(2)分析堆转储文件
分析堆内存转储文件主要用到命令 jhat,jhat 命令也是 JDK 自带的用于分析 heap dump 文件的工具。使用下面的命令可以将堆转储文件的分析结果以 HTML 网页的形式进行展示:(如果系统有可视化界面,也推荐用工具 MAT(Memory Analyzer Tools) 来分析,该工具既可以安装到 Eclipse 上使用,也可以独立运行使用,解压之后双击 MemoryAnalyzer.exe 即可)
# 分析命令:jhat <heap-dump-file>
jhat heap.bin -J-Xmx8192m
参数 -J-Xmx8192m 用来设置命令使用的内存大小,当然首先要保证执行命令的物理机器有足够大的内存,因为堆转储文件往往很大,不加这个参数的话有可能会报内存溢出异常,导致分析失败。如果加了此参数仍然不行,可以尝试设置下环境变量:
JAVA_OPTS:-server -Xms4096m -Xmx4096m
执行成功后显示如下结果:
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.
这个时候访问 http://localhost:7000/ 就可以看到结果了:
那如何从这个 HTML 网页中找到我们想要的有价值的信息呢?需要拉到网页最底部,找到 Other Queries,然后选择 Show instance counts for all classes (excluding platform),找除了 JDK 本身之外实例数最多的类。
看看排名最前的几个类,就可以找出可能会分配大量对象的代码,基本就能定位到问题代码的位置了,如果对整个系统非常熟悉,找到的速度会更快。
7.2内存溢出
7.2.1 排查思路
当 Java 应用消耗大量内存,导致出现异常:
java.lang.OutOfMemoryError: GC overhead limit exceeded
一般出于以下原因: (1)JVM 内存过小; (2)产生的垃圾过多,无法回收; (3)代码有漏洞:
- 对象、线程创建太多,且一直未释放;
- 进行 IO 操作后不关闭流,资源无法释放;
- 消费者速度慢,生产者速度快,任务队列中对象不断堆积。
大多数情况下,通过配置 JVM 启动参数或增大堆内存并不能从根本上解决这个问题,只能延长错误发生的时间。要解决内存溢出问题需清楚两点:哪些对象占用了最多内存?这些对象是在哪部分代码中分配的?终极的解决办法是找到占用内存大的地方,优化相关代码。怎么找呢?依然是通过堆转储文件去找。
7.2.2 命令介绍
除直接分析堆转储文件外,还有一些值得学习的命令可以辅助我们排查问题,有时甚至只用这些命令就能定位到问题代码的位置。
(1)jmap -histo
如果想知道什么对象消耗内存最大,那么可以执行命令:
# 查看类的实例数量、占用的内存、类的名称:jmap -histo:live <pid>
jmap -histo:live 6 | head -n 100
结果会以表格的方式显示存活对象的信息,并按对象所占的 bytes 大小进行降序排列(比如 18874368 bytes = 18M,若要包含未存活的对象,则删掉「:live」,更多用法,可通过「man jmap」寻求帮助)。如果发现某类对象占用内存很大,比如达到了几个 G,那大概率有问题。
(2)pstree
如果想知道创建了多少线程,可以执行命令:
# pstree -p <pid> | wc -l
pstree -p 6 | wc -l
结果会显示进程内的线程数,还有每个线程的线程栈内存。
(3)jmap -heap
如果想查看堆(新生代、老年代)内存分配大小及使用情况,可以执行命令:
# 查看堆的使用情况:jmap -heap <pid>
jmap -heap 6
可用来确认是不是整体内存分配太小。
(4)jstat -gc
如果想查看实时的新生代、老年代内存使用情况及 GC 情况,可以执行命令:
# 提供 GC 和类装载活动的信息: jstat -gc <pid>
jstat -gc 6 1000
其中,6 为进程ID,1000 为数据刷新间隔的毫秒数,更多的命令参数含义可通过「man jstat」寻求帮助。执行结果会显示各个区内存使用情况及 GC 的情况,结果列表表头含义:
- EC:Eden 区容量;
- EU:Eden 区已使用量;
- OC:Old 区容量;
- OU:Old 区已使用量;
- YGC:YongGC 次数(当新生代被填满时执行一次,回收速度快);
- YGCT:YongGC 耗时;
- FGC:FullGC 次数(当老年代被填满时执行一次,回收时应用程序会整个停下来直到回收完成);
- FGCT:FullGC 耗时。
7.3产品功能运行缓慢
除了上文中一旦出现就很严重的两种情况外,还有三种情况,也会导致系统功能运行缓慢,但它的影响范围没那么大,出现问题后不会导致整个系统都不可用,最多只会让一个具体的功能变得很慢。跟上文中的情况不同的是,如果出现这三种问题,通过查看 CPU 和系统内存情况是无法排查出问题原因的,因为它们只是局部的阻塞,CPU 和系统内存使用率都不高,所以需要用一些其他的手段来排查。
7.3.1 代码中有阻塞性的操作
代码阻塞使用不当,会导致调用相关功能时比较耗时。比较典型的例子是,访问某个接口经常需要 2~3s 才能返回,而且是不定时出现。
定位这种问题的思路如下:首先找到该接口,通过压测工具不断加大访问力度,如果该接口中有某个位置比较耗时,由于访问的频率非常高,那么大多数的线程最终都将阻塞于该阻塞点,这样通过多个线程的堆栈日志,就可以定位到接口中耗时的代码位置。
7.3.2 某个线程进入 WAITING 状态
如果某个线程进入 WAITING 状态,可能接着导致主线程也进入 WAITING 状态,这是比较少见的一种情况,且具有一定的不可复现性,在排查的时候是非常难以发现的。在 jstack 日志中,即使是在正常的情况下,也会有很多线程处于 TIMED_WAITING 状态,这与出现 WAITING 问题的线程状态一模一样,非常容易混淆我们的判断。
定位这种问题的思路如下:通过 grep 在 jstack 日志中找出所有处于 TIMED_WAITING 状态的线程,将其导出到某个文件中,如 log1.log,等待几分钟之后,再次对 jstack 日志进行 grep,将其导出到另一个文件,如 log2.log。重复上述操作,待导出 4、5 个文件之后,再对导出的文件进行对比,找出在这几个文件中一直都存在着的用户线程,基本上就能确定是哪里出问题了, 因为正常的请求线程是不会在 30s 之后还是处于等待状态的。
如果找到了多个,那么要排除掉框架自带的线程,找包含了用户自定义的线程名或者类的线程,再查看其堆栈信息即可。
7.3.3 死锁
如果锁使用不当,就有可能造成死锁。定位死锁的思路很简单,直接用 jstack 命令即可:
# 检查死锁:jstack <pid>
jstack 6
# 拉到末尾:Found 1 deadlock.
它自带检查死锁的功能,会在日志底部打印出代码中存在哪些死锁,以及每个死锁的线程堆栈信息,我们可以根据这些很容易定位到发生死锁的代码位置。