JVM

JVM

类初始化

Java对象初始化全过程

image

Loading 第一阶段

  • 类加载过程

    loadClass()与findClass()的调用逻辑,其实就是双亲委派的过程

    image

  • 双亲委派

    双亲委派机制

    image

    • ExtClassLoader和AppClassLoader中的loadClass()方法都是调用的super.loadClass();即调用的ClassLoader中的loadClass()方法。
    • ExtClassLoader和AppClassLoader都是Launcher的内部类,都继承于URLClassLoader => SecureClassLoader => ClassLoader。
    • 自定义类加载器只需要继承ClassLoader,重写findClass()方法,调用父类defineClass将字节流转换为class对象。
    • 双亲委派规则是出于安全考虑,如需打破双亲委派,重写loadClass()方法即可。
  • ClassLoader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    synchronized (getClassLoadingLock(name)) {
    // First, check if the class has already been loaded
    // 先查一下缓存中有没有加载过该Class
    Class<?> c = findLoadedClass(name);
    // 缓存中没有则调用父加载器进行加载
    if (c == null) {
    try {
    if (parent != null) {
    // 调用父加载器进行加载,App=>Ext=>Bootstrap
    c = parent.loadClass(name, false);
    } else {
    // 调用Bootstrap加载器加载
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }

    if (c == null) {
    // If still not found, then invoke findClass in order
    // to find the class.
    // 在父加载器无法加载的时候
    // 调用子类findClass去加载,Bootstrap=>Ext=>App
    c = findClass(name);
    }
    }
    if (resolve) {
    resolveClass(c);
    }
    return c;
    }
    }
  • Launcher

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
    // 初始化ExtClassLoader
    // 使用双重检查单例初始化ExtClassLoader,父类为NULL即BootstrapClassLoader
    var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
    throw new InternalError("Could not create extension class loader", var10);
    }

    try {
    // 初始化AppClassLoader
    // 指定了AppClassLoader的父加载器为ExtClassLoader,即var1=ExtClassLoader
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
    throw new InternalError("Could not create application class loader", var9);
    }

    ...

    // ExtClassLoader
    static class ExtClassLoader extends URLClassLoader {
    private static volatile Launcher.ExtClassLoader instance;

    // 单例模式-双重检查
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
    if (instance == null) {
    Class var0 = Launcher.ExtClassLoader.class;
    synchronized(Launcher.ExtClassLoader.class) {
    if (instance == null) {
    instance = createExtClassLoader();
    }
    }
    }
    return instance;
    }

    // 初始化ExtClassLoader
    private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
    try {
    return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
    public Launcher.ExtClassLoader run() throws IOException {
    // ExtClassLoader加载的路径地址
    File[] var1 = Launcher.ExtClassLoader.getExtDirs();
    ...

    // 初始化操作
    return new Launcher.ExtClassLoader(var1);
    }
    });
    } catch (PrivilegedActionException var1) {
    throw (IOException)var1.getException();
    }
    }

    // ExtClassLoader加载的路径地址
    private static File[] getExtDirs() {
    String var0 = System.getProperty("java.ext.dirs");
    ...
    }

    // 调用父类构造方法进行初始化,传参指定父加载器,这里Ext指定的null
    public ExtClassLoader(File[] var1) throws IOException {
    super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
    ...
    }
    }

    // AppClassLoader
    static class AppClassLoader extends URLClassLoader {
    ...

    // 初始化AppClassLoader,var0=ExtClassLoader
    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    // AppClassLoader加载的路径地址
    final String var1 = System.getProperty("java.class.path");
    ...
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
    public Launcher.AppClassLoader run() {
    ...
    return new Launcher.AppClassLoader(var1x, var0);
    }
    });
    }

    // 调用父类构造方法进行初始化,传参指定父加载器,这里App指定的Ext
    // 即var2=ExtClassLoader
    AppClassLoader(URL[] var1, ClassLoader var2) {
    super(var1, var2, Launcher.factory);
    ...
    }
    }
    }

Linking 第二阶段

  • Verification

    验证文件是否符合JVM规定

  • Preparation

    静态成员变量赋默认值(系统初始化的值)

  • Resolution

    将类、方法、属性等符号引用解析为直接引用

Initializing 第三阶段

开始执行字节码,调用类初始化代码 ,给静态成员变量赋初始值(用户定义的值),执行构造方法

JMM内存模型

JMM(Java Memory Model),是Java虚拟机规范中所定义的一种内存模型。在此内存模型中,规定了,所有的共享变量都是存储于主内存中,每个线程都是将主内存中的共享变量拷贝一份副本,到本线程的本地内存中,然后操作此共享变量副本,修改后,再同步更新到主内存中,因此高并发下就会出现变量修改的问题了。

总结:JMM内存模型是一种规范,用来保证并发编程中共享内存的原子性,可见性,有序性。

JMM保证了以下几点:

原子性 Atomicity

一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。

在 Java 中,可以借助 Synchronized 和各种 Lock 可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (Compare And Swap) 操作(可能也会用到 volatile 或者 final 关键字)来保证原子操作。

可见性 Visibility

当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。

在 Java 中,可以借助 Synchronized 、volatile 以及各种 Lock 实现可见性。如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

有序性 Ordering

由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。

在 Java 中,volatile 关键字可以禁止指令进行重排序优化。

image

JVM内存模型

JVM(Java Virtual Machine),是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

总结:JVM内存区域是一种规范,屏蔽了与具体操作系统平台相关的信息,使字节码可以运行在不同的平台。

JVM运行时数据区的划分:

程序计数器 线程私有

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

虚拟机栈 线程私有

Java虚拟机栈(Java Virtual Machine Stack)描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、返回地址等信息。每一个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法栈 线程私有

本地方法栈(Native Method Stack) 与虚拟机栈所发挥的作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

方法区 线程共享

方法区(Method Area)用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量(Runtime Constant Pool)是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

堆内存 线程共享

Java堆(Java Heap)用来存放对象实例。是Java虚拟机所管理的内存中的最大的一块。

image

常量池分类

常量池 class文件

class 常量池(Class Constant Pool)

java文件被编译成class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池(Constant Pool),用于存放编译器生成的各种 字面量(Literal)符号引用(Symbolic References)

总结: class文件的一部分,每个类文件都包含一个常量池,存放编译器生成的字面量以及符号引用。

image

运行时常量池 方法区

运行时常量池(Runtime Constant Pool)

jvm在加载某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。当类加载到内存中后(解析之后),jvm则会将class常量池中的内容存放到运行时常量池中,同时将符号引用替换为直接引用。

总结: 在方法区中,每个类都有一个运行时常量池,当类加载到内存中后,jvm则会将class常量池中的内容存放到运行时常量池中,同时将符号引用替换为直接引用,这个过程被称为动态链接。

image
image

字符串常量池 堆内存

字符串常量池(String Constant Pool)

在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到String Constant Pool中。所以String Constant Pool 中存的是字符串对象的引用,而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。

在HotSpot VM里实现的String Constant Pool功能的是一个StringTable类的哈希表,且在每个HotSpot VM实例中只有一份,被所有类共享,这个StringTable存的是字符串(用双引号括起来的部分)的引用,引用指向具体堆中的字符串实例。

总结: 在堆内存中,每个虚拟机仅有一份字符串常量池,存放的是字符串对象实例的引用,引用指向具体堆中的字符串实例。

image

Garbage Collection

定位垃圾

  • 引用计数(ReferenceCount)

    记录对象是否被其他对象引用,当对象没有被其他对象引用就说明这个对象已经可以作为垃圾进行回收了。问题是无法解决循环引用。

  • 可达性分析算法(RootSearching)

    通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。

垃圾回收算法

  • 标记清除算法(Mark-Sweep)

    标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。

    优点:实现简单

    缺点:位置不连续,容易产生碎片,效率偏低(两遍扫描)

    备注:CMS使用标记清除算法

  • 复制清除算法(Copying)

    将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。

    优点:没有碎片

    缺点:浪费空间

    备注:目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space, To Space),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor空间中。

  • 标记整理算法(Mark-Compact)

    先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存。

    优点:没有碎片

    缺点:效率偏低(两遍扫描,指针需要调整)

    备注:Serial Old / Parallel Old使用标记整理算法

并发标记算法

  • 三色标记

    白色:未被标记的对象

    灰色:自身被标记,成员变量未被标记

    黑色:自身和成员变量均已标记完成

    未被标记的对象会被垃圾回收器清理

    image

    漏标问题

    只有两者同时出现时,漏标问题才会产生

    1. Remark过程中,黑色指向了白色,如果不对黑色重新扫描,则会漏标,会把白色D对象当做没有新引用指向从而被回收掉。
    2. 并发标记过程中,Mutator删除了所有从灰色到白色的引用,会产生漏标,此时白色对象应该被回收。

    解决漏标问题的核心算法

    1. Incremental Update

      CMS用到,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。

    2. Snapshot At The Beginning(SATB)

      G1用到,关注引用的删除,当灰色->白色消失时,要把这个引用push到GC的堆栈,保证D还能被GC扫描到(由于有RSet存在,不需要扫描整个堆去查找指向白色的引用,效率比较高,SATB配合RSet使用,浑然天成)。

  • 颜色指针

    ZGC使用

分代模型

image

  • Minor GC / YGC

    年轻代空间耗尽时触发

  • Major GC / FGC

    在老年代无法继续分配空间时触发,新生代老年代同时进行回收

对象分配过程

image

栈上分配

对那些作用于不会逃逸出方法的对象,在分配内存时,不在将对象分配在堆内存中,而是将对象属性打散后分配在线程私有栈内存上,这样随着方法调用结束,栈上分配打散的对象也被回收掉,不在增加 GC 额外压力。

栈上分配需要有一定的前提:

  • 开启逃逸分析

    1
    -XX:+DoEscapeAnalysis

    逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外,在server虚拟机模式下才可以开启(jdk1.6默认开启)

  • 开启标量替换

    1
    -XX:+EliminateAllocations

    标量替换的作用是允许将对象根据属性打散后分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配,默认该配置为开启。

TLAB

TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区(线程私有分配区,私有分配,公共查看),占用 Eden 区(缺省 Eden 的1%),默认开启,JVM 会为每一个线程分配一块 TLAB 区域,避免堆对象共享造成的多线程线程同步。

  • 开启TLAB

    1
    -XX: +UseTLAB 

    此功能默认开启

垃圾回收器

常用搭配规则:

  • Serial + Serial Old
  • Parallel Scavenge + Parallel Old (jdk1.8 默认)
  • ParNew + CMS

image

Serial / Serial Old

年轻代:Serial 串行回收 复制清除算法

老年代:Serial Old 串行回收 标记整理算法

image

Parallel Scavenge / Parallel Old

年轻代:Parallel Scavenge 并行回收 复制清除算法

老年代:Parallel Old 并行回收 标记整理算法

image

ParNew / CMS

STW:大概意思就是在安全点将工作线程停住,开始回收垃圾(会影响用户体验)

年轻代:ParNew对Parallel Scavenge进行了增强,配合CMS的并行回收 复制清除算法

老年代:CMS并发回收(无法忍受SWT)标记清除算法

image

语义的区别

  1. PS/PO是用户线程停住,多个GC线程并行回收的概念
  2. CMS是用户线程不用停,跟GC线程并发运行的概念

标记过程

  1. 初始标记:标记根对象
  2. 并发标记:边产生垃圾边清理(最浪费时间)
  3. 重新标记:标记并发标记过程中产生的新垃圾或者取消又被引用的垃圾
  4. 并发清理:并发清理垃圾,并发清理过程中产生的垃圾叫做浮动垃圾,等下次GC清理

缺点

  1. 因为采用标记清除算法,会产生很多碎片

    解决办法:对内存空间进行压缩

    多少次FGC之后进行压缩

    1
    -XX:CMSFullGCsBeforeCompaction 
  2. 会有浮动垃圾

    对于内存很大的情况,当内存已经满了的时候,新对象无法分配内存,这个时候CMS会使用Serial Old去清理垃圾,严重影响效率。

    解决办法:降低触发CMS的阈值

    使用多少比例的老年代后开始CMS收集,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收)

    1
    -XX:CMSInitiatingOccupancyFraction 

G1

逻辑上分代(Eden区,Survivor区,Old区,Humongous区-大对象区),物理不分代

YGC

发生在年轻代的GC算法,一般对象(除了大对象)都是在Eden Region中分配内存,当所有Eden Region被耗尽无法申请内存时,就会触发一次YGC,这种触发机制和之前的YGC差不多,执行完一次YGC,活跃对象会被拷贝到Survivor Region或者晋升到Old Region中,空闲的Region会被放入空闲列表中,等待下次被使用。

MixedGC

当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即MixedGC,除了回收整个Eden Region,还会回收一部分的Old Region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些Old Region进行收集,从而可以对垃圾回收的耗时时间进行控制。

MixedGC触发阈值参数,默认45%,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次MixedGc

1
-XX:InitiatingHeapOccupancyPercent

MixedGC的执行过程有点类似CMS,主要分为以下几个步骤:

  1. Initial Mark:初始标记过程,整个过程STW,标记了从GC Root可达的对象。
  2. Concurrent Marking: 并发标记过程,整个过程GC Collector线程与工作线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息。
  3. Remark: 重新标记过程,整个过程STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象。
  4. Clean Up: 垃圾清除过程,将存活的对象复制到另外一个Region区域,复制同时进行了压缩,碎片没有CMS多。(标记整理算法)

FGC

如果对象内存分配速度过快,Mixed GC来不及回收,导致老年代被填满,就会触发一次FGC,G1的FGC算法就是单线程执行的Serial Old ,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免FGC。

image

补充知识:

CSet

Collection Set:一组可被回收的分区的集合。

在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden区,Survivor区,Old区。CSet会占用不到整个堆空间的1%的大小。

RSet

Remember Set:记录了其他Region中的对象到本Region的引用。

RSet的价值在于使得垃圾回收器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

CardTable

Card Table:记录了老年代到新生代的引用。

由于新生代的垃圾收集通常很频繁(YGC),如果老年代对象引用了新生代的对象,使用Cara Table就可以避免扫描整个老年代,以减少系统开销。

JVM调优参数

GC常用参数

  • -Xmn -Xms -Xmx -Xss (年轻代 最小堆 最大堆 栈空间-指设定每个线程的堆栈大小)
  • -XX:+PrintCommandLineFlags (查看使用的GC配置参数)
  • -XX:+UseTLAB (使用TLAB,默认打开)
  • -XX:+PrintTLAB (打印TLAB的使用情况)
  • -XX:TLABSize (设置TLAB大小)
  • -XX:NewRatio(新生代与老年代的比值,默认2,即新生代:老年代=1:2,年轻代占整个堆的1/3)
  • -XX:+DisableExplictGC (System.gc()不管用 ,FGC)
  • -XX:+PrintGC (打印GC日志)
  • -XX:+PrintGCDetails (打印GC详细日志)
  • -XX:+PrintHeapAtGC
  • -XX:+PrintGCTimeStamps
  • -XX:+PrintGCApplicationConcurrentTime (打印应用程序时间)
  • -XX:+PrintGCApplicationStoppedTime (打印暂停时长)
  • -XX:+PrintReferenceGC (记录回收了多少种不同引用类型的引用)
  • -XX:+PrintVMOptions
  • -XX:+HeapDumpOnOutOfMemoryError (设置OOM时生成堆转储文件)
  • -XX:HeapDumpPath=/tmp/heapdump.hprof (指定堆转储文件时的路径或文件名)
  • -XX:+PrintFlagsFinal | grep XXX (查看所有(或指定)JVM参数启动的初始值)
  • -XX:MaxTenuringThreshold (升代年龄,默认值15)
  • -verbose:class (类加载详细过程)
  • -Xloggc:/opt/log/gc.log (记录GC日志)

Parallel常用参数

  • -XX:SurvivorRatio(Eden区与Subrvivor区大小的比值,默认8,即8:1:1)
  • -XX:PreTenureSizeThreshold(大对象到底多大)
  • -XX:MaxTenuringThreshold(升代年龄,默认值15)
  • -XX:+ParallelGCThreads (并行收集器的线程数,同样适用于CMS,一般设为和CPU核数相同)
  • -XX:+UseAdaptiveSizePolicy (自动选择各区大小比例)

CMS常用参数

  • -XX:+UseConcMarkSweepGC
  • -XX:ParallelCMSThreads (CMS线程数量)
  • -XX:CMSInitiatingOccupancyFraction (老年代回收比例,默认是68%(近似值),如果频繁发生SerialOld卡顿,应该调小,(频繁CMS回收))
  • -XX:+UseCMSCompactAtFullCollection (在FGC时进行压缩)
  • -XX:CMSFullGCsBeforeCompaction (多少次FGC之后进行压缩)
  • -XX:+CMSClassUnloadingEnabled(CMS是不会默认对永久代进行垃圾回收的,设置此参数则是开启)
  • -XX:CMSInitiatingPermOccupancyFraction (达到什么比例时进行Perm回收GCTimeRatio 设置GC时间占用程序运行时间的百分比)
  • -XX:MaxGCPauseMillis (停顿时间,是一个建议时间,GC会尝试用各种手段达到这个时间,比如减小年轻代)

G1常用参数

  • -XX:+UseG1GC
  • -XX:MaxGCPauseMillis (建议值,G1会尝试调整Young区的块数来达到这个值)
  • -XX:GCPauseIntervalMillis (GC的间隔时间)
  • -XX:G1HeapRegionSize (分区大小,建议逐渐增大该值,1 2 4 8 16 32随着size增加,垃圾存活时间更长,GC间隔更长,但每次GC的时间也会更长 )
  • -XX:G1NewSizePercent (新生代最小比例,默认为5%)
  • -XX:G1MaxNewSizePercent (新生代最大比例,默认为60%)
  • -XX:GCTimeRatio (GC时间建议比例,G1会根据这个值调整堆空间)
  • -XX:ConcGCThreads (线程数量)
  • -XX:InitiatingHeapOccupancyPercent (启动G1的堆空间占用比例 默认45%)

JVM排查工具

JDK工具

Linux PID 和 TID

PID:PID是进程号的缩写,它是一个唯一的数字,用于标识Linux操作系统的进程。每个进程都有一个PID,它由内核动态分配,以便Linux操作系统可以区分不同的进程并在进程终止时清理它所使用的资源。当一个进程被创建时,它会被分配一个新的PID,而当一个进程终止时,它的PID就会被释放,从而被系统重新使用。

TID:TID是线程ID的缩写,它是一个数字,用于标识Linux操作系统的线程。在Linux中,线程被视为一种特殊的进程,每个线程都有一个唯一的TID。因为Linux支持多线程,所以我们需要一种机制来区分不同的线程。每个线程都具有与其关联的进程的PID,并具有其自己的TID,以便操作系统可以在多个线程之间进行调度。

PID 和 TID 关系

每个进程都有一个唯一的PID,但在Linux操作系统中,一个进程可以有多个线程。这些线程共享相同的进程上下文,并使用相同的系统资源。因此,在Linux中,进程和线程之间并没有严格的区别,它们可以被视为同一种实体。所以,我们在使用Linux操作系统时,需要理解PID和TID之间的关系。

在Linux中,每个线程都由一个进程创建。当一个进程创建一个线程时,该线程会继承其父进程的PID,并被分配一个新的TID。这种机制使得Linux在多线程运行时能够更加灵活地对线程进行调度,同时也便于我们在进程和线程之间切换。

查看PID

1
top

查看TID

1
top -Hp <pid>

JPS

jps是jdk提供的一个查看当前java进程的小工具, 可以看做是JavaVirtual Machine Process Status Tool的缩写

jps参数

  • no option :查看java进程
  • -q :仅输出VM标识符,不包括classname,jar name,arguments in main method
  • -m :输出传递给 Java 进程 main() 函数的参数
  • -l :输出主类的全名,如果进程执行的是 Jar 包,输出 Jar 路径
  • -v :输出虚拟机进程启动时 JVM 参数
  • -V :输出通过flag文件传递到JVM中的参数
  • -Joption :传递参数到vm,例如:-J-Xms512m

jps常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 显示进程的 PID 和 类的名称
jps

# 输出输出完全的包名,应用主类名,jar的完全路径名
jps –l

# 输出jvm参数
jps –v

# 显示java进程号
jps –q

# main 方法
jps -m

# 远程查看
jps -l xxx.xxx.xx.xx

image

JSTACK

jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息

jstack参数

  • <pid> :打印线程信息
  • -l :长列表,打印关于锁的附加信息,例如属于java.util.concurrent 的 ownable synchronizers列表
  • -F :当‘jstack [-l] ’没有相应的时候强制打印栈信息
  • -m :打印java和native c/c++框架的所有栈信息
  • -h | -help :打印帮助信息

jstack常用命令

1
2
3
4
5
6
7
8
# 输出<pid>=30200的线程栈信息
jstack 30200

# java和native c/c++框架的所有栈信息
jstack -m 30200

# 额外的锁信息列表,查看是否死锁
jstack -l 30200

image

JINFO

jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括Java System属性和JVM命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息

jinfo参数

  • <pid> :输出全部的参数和系统属性
  • -flag :输出对应名称的参数
  • -flag [+|-] :开启或者关闭对应名称的参数
  • -flag = :设定对应名称的参数
  • -flags :输出全部的参数
  • -sysprops :输出系统属性

jinfo常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 输出当前 jvm 进程的全部参数和系统属性
jinfo 30200

# 输出所有的参数
jinfo -flags 30200

# 查看指定的 jvm 参数的值,name=MaxHeapSize
jinfo -flag MaxHeapSize 30200

# 开启/关闭指定的JVM参数
jinfo -flag +PrintGC 30200

# 输出当前 jvm 进行的全部的系统属性
jinfo -sysprops 30200

image

JMAP

命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列

jmap参数

  • -heap <pid> : 显示Java堆详细信息
  • -histo[:live] : 显示堆中对象的统计信息
  • -clstats :打印类加载器信息
  • -finalizerinfo : 显示在F-Queue队列等待Finalizer线程执行finalizer方法的对象
  • -dump: :生成堆转储快照
  • -F : 当-dump没有响应时,使用-dump或者-histo参数 在这个模式下,live子参数无效.
  • -help :打印帮助信息
  • -J :指定传递给运行jmap的JVM的参数

jmap常用命令

1
2
3
4
5
6
7
8
9
# 查看堆的情况
jmap -heap 30200

# dump
jmap -dump:live,format=b,file=/tmp/heap2.bin 30200
jmap -dump:format=b,file=/tmp/heap3.bin 30200

# 查看堆的占用
jmap -histo 30200 | head -10

image

JSTATE

jstat(JVM Statistics Monitoring Tool) 使用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程(需要远程主机提供 RMI 支持)虚拟机进程中的类信息、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI,只提供了纯文本控制台环境的服务器上,它将是运行期间定位虚拟机性能问题的首选工具

jstat参数

  • -gcutil <pid>:显示垃圾收集信息
  • -class :显示 ClassLoader 的相关信息
  • -compiler :显示 JIT 编译的相关信息
  • -gc :显示与 GC 相关的堆信息
  • -gccapacity :显示各个代的容量及使用情况
  • -gcnew :显示新生代信息
  • -gcnewcapcacity :显示新生代大小与使用情况
  • -gcold :显示老年代和永久代的行为统计,从jdk1.8开始,该选项仅表示老年代,因为永久代被移除了
  • -gcoldcapacity :显示老年代的大小
  • -gcpermcapacity :显示永久代大小,从jdk1.8开始,该选项不存在了,因为永久代被移除了

jstat常用命令

1
2
3
4
5
# 显示垃圾收集信息,每隔 1000ms 打印一次
jstat -gcutil 30200 1000

# 分析进程 id 为 30200 的 gc 情况,每隔 1000ms 打印一次记录,打印 10 次停止,每 3 行后打印指标头部
jstat -gc -h3 30200 1000 10

image

image

JVisual VM

可视化工具,压测监控使用,生产不建议使用

image

在线排查工具

Arthas

参考资料:

Arthas官网介绍

调试排错 - Java 问题排查之应用在线调试Arthas