那么问题来了, 一个 Java 项目在咱们平时启动项目的时候, 究竟发生了什么, 创建几个简单的项目, 用 VisualVM 来分析一下~
Main
简单的项目, 应该没有比 HelloWorld 更简单的了吧, 按照老规矩, 咱们就从 HelloWorld 开始分析! 那么简单的项目大家都能闭着眼睛敲出来, 是不是没分析的必要啊, 别着急, 写好 HelloWorld 咱们开始分析:
- System.out.println("HelloWorld start");
- // 这里让线程睡一会, 方便分析
- Thread.sleep(100000);
- System.out.println("HelloWorld end");
运行 main 方法, 打开 VisualVM, 发现事情并不简单哦, 这个简单的项目有十六个线程维护, 其中守护线程有十五个.
其中几大线程的内存分配情况如下:
这些线程都是干什么用的? 写了那么多年 HelloWorld 没想到还有这种知识盲区:
RMI TCP Connection(2)-10.128.227.33
10.128.227.33 是我本地的 ip 地址. 正确而愚蠢的原因是因为开了 VisualVM(JMX 客户端),JVM 需要把他的数据传递给这个客户端, 就是使用的 TCP 传递, 相同作用的线程还有 JMX server connection timeout:MAIN 方法跑完了, JMX 连接的心跳断开. RMI TCP Connection(idle): 用来在 RMI 连接池中创建线程.*** Profiler Agent Communication Thread:Profiler 代理通信线程. RMI TCP Accept-0: 进行 JMX 进行 JMX 监测.
Attach Listener
Attach Listener 线程是负责接收到外部的命令, 对该命令进行执行并把结果返回给发送者. 通常我们会用一些命令去要求 jvm 给我们一些反馈信息, 如: java -version,jmap,jstack 等等. 如果该线程在 jvm 启动的时候没有初始化, 那么, 则会在用户第一次执行 jvm 命令时, 得到启动.
main
main 线程, 就是我们代码所写得代码对应线程
Monitor Ctr-Break
这应该是 IDEA 通过反射的方式, 伴随你的程序一起启动的对你程序的监控线程. 这也是一个默认全局线程
Signal Dispatcher
前面提到的 Attach Listener 线程职责是接收外部 jvm 命令, 当命令接收成功后, 就会交给 signal dispather 线程分发到各个不同的模块处理, 并且返回处理结果. signal dispather 线程是在第一次接收外部 jvm 命令时, 才进行初始化工作.
Finalizer
这个线程是在 main 线程之后创建的, 其优先级为 10, 主要用于在垃圾收集前, 调用对象的 finalize()方法; 关于 Finalizer 线程的几点:
只有当开始一轮垃圾收集时, 才会开始调用 finalize()方法; 因此并不是所有对象的 finalize()方法都会被执行;
该线程是守护线程, 因此如果虚拟机中没有其他非守护线程的线程, 不管该线程有没有执行完 finalize()方法, JVM 也会退出;
JVM 在垃圾收集时会将失去引用的对象包装成 Finalizer 对象(Reference 的实现), 并放入 ReferenceQueue, 由 Finalizer 线程来处理; 最后将该 Finalizer 对象的引用置为 null, 由垃圾收集器来回收;
JVM 为什么要单独用一个线程来执行 finalize()方法呢? 如果 JVM 的垃圾收集线程自己来做, 很有可能由于在 finalize()方法中误操作导致 GC 线程停止或不可控, 这对 GC 线程来说是一种灾难, 所以单独创建了一个守护线程.
Reference Handler
VM 在创建 main 线程后就创建 Reference Handler 线程, 其优先级最高, 为 10, 它主要用于处理引用对象本身 (软引用, 弱引用, 虚引用) 的垃圾回收问题.
经过上面的分析可以看出来 main 本身程序的线程有: main 线程, Reference Handler 线程, Finalizer 线程, Attach Listener 线程, Signal Dispatcher 线程.
java 代码想要实现也很简单, 如下即可:
- // 获取 java 线程管理器 MXBean,dumpAllThreads 参数:
- // lockedMonitors 参数表示是否获取同步的 monitor 信息
- // lockedSynchronizers 表示是否获取同步的 synchronizer
- ThreadInfo[] threadInfos = ManagementFactory.getThreadMXBean().dumpAllThreads(true, false);
- for (ThreadInfo threadInfo : threadInfos) {
- System.out.println(threadInfo.getThreadId() + ":" + threadInfo.getThreadName());
- }
得到的打印结果为:
也就是说, 写了那么多年的 HelloWorld 居然有五个线程来支撑, 而我却一直被蒙在鼓里?? 谁能随时去关注项目有多少个线程啊, VIsualVM 可以 = =, 虽然我觉得他一直起线程进行通信很蠢, 但是项目结构大了就有必要了.
Spring-Boot
那么一个啥都没有的 springBoot 项目启动了之后, 会有哪些线程呢? 先看看他的 pom 文件:
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.2.1.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.visual.vm.performance</groupId>
- <artifactId>mock</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>mock</name>
- <properties>
- <java.version>1.8</java.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
只引入了 spring-boot-starter-Web 的依赖, 其他的什么都没有, 启动着试一下. 共有 27 个线程, 守护线程有 23 个.
不同的颜色对应着不同的状态, 详情看右下角. 这些线程很多都是熟悉的, Main 方法分析过的, 通过 VisualVM 工具进行 JMX 监视 (RMI TCP...) 开了些线程; IDEA(Monitor Ctrl-Break)开了些线程; 垃圾回收 (Finalizer,Reference Handler) 开了些线程. 着重讲一下没见过的线程.
DestroyJavaVM
所有 POJO 应用程序都通过调用该 main 方法开始. 正常情况下, main 完成后, 将告知 JVM 的 DestroyJavaVM` 线程来关闭 JVM, 该线程等待所有非守护进程线程完成后再进行工作. 这是为了确保创建的所有非守护程序线程都可以在 JVM 拆除之前运行完毕.
但是, 带有 GUI 的应用程序通常以多个线程运行. 用于监视系统事件, 例如键盘或鼠标事件. JVM 仍然会创建 DestroyJavaVM 线程, 且需要等待所有创建的线程完成, 然后再拆除 VM, 然而应用并不会停止, 所以 DestoryJavaVM 线程就会一直处于等待, 直到应用运行完成.
任何创建线程并仅依赖其功能的应用程序都会有一个 DestroyJavaVM 线程, 等待应用程序完成并关闭 JVM. 由于它等待所有其他线程执行完毕(join), 因此它不会消耗任何资源.
Http-nio-8080-Acceptor,Http-nio-8080-ClientPoller,Http-nio-8080-BlockPoller,http-nio-8080-exec-1...10
这些线程都有个特点, http-nio-8080 开头. 8080 就是这个应用的端口, 显然这是给容器使用的. 项目引入的是 spring-boot-starter-Web 依赖, 也就是默认使用 springBoot 的内置 tomcat 容器启动, 我们的 maven 下面也会有这样的几个包: tomcat-embed-core,tomcat-embed-el,tomcat-embed-websocket, 我们所看到的线程都是由这几个包产生的. 那么这些线程是干什么用的?
解决这个问题之前, 先看一下 tomcat 的总体架构:
Tomcat 由 Connector 和 Container 两个核心组件构成, Connector 组件负责网络请求接入, 目前支持 BIO,NIO,APR 三种模式, Tomcat5 之后就支持了 NIO, 看我们的线程名也就是用的 NIO;Container 组件负责管理 servlet 容器. service 服务将 Container 和 Connector 又包装了一层, 使得外部可以直接获取. 多个 service 服务运行在 tomcat 的 Server 服务器上, Server 上有所有的 service 实例, 并实现了 LifeCycle 接口来控制所有 service 的生命周期.
而 NIO 对应线程主要是实现在 Connector 组件中, 他负责接受浏览器发过来的 tcp 请求, 创建一个 Reuqest 和 Response 对象用来请求和响应, 然后产生一个线程, 将 Request 和 Response 分发给他们对应处理的线程.
终于看到了线程名中包含的 Acceptor,Poller. 他们都在 Connector 组件下的 Http11NioProtocol 下. 着重介绍一下 Http11NioProtocol 下面的几个组件
Acceptor: 接受 socket 线程, 接受的方法比较传统: serverSocket.accept(), 得到 SocketChannel 对象并封装到 NioChannel 对象中. 然后 NioChannel 对象封装在 PollerEvent 对象中, 并放到 events queue 中. 使用队列 (生产者 - 消费者) 和 Poller 组件交互, Acceptor 是生产者, Poller 是消费者, 通过 events queue 通信.
- package org.apache.tomcat.util.NET;
- public class Acceptor<U> implements Runnable {
- ...
- public void run() {
- byte errorDelay = 0;
- while(this.endpoint.isRunning()) {
- ....
- try {
- this.endpoint.countUpOrAwaitConnection();
- if (!this.endpoint.isPaused()) {
- Object socket = null;
- try {
- // 这句会调用 NioEndPoint 类, 底层是 serverSock.accept()
- socket = this.endpoint.serverSocketAccept();
- } catch (Exception var6) {
- ...
- }
- ...
- }
- } catch (Throwable var7) {
- ...
- }
- }
- this.state = Acceptor.AcceptorState.ENDED;
- }
- }
Poller:NIO 选择器 Selector 用于检查一个或多个 NIO Channel(通道)的状态是否可读, 可写. 如此可以实现单线程管理多个 channels 也就是可以管理多个网络线程. Poller 是 NIO 实现的主要线程, 首先从 events queue 队列中消费得到 PollerEvent 对象, 再将此对象中的 Channel 以 OP_READ 事件注册到主 Selector 中, Selector 执行 select 操作, 遍历出可以读数据的 socket, 并从 Worker 线程池中拿到可用的 Workrer 线程, 将可用的 socket 传递给 Worker 线程.
package org.apache.tomcat.util.NET; public class Poller implements Runnable { ... public void run() { while(true) { boolean hasEvents = false; label59: { try { if (!this.close) { hasEvents = this.events(); if (this.wakeupCounter.getAndSet(-1L)> 0L) { this.keyCount = this.selector.selectNow(); } else { // selector.select 方法, 接受 acceptor 的 socket this.keyCount = this.selector.select(NioEndpoint.this.selectorTimeout); } this.wakeupCounter.set(0L); } if (!this.close) { break label59; } this.events(); this.timeout(0, false); try { this.selector.close(); } catch (IOException var5) { NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorCloseFail"), var5); } } catch (Throwable var6) { ExceptionUtils.handleThrowable(var6); NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorLoopError"), var6); continue; } NioEndpoint.this.getStopLatch().countDown(); return; } if (this.keyCount == 0) { hasEvents |= this.events(); } Iterator iterator = this.keyCount> 0 ? this.selector.selectedKeys().iterator() : null; while(iterator != null && iterator.hasNext()) { SelectionKey sk = (SelectionKey)iterator.next(); NioEndpoint.NioSocketWrapper socketWrapper = (NioEndpoint.NioSocketWrapper)sk.attachment(); if (socketWrapper == null) { iterator.remove(); } else { iterator.remove(); // 然后调用 processKey 方法, 将 socket 传给 worker 线程进行处理 this.processKey(sk, socketWrapper); } } this.timeout(this.keyCount, hasEvents); } } }
Worker:Worker 线程从 Poller 传过来的 socket 后, 将 socket 封装在 SocketProcessor 对象中, 然后从 Http11ConnectionHandler 获取 Http11NioProcessor 对象, 从 Http11NioProcessor 中调用 CoyoteAdapter 的逻辑(这就出了 Http11NioProtocol 组件, 可以看上上图). 在 Worker 线程中, 会完成从 socket 中读取 http request, 解析成 HttpervletRequest 对象, 分派到相应的 servlet 并完成逻辑, 然而将 response 通过 socket 发回 client.
package org.apache.tomcat.util.NET; protected class SocketProcessor extends SocketProcessorBase<NioChannel> { public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) { super(socketWrapper, event); } protected void doRun() { // 这一句从 Poller 拿到 socket, 然后进行 tomcat 主线程处理流程 NioChannel socket = (NioChannel)this.socketWrapper.getSocket(); SelectionKey key = socket.getIOChannel().keyFor(socket.getSocketWrapper().getPoller().getSelector()); NioEndpoint.Poller poller = NioEndpoint.this.poller; if (poller == null) { this.socketWrapper.close(); } else { try { int handshake = -1; try { if (key != null) { if (socket.isHandshakeComplete()) { handshake = 0; } else if (this.event != SocketEvent.STOP && this.event != SocketEvent.DISCONNECT && this.event != SocketEvent.ERROR) { handshake = socket.handshake(key.isReadable(), key.isWritable()); this.event = SocketEvent.OPEN_READ; } else { handshake = -1; } } } catch (IOException var13) { handshake = -1; if (NioEndpoint.log.isDebugEnabled()) { NioEndpoint.log.debug("Error during SSL handshake", var13); } } catch (CancelledKeyException var14) { handshake = -1; } if (handshake == 0) { SocketState state = SocketState.OPEN; if (this.event == null) { state = NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.OPEN_READ); } else { state = NioEndpoint.this.getHandler().process(this.socketWrapper, this.event); } if (state == SocketState.CLOSED) { poller.cancelledKey(key, this.socketWrapper); } } else if (handshake == -1) { NioEndpoint.this.getHandler().process(this.socketWrapper, SocketEvent.CONNECT_FAIL); poller.cancelledKey(key, this.socketWrapper); } else if (handshake == 1) { this.socketWrapper.registerReadInterest(); } else if (handshake == 4) { this.socketWrapper.registerWriteInterest(); } } catch (CancelledKeyException var15) { ... } finally { ... } } } }
NioSelectorPool:NioEndPoint 对象维护了一个 NioSelectorPool 对象, 这个 NioSelectorPool 中又维护了一个 BlockPoller 线程(基于 Selector 进行 NIO 逻辑).
总结
平时看起来很熟悉的代码, HelloWorld 和 SpringBoot 初始化的项目. 没想到背地里有那么多线程来支撑. 装了 VisualVM 插件并不是让你蹭的就变强, 但是可以给你提供一些进步的思路, 引导你去思考去进步. 下面还会继续带着分析更复杂的项目, 不知道会不会有更多常见又未知的知识点等待我们去发现~
来源: https://www.cnblogs.com/metabolism/p/12004190.html