分类 Java 相关 下的文章

如何获取 Java 应用 每个线程的 CPU usage

很多时候, 我们看到 Java 应用程序的 CPU 使用率很高, 或者是 GC 导致的, 或者是死循环导致的, 或者是执行某些特耗 CPU 的操作, 比如使用正则表达式匹配. 那么如何找这个使用率特别高的线程呢?


如何找出CPU 使用率最高的线程?

在 Linux 操作系统下, 使用 top -i 命令很容易找到CPU 使用率最高的进程, 如果使用 top -i -H 就很容易找到这个线程.

找到线程后, 如何查看它在忙什么呢?

使用 Linux 系统自带的 strace, ftrae, ltrace 等, 可以查看 OS 级别的系统调用, 函数, 库等调用, 可是无法查看用户空间的更多信息.

如何找出那些方法在消耗 CPU

通常有 2 种方法.

  1. 第一种是你大概知道那个方法耗时, 它可能是比较靠外层的方法, 你可以给他手工在方法开始和结束加一些代码, 然后计算时间差, 通过某些方式把时间差展现给你. 某些工具提供了一些面向方面(面向切面 Aspect Oriented Programing)的接口, 可以通过在线 unload 某些 class, 然后重新 load 加入新的切面的 class 来计算时间差, 比如 阿里开源的 Arthas;
  2. 通过 Sampling 的方式, 这种方法通过不断的对线程 stack 做 snapshot 然后画出火焰图. 通过火焰图, 很容易观察出那里占用了大量的 CPU 时间. 不过这里有个权衡, 多久做一个线程 stack 的 snapshot, 如果做的太多, 那么会影响CPU 的占用实际情况, 如果做的太少, 可能错过了最耗 CPU 的时间点, 或者每次正好错过这个点.

一个 Java 自带比较折中的方法.

在 Java 里, 我们可以通过程序的方式列出当前应用里面的所有正在运行的 Java 线程, 同时通过 MBean API 可以知道任何一个线程在开始之后占用的 CPU 时间. 如果我们在某个时间间隔的开始记录所有线程已经使用的时间, 在间隔的结尾再次记录, 那么就可以知道任何线程在这段时间内使用 CPU 的占用情况. 在这个间隔中间终止的线程, 不能给出正确的结果. 幸好大部分 Java 应用程序大部分使用线程池. 同时在间隔结尾处, 可以记录下线程栈的, 能大概给出一些信息. 如果一个线程长时间使用 CPU 很高, 可以做多次, 然后大概看出那部分代码比较耗 CPU.

如何具体实现

  1. 获得 ThreadMXBean
    ManagementFactory.getThreadMXBean();

  2. 获得线程 和 其它相关信息 ThreadMXBean
    首先通过 isThreadCpuTimeSupported() 方法获得是不是支持;
    可以获得死锁线程 findDeadlockedThreads()
    可以获得所有线程 ID: getAllThreadIds()
    可以获得一个线程已经使用的 CPU 时间: getThreadCpuTime(threadId)
    可以获得一个线程 Blocked 和 Waited time:
    ThreadInfo 的getBlockedTime() 和 getBlockedTime()
    ThreadInfo 的getWaitedTime() 和 getWaitedCount()
    可以通过 ThreadInfo 获得 stack -> getStackTrace()

  3. 其它要注意的地方
    ..1. 有几个 CPU -> Runtime.getRuntime().availableProcessors();
    ..2. 获得时间是 Nano time;
    ..3. 可以对使用率做百分比, 可以排序, 可以只看 Blocked 线程等.

ZGC

Java 11开始, 引入了一个新的实验性质的垃圾收集器 Z Garbage Collector. 它能处理几 GB 到几 TB 的 heap, 保证 stop the world 的时间不超过10ms, 并且这个时间不随着 heap 的大小而增加.

官方主页: https://wiki.openjdk.java.net/display/zgc/Main

java debug JPDA (JavaTM Platform Debugger Architecture)

Java 是在 VM 里面运行的语言, 同时要做到平台无关性, 所以它有自己的 debug 接口和实现.
官方关于 JPDA 的链接: https://docs.oracle.com/javase/6/docs/technotes/guides/jpda/
jpda.png

上面的图很容易理解, VM 具体实现和 backend 接口直接使用 JVM TI 来作为通信接口.
Debugger 和 Debuggee 之间定义了协议: JDWP (这类似于 HTTP).
Debugger 自己的和后端通信以及自己的 UI 使用 JDI(这类似与 HTML)

如何远程 debug:
从 Java 9 开始:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8000 myApp
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 myApp

Java 9 之前:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 OurApplication

Java 5 之前:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 myApp

参数列表:
a list of options:

  1. transport is the only fully required option. It defines which transport mechanism to use. dt_shmem only works on Windows and if both processes run on the same machine while dt_socket is compatible with all platforms and allows the processes to run on different machines

  2. server is not a mandatory option. This flag, when on, defines the way it attaches to the debugger. It either exposes the process through the address defined in the address option. Otherwise, JDWP exposes a default one

  3. suspend defines whether the JVM should suspend and wait for a debugger to attach or not

  4. address is the option containing the address, generally a port, exposed by the debuggee. It can also represent an address translated as a string of characters (like javadebug if we use server=y without providing an address on Windows)

客户端最简陋的可以使用 jdk 自带 jdb.
更多信息参考: https://www.baeldung.com/java-application-remote-debugging

常见的压缩/解压 jar 文件

只是为了下次使用直接 copy:

解压 jar 文件:

jar xvf xxx.jar 
unzip xxx.jar -d ./directoryToExtractTo

//使用 jar 命令只能解压到当前目录

压缩为 jar 文件:

jar cvf xxx.jar .

如果是使用 Spring boot big jar 的方式, 要制定 manifest 文件

jar cfm xxx.jar Manifest.txt MyPackage/*.class

more info: https://docs.oracle.com/javase/tutorial/deployment/jar/appman.html

Java NIO & NIO2 里面的 attachment 是怎么回事?

在 Java NIO 和 NIO2 里面我们经常看到attach 和 attachment 相关的 API, 如:

Objcet SelectionKey.attch(Object obj)
Object SelectionKey.attachment()
SelectionKey AbstractSelector.register(AbstractSelectableChannel ch,int ops, Object att);

这里的 Attachment 是一个 Object 对象, 也就是说它可以 attach 任何对象, 为什么要 attach 一个对象呢?
我们从 Blocking IO 说起, 当 Blocking IO 的时候, 读字节流或字符流的的线程一直 block 在读操作上, 不去干其它事情, 当来一个字节/字符的时候, 它就读一个, 直到返回 -1 代表读完(EOF). 可是当时 NIO 的时候, 读线程直接返回, 是由一个专门的 IO 线程操作 Selector 去读取的, 当有部分数据到达的时候, 它就帮你读取, 读完来的数据之后, 还没读取到 EOF, 那么要继续监听这个 socket, 再次等待它接下来的数据, 那么刚才读取的数据放哪里呢? 之后来的数据怎么拼接到原来的数据之后呢? 那么就需要有一个容器放置还没有读完的半成品, 在下次同一个 socket 来数据的时候, 再次拿出这个容器, 继续放数据, 那么这个容器怎么可以容易的每次都有数据的时候拿到呢? 那就放到 SelectionKey 上面吧, 反正每次都是通过 Selectionkey 知道有数据的, 一旦 SelectionKey 有数据, 就通过它拿到之前装有半成品的容器, 那么可以继续放了.

每次读完通过 attach(Object) 方法附在上面, 下次当有数据来的时候, 通过 attachment 方法拿到之前的容器.

参考: https://jfarcand.wordpress.com/2006/07/06/tricks-and-tips-with-nio-part-ii-why-selectionkey-attach-is-evil/