手把手教你使用JConsole

目前市面上有多种 JVM 监控工具供我们选择,其中 JConsole 可以算是 JVM 监控始祖了,从 JDK1.5 就开始引入。最关键的是 JConsole 是 JDK 官方自带的控制台,在一些特殊环境,比如网络不通的情况下,可能是你唯一可以使用的图形化监控工具了。因此我们有必要对 JConsole 的使用方法做一个了解。并且对大多数监控需求来说,JConsole 完全可以满足我们的需要,今天我们就来看看一直被我们低估的 JConsole 到底如何使用。

首先我们先找到 JDK/bin 目录下的 JConsole 命令,启动 JConsole。下图是我本机的 JConsole 启动界面。

我们可以看到,JConsole 启动时在本地进程中会列出所有 JVM 进程 PID,相当于可视化的 jps 命令。我本机现在运行的几个虚拟机进程分别是 StudyDemoApplication、JConsole、jps 和 idea。现在我们双击 StudyDemoApplication 进去看看。

进入具体的进程后,我们会看到几个 tab 选项,其中概览是我们需要重点关注的,概览的内容比较直观,包括了我们最关心的堆内存使用量、线程、类、CPU 使用情况这四个信息的曲线图。除了概览,被高频使用的还有内存和线程 2 个 tab,下面我们分别介绍它们。

内存

我们先来看内存 tab 吧。 **内存这个 tab 用于监视虚拟机堆内存、非堆内存、内存池等的变化趋势。**可以通过图表下拉框去选择要监视的信息,还可以选择时间范围。

之前我在《 如何 使用 JVM 工具排查线上问题》课中介绍过 jstat 的使用方法,JConsole 内部集成了 jstat,图形化的展示让内存信息更直观。注意,这里的下拉列表和你使用的垃圾收集器有关,比如默认使用的是 Parallel Scavenge 垃圾收集器,缩写就是 PS,因此会看到 PS 前缀。

我们通过一个具体的例子来感受一下。下面这段代码每隔 30 毫秒都会往 list 追加大小为 64KB 的 OOMObject 对象,此时我的堆设置是 100MB。

arduino 复制代码
public class TestMemoryMonitor {
    /**
     * 存占位符对象,一个 OOMObject 大约占 64KB
     */
    staticclass OOMObject{
        publicbyte[] placeholder = newbyte[64 * 1024];
    }
    public static void fillHeap(int num) throws InterruptedException {
        List<OOMObject> list = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            //稍作延时,令监视曲线的变化更加明显
            Thread.sleep(30);
            list.add(new OOMObject());
        }
        System.gc();
    }
    public static void main(String[] args) throws Exception {
        fillHeap(3000);
    }
 }

程序运行后,连接一下运行的 PID。在图表列表中,选择 内存池"PS Eden Space",我们一起来看下内存的使用情况。

我们看到 for 循环 1000 次执行完成后,形成了一个折线状的图。你可以看一下,右下角柱状图中的【堆】区域有 3 个柱状图,从左到右依次是"PS Old Gen"、"PS Eden Space"和"PS Survivor Space"。

我们可以看到在开始往堆填充数据时,出现了一条匀速向上的、趋近直线的折线。每隔 30 毫秒停止填充,曲线急剧下降,周而复始直至完成。我们再来看看图表右下角的柱状图,当 for 循环执行结束,我们执行 System.gc() 后,Eden 和 Survivor 区中的空间几乎都被释放了,但是老年代的已使用空间还在缓慢增长。

显然,我们的对象并没有真正被释放,而是在 System.gc() 发生之后进入了老年代。在我们的代码中 List 在整个 fillHeap 方法中都是有效的,当我们执行 System.gc() 时并没有离开 List 的作用域。

如果这个时候想要回收全部内存该怎么办呢?你可以尝试一下,把 System.gc() 放到 fillHeap 方法外运行一下试试,显然由于此时 fillHeap 方法已经退出,List 不再有效,随时可以被回收。

在实验的时候要注意 System.gc() 并不一定总是有效的。这是因为 System.gc() 并不是强制执行的,而只是通知 JVM 快去回收对象,具体什么时候执行 JVM 说了算。这种感觉就像你在餐厅点菜,你觉得上菜比较慢,你就会催促服务员,但是催促服务员并不意味着立刻就会上菜,具体菜什么时候做好还是厨师说了算。

线程

接下来,我们聊聊另一个重要的 tab------线程。我在《 如何 使用 JVM 工具排查线上问题》课程中介绍过 jstack 命令,线程 tab 的功能基本和 jstack 命令一致。我们知道线程出现阻塞的原因包括: 等待外部资源、长时间执行的循环、锁。

下面我们通过一段代码来观测常见的线程阻塞例子。

scss 复制代码
public static void createBusyThread(){
    Thread test1= new Thread(()->{while(true);});
    test1. start();
}
public static void createLockThread (final Object lock) {
    Thread test2= new Thread (()->{
    synchronized(lock){
                try{
                    lock. wait();
                }catch(InterruptedException e){
                }
            }});
    test2. start();
}
public static void main (String[] args) throws Exception {
    BufferedReader br= new BufferedReader( new InputStreamReader(System. in));
    br.readLine();
    createBusyThread();
    br.readLine();
    Object obj= new Object();
    createLockThread(obj);
}

监控线程的Runnable 状态

我们在线程 tab 中可以看到 main 线程,你可以看一下图片。右侧堆栈显示 readBytes 在参数输入,此时线程还是 Runnable 状态,Runnable 意味着线程会被分配 CPU 时间片,readBytes 发现没有任何输入又会归还 CPU 时间片,这种性能消耗几乎可以忽略不计。

接着监控 test1 线程,test1 线程执行的是自旋操作,从右侧的堆栈中我们可以发现线程此时在 MonitoringTest.java 代码的 41 行,而第 41 行就是死循环。显然这种自旋会极大浪费 CPU 资源。

监控线程的 WAITING 状态

下面我们再来看看 test2 线程,图片显示 test2 线程在等待某个锁,线程这时候处于 WAITING 状态。test2 线程处于一个正常的状态,它在等待一个锁,直到锁对象被 notify 调用才会继续执行。

监控线程的 BLOCKED 状态(死锁)

下面我们看一个死锁的例子。出现线程死锁之后,我们可以看到一个新的"死锁"tab。

图中我们可以很清晰地看到 43 号线程在等待一个 Integer 对象,这个对象被 12 号线程持有。而点击 12 号线程则显示它也在等待一个 Integer 对象,这个对象被 43 号线程持有,43 号线程和 12 号线程形成了死锁,是不是很直观?

总结

以上就是我们讲的 JConsole 的常用用法,可以看到,我们日常的监控、死锁判断、内存排查都可以使用 JConsole 去排查。在没有图形化的生产环境可以使用 JConsole 远程连接。对 JConsole 的使用大多数是线上问题诊断。既然要诊断,最重要的还是基于我们的研发经验,JConsole 只是工具而已,不能本末倒置,认为有一个工具就可以解决问题了,更需要的是我们人为的判断。

此外,我们通过线程 tab 看到了线程的 Runnable、WAITING 以及 BLOCKED 3 种状态。要特别注意区分 WAITING 和 BLOCKED 状态,简单来说 WAITING 和我们的 wait 方法有关,而 BLOCKED 则和锁有关。

相关推荐
草捏子3 分钟前
状态机设计:比if-else优雅100倍的设计
后端
stein_java1 小时前
springMVC-10验证及国际化
java·spring
weixin_478689761 小时前
C++ 对 C 的兼容性
java·c语言·c++
LUCIAZZZ1 小时前
HikariCP数据库连接池原理解析
java·jvm·数据库·spring·springboot·线程池·连接池
考虑考虑2 小时前
Springboot3.5.x结构化日志新属性
spring boot·后端·spring
涡能增压发动积2 小时前
一起来学 Langgraph [第三节]
后端
sky_ph2 小时前
JAVA-GC浅析(二)G1(Garbage First)回收器
java·后端
涡能增压发动积2 小时前
一起来学 Langgraph [第二节]
后端
IDRSolutions_CN2 小时前
PDF 转 HTML5 —— HTML5 填充图形不支持 Even-Odd 奇偶规则?(第二部分)
java·经验分享·pdf·软件工程·团队开发
hello早上好2 小时前
Spring不同类型的ApplicationContext的创建方式
java·后端·架构