一文详解JVM

详解JVM

最近学习了:周志明《深入理解高并发编程》;;
特此简要对学习做了部分总结,方便后续对JVM相关知识的完善和巩固;
若想深入了解学习,可阅读上述参考原著;

Java内存区域与OOM

运行时数据区域

Java虚拟机在执行Java程序时,会将它所管理的内存空间,划分为若干不同的数据区域。

这些区域都有各自的用途、创建和销毁时间

Java虚拟机所管理的内存,将会包括以下几个运行时数据区域:

在这里插入图片描述

  1. 程序计数器

    一块较小的内存区域,可以看做是当前线程所执行的字节码的行号指示器

    在虚拟机概念模型中,字节码解释器就是通过改变此计数器的值,来选取下一条需要执行的字节码指令

    由于Java虚拟机多线程是通过线程切换,并分配CPU时间片来实现的;在任一特定时刻,一个处理器(或多核处理器的一个内核)都只会执行一条线程中的指令。因此,为了线程切换后,能够恢复到正确执行位置,每个线程都需要一个私有的计数器;各线程的计数器互不影响,独立存储。

    注意:

    ​ 若执行的是Java方法,则这个计数器的值为虚拟机字节码指令地址;

    ​ 若执行的是Native方法,则此值为空;

    ​ 此区域是唯一一个虚拟机规范中没有规定OOM情况的区域;

  2. java虚拟机

    Java虚拟机也是线程私有的,生命周期与线程相同;

    虚拟机栈描述的是Java方法执行的内存模型:每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

    每个方法从调用直到执行完成的过程,就对应着一个栈帧从虚拟机栈从入栈到出栈的过程;

    局部变量表存储了编译器可知的各种基本数据类型、对象引用;局部变量表所需的内存空间会在编译期完后分配;

    Java虚拟机规范中,此区域规定了两种可出现的异常:

    1. StackOverflowError:线程所请求的栈深度大于虚拟所允许的栈深度;

    2. OutofMemorryError:大部分虚拟机栈可动态扩展,若扩展时无法申请到足够的内存,则抛出此异常;

  3. 本地方法栈

    作用于虚拟机栈类似,最大的区别是:

    虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行Native方法服务

    由于虚拟机规范未对本地方法栈强制规定,因此具体虚拟机可自由实现,有的虚拟机(如Sun Hotspot)直接把本地方法栈和虚拟机栈合二为一;

    本地方法栈和虚拟机栈类似,也会抛出StackOverflowError和OutofMemorryError;

  4. java堆

    对大多数应用来说,Java堆是虚拟机所管理的内存中最大的一块;

    Java堆是对所有线程共享的内存区域,在Java虚拟机启动时创建

    此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存;

    Java堆是垃圾收集器管理的主要区域

    从内存回收的角度看,由于当前收集器都采用分代收集算法,所以Java堆还可细分为:新生代和老年代;

    再细致一点的话,可分为:Eden区、From Survivor区、To Survivor区等;

    如此进一步细分的目的是:为了更好的回收内存,或者更快的分配内存;

    根据Java虚拟机规范规定,Java堆可处于物理上不连续的内存空间中,只要逻辑上是连续的即可;

    如果堆中没有内存进行实例分配,并且堆也无法再扩展时,将会抛出OutofMemoryError异常;

  5. 方法区

    方法区与Java堆一样,是各个线程共享的内存区域;

    用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;

    Java虚拟机规范对方法区的限制非常宽松,除了和堆一样不需要连续空间和可选择固定大小或可扩展外,

    还可以选择不实现垃圾收集。

    垃圾收集行为在此区域是比较少出现的,此区域的内存回收主要是针对常量池的回收和对类型的卸载;

    根据Java虚拟机规范,当方法区无法满足内存分配需求时,将抛出OutofMemoryError异常;

  6. 运行时常量池

    运行时常量池是方法区的一部分;

    Class文件中除了有类的版本、字段、接口、方法等描述性信息外,还有一项信息是常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后,进入方法区的运行时常量池

    运行时常量池对于Class文件常量池的另一特征是具备动态性

    ​ Java语言并不要求常量一定要编译期才能产生,也即并非预置于Class文件中常量池的内容,才能进入运行时常量池;运行期间也可能将常量放入池中;此特性被利用的较多的便是String类的intern()方法;

    (调用intern()后,首先检查字符串常量池中是否有该对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入并返回给变量)

  7. 直接内存

    直接内存并不是虚拟机运行时数据区,也不是Java虚拟机规范中定义的内存区域,但这部分内存也被频繁的使用,且也可能导致OutofMemoryError异常;

    显然,本机直接内存的分配不会受到Java堆大小的限制;但既然是内存,肯定会收到本机总内存大小和处理器寻址空间的限制;

    当配置虚拟机参数时,忽略直接内存,使得各内存区域总和大于物理内存限制,从而导致动态扩展时出现OutofMemoryError异常;

实战OutofMemoryError异常

在Java虚拟机规范中,除了程序计数器外,其他几个运行时数据区域都有发生OutofMemoryError异常的可能,接下来将通过几个实例来验证异常发生的场景,并介绍几个内存相关的虚拟机参数;

先介绍几个JVM参数:

  1. -Xms:设置JVM初始堆内存的大小
  2. -Xmx:设置JVM最大堆内存的大小
  3. -Xmn:设置年轻代的大小
  4. -Xss:设置每个线程的栈的大小
  5. -XX:+HeapDumpOnOutofMemoryError:发生OOM异常时,生成heap dump文件
  6. -XX:HeapDumpPatch=patch:存放生成的heap dump文件路径;例如-XX:HeapDumpPath=F:\books
  7. -XX:+PrintGCDetails:打印GC的详细信息
  8. -XX:+PrintGCTimeStamps:打印GC的时间戳
  9. -XX:MetaspaceSize:设置元空间触发垃圾回收的大小
  10. -XX:MaxMetaspaceSize:设置元空间的最大值

实战模拟OOM异常:

​ 下文中的实例代码都是基于SUN公司的Hotspot虚拟机来运行的,对于不同公司不同版本的虚拟机,参数和运行结果可能会有所不同;

  1. Java堆溢出

    ​ Java堆用于存储对象,只要不断的创建对象,并且保证GC Routes到对象之间有可达路径来避免垃圾回收机制来清除这些对象,那么在对象所占内存达到最大堆的容量限制后,就会出现OOM异常;

    示例代码中,限制Java堆大小为20M,不可扩展(将堆的最小值-Xms参数与最大值-Xmx参数设为一样,可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath=F:\books,可让虚拟机出现OOM异常时,Dump出当前的堆内存转储快照,并保存在指定位置,以便于后续分析;

    示例代码中,我们设置虚拟机参数如下:

在这里插入图片描述

示例代码:

/**
 * @author Snow
 * Java堆OOM异常模拟
 */
public class HeapOomDemo {
    
    

    static class OomTest{
    
    }

    public static void main(String[] args) {
    
    
        ArrayList<OomTest> oomTests = new ArrayList<>();
        while (true){
    
    
            oomTests.add(new OomTest());
        }
    }
}

执行代码,运行结果如下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to F:\books\java_pid11132.hprof ...
Heap dump file created [28150606 bytes in 0.068 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at jvm.HeapOomDemo.main(HeapOomDemo.java:16)

由运行结果可知,发生了OOM异常,并在我们虚拟参数中配置的Dump路径下产生了dump文件:

在这里插入图片描述

要解决此异常,我们需要先通过内存印象分析工具,对Dump下来的文件进行分析,下面我们使用Java的VisualVm分析此文件:命令行执行:jvisualvm

在这里插入图片描述

执行命令后,将打开VisualVm工具,我们导入前面的文件:

在这里插入图片描述

导入分析Dump出的文件之后,可看到如下的分析结果:

在这里插入图片描述

  1. 虚拟机栈和本地方法栈溢出

    由于在Hotspot虚拟中,并不区分虚拟机栈和本地方法栈,因此对Hotspot虚拟机来说,虽然-Xoss(本地方法栈大小)参数存在,但实际上是无效的,栈容量只由-Xss参数设定。

    对于虚拟机栈和本地方法栈,Java虚拟机规范定义了两种异常:

    1. 如果线程请求的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常
    2. 如果虚拟机在扩展内存时,无法申请到足够的内存空间,则抛出OutofMemoryError异常

    下述示例中,将实验范围限制于单线程中的操作:

    1. 使用-Xss参数减少栈内存容量;结果:抛出StackOverflowError,异常出现时,输出的堆栈深度相应的缩小
    2. 定义了大量的本地变量,增大此方法帧中本地变量表的长度;结果:抛出StackOverflowError,输出的堆栈深度相应的缩小

    示例代码:

    public class StackSomDemo {
          
          
    
        static class SomTest{
          
          
            private int stackLength = 1;
    
            public void stackLeak(){
          
          
                stackLength ++;
                stackLeak();
            }
        }
    
        public static void main(String[] args) {
          
          
            SomTest somTest = new SomTest();
            try {
          
          
                somTest.stackLeak();
            }catch (Throwable e){
          
          
                System.out.println("stack length:"+somTest.stackLength);
                throw e;
            }
    
        }
    }
    

    运行示例代码,结果如下:

    stack length:982
    Exception in thread "main" java.lang.StackOverflowError
    	at jvm.StackSomDemo$SomTest.stackLeak(StackSomDemo.java:9)
    	at jvm.StackSomDemo$SomTest.stackLeak(StackSomDemo.java:10)
    	at jvm.StackSomDemo$SomTest.stackLeak(StackSomDemo.java:10)
    

    上述运行结果表明:在的单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配时,虚拟机抛出的都是StackOverflowError异常;

  2. 方法区和运行时常量池溢出

  3. 本机直接内存溢出

垃圾收集器与内存分配策略

对象已死吗?

在堆里存放着Java中几乎所有的对象实例。垃圾收集器在对对象回收前,第一件事就是判断这些对象,哪些“存活着”,哪些“死去”;

引用计数法

即给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;

任何时刻,计数器值为0的对象,就是不可能再被使用的,算作“死亡”对象。

引用计数法,实现简单,判定效率也挺高;但它很难解决对象之间循环引用的问题,所以很少在主流虚拟机中采用

示例代码:

//-XX:+PrintGCDetails,配置此选项打印GC信息
public class RefCountDemo {
    
    
    public Object instance = null;
    public static final int _1MB = 1024*1024;
    /**
     * 占用内存,便于GC日志查看是否被回收
     */
    private byte[] bigSize = new byte[2 *_1MB];

    public static void main(String[] args) {
    
    
        RefCountDemo objA = new RefCountDemo();
        RefCountDemo objB = new RefCountDemo();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        //假设这行发生GC,objA和objB能否被回收
        System.gc();
    }

}

运行上述代码,结果如下:

[GC (System.gc()) [PSYoungGen: 9359K->728K(153088K)] 9359K->736K(502784K), 0.0007083 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 728K->0K(153088K)] [ParOldGen: 8K->588K(349696K)] 736K->588K(502784K), [Metaspace: 3116K->3116K(1056768K)], 0.0030812 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 153088K, used 3947K [0x0000000715f00000, 0x0000000720980000, 0x00000007c0000000)
  eden space 131584K, 3% used [0x0000000715f00000,0x00000007162daf90,0x000000071df80000)
  from space 21504K, 0% used [0x000000071df80000,0x000000071df80000,0x000000071f480000)
  to   space 21504K, 0% used [0x000000071f480000,0x000000071f480000,0x0000000720980000)
 ParOldGen       total 349696K, used 588K [0x00000005c1c00000, 0x00000005d7180000, 0x0000000715f00000)
  object space 349696K, 0% used [0x00000005c1c00000,0x00000005c1c93208,0x00000005d7180000)
 Metaspace       used 3161K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 342K, capacity 388K, committed 512K, reserved 1048576K

从上述运行结果可以看出,GC日志中包含“9359K->728K”,说明虚拟机并没有因为上述两对象互相引用,就没有回收它们;也从侧面说明当前虚拟机并未采用引用计数法来判定对象是否存活。

可达性分析算法

在主流商用程序语言的主流实现中,都是通过可达性分析来判定对象存活的。

可达性分析算法的思路主要是:通过一系列成为“GC-Roots”的对象作为起点,从这些起点开始往下搜索,搜索所走过的路径称为“引用链”;当一个对象到GC-Roots没有任何引用链相连(用图论的话说,就是该对象到GC-Roots不可达),则判定此对象是不可使用的。

如示例图所示,对象obj5/obj6/obj7他们虽然互有关联,但他们到GC Roots是不可达的,因此判定为可回收的对象:

在这里插入图片描述

在Java语言中,可作为GC Roots的对象分为以下几种:

  1. 虚拟机栈(栈帧中局部变量表)中引用的对象
  2. 方法区中类静态属性引用的变量
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI(即一般说的Native方法)引用的对象
再谈引用

JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种,这四种引用强度逐渐减弱:

  1. 强引用(Strong Reference)

    ​ 强引用就是类似于“ Object obj = new Object() ”这种;在代码中普遍存在的对象,只要强引用还存在,就会被GC回收掉对应的对象;

  2. 软引用(Soft Reference)

    ​ 软引用用来描述一些有用但非必要的对象;在系统发生OOM异常之前,会对软引用指向对象进行二次回收,若还是没有足够内存,则抛出OOM异常

  3. 弱引用(Weak Reference)

    ​ 用来描述非必需对象的;弱引用关联的对象,只能存活到下一次垃圾收集发生之前

  4. 虚引用(Phantorn)

    ​ 也被成为幽灵引用或者幻影引用,唯一作用就是关联对象被垃圾收集时,会有一个系统通知;

生死还是死亡

即使在可达性分析算法中,不可达的对象,要真正宣告“死亡”,也至少要经历来两次标记过程:

  1. 若对象进行可达性分析后,没有GC Roots的引用链与其相连,则将会进行第一次标记,并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法:
    1. 当对象没有覆盖finalize()方法,或者finalize()方法已被虚拟机调用过,此情况都视为“没必要执行”。
    2. 若对象判定有必要执行finalize()方法,那么此对象将进入一个F-Queue的队列中,并在稍后由虚拟机建立的、低优先级的Finalizer线程取执行它;
  2. finalize()方法,是对象逃脱死亡命运的最后一次机会,稍后GC 将堆F-Queue中的对象进行二次小规模标记,若对象在finalize()方法中拯救自己——只要与引用链上的任何一个对象建立关联即可,比如把自己赋值给某个类变量或者对象的成员变量,那么第二次标记时,它将被移除出“ 即将回收 ”集合;

垃圾收集算法

标记-清除算法

标记-清除算法算是最基础的收集算法,主要分为标记、清除两个阶段:

  1. 首先标记出所有需要回收的对象
  2. 在标记完成后,统一回收所被标记的对象

之所以称为最基础,因为后续算法都是基于其思路,并针对其不足改进得到的

此算法主要不足有:

  1. 效率问题,标记和清楚两个过程的效率都不高
  2. 空间问题,标记清除后会产生大量的内存碎片;内存碎片太多,可能导致后续需要分配较大对象空间时,

无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

为了解决效率问题,一种称为“复制”的算法出现了:

​ 它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点和不足:

这样使得每次都是对每个半区进行回收,内存分配时也不用考虑内存碎片等情况,只要移动栈顶指针,按顺序分配内存即可,实现简单,运行高效。

不足是这种算法将内存缩小为了原来的一半,代价太高了点

注意:现在商业虚拟机都采用这种收集算法来回收新生代,IBM公司专门的研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden和其中一块Survivor当回收时,将Eden和Survivor中还存活的对象,一次性地复制到另一块Survivor上,最后清理掉Eden和刚才使用的Survivor

Hotspot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。

当前前面所说98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不足时,需要依赖其他内存(这里指老年代)进行分配担保。

标记-整理算法

复制收集算法在对象存活率较高时,就要进行较多的复制操作,效率将会变低。更关键的是,若不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对所使用的内存中,所有对象都100%存活的极端情况,所以在老年代一般不能使用这种算法。

根据老年代的特点,有人提出了一种“标记-整理”算法:

​ 标记过程仍然和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象,都向一端移动,然后直接清理掉端边界之外的内存

分代收集算法

当前商业虚拟机的垃圾收集,都采用“分代收集”算法,这种算法主要是根据对象存活周期的不同将内存划分为几块。一般是将Java堆划分为新生代和老年代;这样就可以根据各个年代的特点采用最适当的收集算法。

在新生代中,每次垃圾收集时,都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完后收集。

而在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或“标记-整理”算法来进行回收。

垃圾收集器

如果说垃圾回收算法是内存回收的方法论,那么收集器就是内存回收的具体实现;

Java虚拟机规范中,对如何收集器如何实现内存回收没有具体规定,因此不同厂商、不同版本虚拟机所实现的垃圾收集器可能会有很大区别,并且一般都会提供配置参数供用户根据自身特点和需要,组合出各个年代所需要的收集器。

这里将讨论的虚拟机基于JDK1.7 Update 14之后的虚拟机,此虚拟机所包含的收集器如下图所示:

在这里插入图片描述

如上图显示7中作用于不同分代的收集器,若两个收集器之间存在连线,则表示他们可以搭配使用。虚拟机所处的区域,则表示他们是处于新生代还是老年代。

在接下来介绍这些收集器之前,我们先明确一点:虽然我们是在对各个收集器进行比较,但并非是为了选出最好的收集器。因为到目前为止,还没有最好的收集器,也没有万能的收集器,所以我们选择的只是针对具体应用场景的最合适的收集器。

Serial收集器

Serial收集器是最基础的,历史最悠久的收集器,曾是JDK1.3之前新生代的唯一收集器。

从名字可看出,此收集器是单线程的,这不仅说明它会使用的单个CPU和单个线程取进行垃圾回收,而且更重要的是,它在进行垃圾回收时,必须暂停其他所有工作的线程,直到它收集结束。

"Stop the world"是由虚拟机在后台自动发起和自动完后的,它会导致一定时间内,在用户不可见的情况下,把用户工作的线程全部停掉,这对很多应用来说都是难以接受的。

下图显示了Serial/Serial Old收集的运行过程:

在这里插入图片描述

从JDK1.3开始,Hotpost虚拟机开发团队,一直致力于消除或减少工作线程因内存回收而导致的停顿,从Serial收集器到Parallel收集器,再到CMS收集器,乃至最新的G1收集器,用户线程的停顿时间不断减少,但仍没有办法完全消除(这里暂不包括RTJS中的收集器)

目前为止,Serial收集器仍是虚拟机运行在Client模式下,默认的新生代收集器;它也有着由于其他收集器的特点:简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得较高的单线程收集效率。

所以,Serial收集器对于运用在Client模式下的虚拟机是一个很好的选择

Parnew收集器

Parnew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop the world、对象分配规则、回收策略等,都与Serial收集器一样。

Parnew/Serial Old收集器的示意图如下:

在这里插入图片描述

Parnew收集器除了多线程收集外,它还是许多运行在Server模式下虚拟机首选的新生代收集器;其中一个与性能无关但很重要的原因是:目前除了Serial收集器之外,只有它能与CMS收集器配合使用。

JDK1.5时期,Hotspot收集器——第一款真正意义上的并发收集器,第一次实现了GC线程和用户线程(基本上)同时运行。但CMS作为老年代收集器,却无法与JDK1.4中已有的Parallel scavenge收集器配合运行;所以在JDK1.5中,使用CMS作为老年代收集器时,新生代只能选择Serial或Parnew收集器。

在单CPU环境中,Parnew收集器绝不会有比Serial收集器更好的效果;甚至于由于线程交互的开销,Parnew收集器在多线程实现时,在两CPU环境下,它也不保证百分百优于Serial收集器;

当然,随着CPU核数的增加,Parnew收集器对于GC资源的有效利用还是有好处的

Parallel scavenge收集器

Parallel scavenge收集器是一个新生代收集器,也是使用复制算法的收集器,又是并行的多线程收集器;看上去似乎和Parnew收集器类似,那它有什么其他特点呢?

Parallel scavenge收集器的不同在于,关注点与其他收集器不同:CMS等收集器更关注于缩短GC时,用户线程的停顿时间,而Parallel scavenge则是关注于达到一个可控的吞吐量;所谓吞吐量即:吞吐量 = 运行用户线程时间/(运行用户线程时间+运行GC线程时间)

用户线程停顿时间越短,越适合需要与用户交互的程序;而高吞吐量则可以高效的利用CPU资源,主要适合在后台运算,而不需要与用户交互的任务;

Parallel scavenge收集器提供了两个参数,用于精确控制吞吐量:

  1. -XX:MaxGCpauscMillis:最大垃圾收集停顿时间,参数值是一个大于0的毫秒数
  2. -XX:GCTimeRadio:吞吐量大小,参数值是一个大于0小于100的整数

由于与吞吐量密切相关,Parallel scavenge收集器也常被成为“吞吐量优先”收集器

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法;

此收集器的主要意义也是在于给Client模式下的虚拟机使用;

如果再Server模式下,那么它主要还有两大用途:

  1. 一种用途是在JDK1.5及以前的版本中,作为老年代搭配Parallel scavenge收集器使用;
  2. 另一用途就是,作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

Serial Old收集器的工作模式如下图所示(配合Serila使用):

在这里插入图片描述

Parallel Old收集器

Parallel Old是Parallel scavenge收集器的老年代版本,使用多线程和“标记-整理”算法;

此收集器在JDK1.6中才开始提供,在此之前,新生代的Parallel scavenge收集器,老年代只能选择Serial Old收集器配合使用,而由于Serial Old的单线程老年代收集,无法充分利用多核CPU的优势,可能有所拖累;

直到Parallel Old出现,“吞吐量优先”收集器终于有了比较名副其实的收集器组合,在注重吞吐量和CPU资源敏感的场合,都可以优先考虑Parallel scavenge/Parallel Old的组合;

Parallel scavenge/Parallel Old收集器运行示意图如下:

在这里插入图片描述

Cms收集器

CMS(Concurrent Mark Sweep)收集器,是一种以获取最短回收停顿时间为目标的收集器;

目前很大一部分Java应用,十分注重服务器的响应速度,以给带来更好的体验;CMS收集器就非常适合这种应用的需求;

从名字就可看出,CMS是使用“标记-清除”算法实现的,整个过程主要分为四个步骤:

  1. 初始标记(CMS intinal mark)
  2. 并发标记(CMS concurrent mark)
  3. 重新标记(CMS remark)
  4. 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop the world”。

初始标记,仅仅是标记下GC Roots可直接关联到的对象,速度很快;

并发标记,就是进行GC Roots Tracing的过程;

重新标记,则是修正并发标记时,由于用户线程继续运行导致标记变动的那部分记录;此阶段的停顿时间一般会比初始标记极端稍长,但远比并发标记阶段短;

由于整个过程中,耗时最长的并发标记和并发清除阶段,GC线程都和用户线程一起工作;所以,从总体上看,CMS的GC线程是和用户线程一起并发执行的;

通过下图,可较为清楚的看到CMS收集器的运作过程:

在这里插入图片描述

CMS收集器是一款优秀的收集器,其优点有:并发收集、低停顿;SUN官方也称之为“并发低停顿”收集器;

但CMS远未达到完美的地步,其主要有三个缺点:

  1. CMS收集器对CPU资源非常敏感;
  2. CMS收集器无法处理“浮动垃圾”;
  3. CMS收集采用“标记-清除”算法实现,意味着收集后,会有大量空间碎片产生;
G1收集器

G1收集器是一款面向服务端的垃圾收集器。Hotpot虚拟机赋予它的使命是未来可以替换掉JDK1.5中的CMS收集器。

与其他收集器相比,G1具有如下特点:

  1. 并行与并发:G1能充分应用多CPU、多核的优势,来缩短Stop-the-world的时间;部分其他收集器,原本需要暂停用户线程来执行GC的动作,G1仍可以在GC时,让用户线程继续执行;
  2. 分代收集:与其他收集一样,分代概念在G1中依然得以保留;
  3. 空间整合:G1从总体来看是基于“标记-整理”算法来实现收集器的,这避免了CMS采用“标记-清除”算法所导致的内存碎片;
  4. 可预测的停顿:这是G1相对于CMS的一大优势:G1除了追求低停顿外,还能建立可预测的低停顿模型;能让使用者明确指定在一个长度为M毫秒的时间内,消耗在GC上的时间不超过N毫秒,这几乎已经是实时Java(RTSJ)垃圾收集器的特征了

G1之前的收集器都是基于新生代和老年代来收集,而G1不再是这样。

G1收集器将Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代已没有物理上的隔离了,他们都是一部分Region(不需要连续)的集合。

G1收集器,之所以可以建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小及回收所需时间的经验值)。在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这就是Garbage-first的由来)。

这种使用Region划分内存空间并有优先级的回收方式,保证了G1收集器在有限时间内可以获取尽可能高的回收效率。

在G1收集器中,Region之间的引用,或其他收集器中新生代和老年代之间的对象引用,虚拟机都是通过Remember Set来避免全堆扫描的;

G1中每个Region都有一对应的Remember Set,当程序对Reference类型数据写操作时,虚拟机会先生成一个写屏障来暂时中断写操作,检查Reference指向的对象是否属于不同Region(分代收集中,就是检查老年代中对象是否引用了新生代对象),如果是,则通过CardTable 将引用信息记录到被引用对象所属的Region的Remember Set中;当进行垃圾回收时,在GC Root的枚举范围中加入Remember Set,即可保证不对全堆扫描,也不会有遗漏。

若不计算维护Remember Set的操作,G1收集器的运行,大致可分为如下操作:

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

G1收集器的运行示意图大致如下:

在这里插入图片描述

理解GC日志
0.144: [GC (System.gc()) [PSYoungGen: 9359K->696K(153088K)] 9359K->704K(502784K), 0.0180797 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
0.162: [Full GC (System.gc()) [PSYoungGen: 696K->0K(153088K)] [ParOldGen: 8K->616K(349696K)] 704K->616K(502784K), [Metaspace: 3201K->3201K(1056768K)], 0.0034062 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 153088K, used 1316K [0x0000000715f00000, 0x0000000720980000, 0x00000007c0000000)
  eden space 131584K, 1% used [0x0000000715f00000,0x00000007160490d0,0x000000071df80000)
  from space 21504K, 0% used [0x000000071df80000,0x000000071df80000,0x000000071f480000)
  to   space 21504K, 0% used [0x000000071f480000,0x000000071f480000,0x0000000720980000)
 ParOldGen       total 349696K, used 616K [0x00000005c1c00000, 0x00000005d7180000, 0x0000000715f00000)
  object space 349696K, 0% used [0x00000005c1c00000,0x00000005c1c9a328,0x00000005d7180000)
 Metaspace       used 3208K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K

每一种收集器的日志形式,都是由他们自身的实现决定的;换而言之,每个收集器的日志格式都可以不一样;

如上上述日志,可以看出:

  1. 0.144和0.162,分别代表GC发生的时间:这个数字的含义是虚拟机启动以来经过的秒数
  2. [GC和[Full GC,说明了此次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的;说明这次GC是发生了Stop-the-world的。
  3. [GC (System.gc()):如果是调用System.gc()所触发的收集,那么这样显示;
  4. [PSYoungGen:表示使用的Parallege Scavenge收集器,收集区域为新生代;
  5. 9359K->696K(153088K):方括号内部的内存变化,表示GC前该内存区域已用容量-> GC后该内存区域已用容量
  6. 9359K->704K(502784K):括号外内存变化,表示GC前Java堆已用容量->GC后Java堆已用容量
  7. [Times: user=0.00 sys=0.00, real=0.02 secs]:这里与Linux的Time命令输出含义一致,分别表示用户态消耗的CPU时间、内核态消耗的CPU时间、操作从开始到结束所经历的墙钟时间;(CPU时间与墙钟时间的区别:墙钟时间包括各种非运算的等待耗时,例如等待磁盘IO、等待线程阻塞等;而CPU时间不包括这些耗时)
垃圾收集器参数总结

下面将列举一些虚拟机垃圾收集相关常用参数:

  1. UseSerialGC:虚拟机运行在Client模式下的默认值;打开此开关,将使用Serial/Serial Old组合进行GC
  2. UseParNewGC:打开后,将使用ParNew/Serial Old组合进行GC
  3. UseConcMarkSweapGC:打开后,将使用ParNew+CMS+Serial Old组合进行GC
  4. UseParallelCG:打开后,使用Parallege Scavenge+Serial Old组合进行GC
  5. SurvivorRatio:新生代中Eden区和Survivor区域的容量比值,默认为8
  6. PretenureSizeThreshOld:直接晋升到老年代的大小
  7. MaxTenuringThreshOld:直接晋升到老年代的对象年龄
  8. UseAdaptiveSizePolicy:动态调整Java个区域的大小和进入老年代的年龄
  9. ParallelGCThreads:设置并行GC时内存回收的线程数
  10. CMSInitiatingQccupancyFraction:设置CMS收集器在老年代被占用多少时,触发GC;默认为68%;仅在CMS收集器时有效
  11. UseCMSCompactAtFullCollection:设置CMS在进行GC后是否进行一次内存碎片整理
  12. CMSFullGCBeforeFraction:设置CMS进行若干次GC后,再进行一次内存碎片整理

内存分配与回收策略

Java体系的自动内存管理最终可归结自动化地处理为两个问题:给对象分配内存和回收分配给对象的内存;

对象的内存分配,大方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配;少数情况下,也可直接分配在老年代中,具体分配细节取决于当前使用了那种垃圾收集器,也和虚拟机的配置参数有关。

接下来将讲解几条普遍的内存分配规则,并通过代码去验证这些规则

  1. 对象优先在Eden分布

    ​ 大多数情况下,对象将在新生代的Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC;

    Minor GC和Full GC有什么不同?

    1. 新生代GC(Minor GC):指发生在新生代的GC;因为Java对象大都具备朝生夕灭的特性,所以Minor GC非常频繁,回收速度也特别快
    2. 老年代GC(Major GC/Full GC):指发生在老年代的GC;出现了Major GC,通常会伴随至少一次的Minor GC(不绝对,Parallege Scavenge收集器收集策略里,就有直接进行Major GC的策略)。Major GC的速度一般会比Minor GC慢10倍以上
  2. 大对象直接进入老年代

    ​ 所谓大对象是指,需要大量连续内存的Java对象,典型的就是那种很长的字符串和数组;经常出现大对象,容易导致内存还有不少空间时,就提前触发GC来需求足够大的连续空间来“安置”此对象

  3. 长期存活的对象将进入老年代

    ​ 虚拟机会给每个对象定义一个对象年龄计数器,如果对象在Eden出生,并在经历一次Minor GC后仍然存活,并且能够被Survivor容纳的话,那将移入Survivor区,并将年龄设为1;对象在Survivor区每熬过一次“Minor GC”,对象年龄就加1岁;当它的年龄增加到一定程度(默认15岁),将晋升到老年代;

    至于晋升到老年代的年龄阈值,可通过参数-XX:MaxTenuringThreshOld 来配置

  4. 对象动态年龄判定

    ​ 为了能适应不同程序的内存情况,虚拟机并不永远要求对象年龄必须达到阈值才能晋升老年代;如果Survivor中相同年龄的所有对象大小,超过Survivor内存空间一般;那么大于等于该年龄的对象将晋升老年代;无须等到满足XX:MaxTenuringThreshOld 的阈值年龄

  5. 空间分配担保

性能监控与故障处理

JDK命令行工具

SUN JDK 监控和故障处理工具

  1. jps:JVM Process Status Tool,显示系统内所有的Hotspot虚拟机线程
  2. jstat:JVM Statistics Monitoring Tool,用于收集虚拟机各方面的运行数据
  3. jinfo:Configration Info for java,显示虚拟机配置信息
  4. jmap:Memorry Map for Java:生成虚拟机的内存转储快照
  5. jhat:JVM Heap Dump Browser:用于分析Heap Dump文件,它会创建一个Http/Html服务器,可以让用户在浏览器上查看分析结果
  6. jstack:Stack Trace for Java,显示虚拟机线程快照
jps:虚拟机进程状况工具

JVM Process Status Tool,作用也和UNIX的ps命令类似:

​ 可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main函数所在的类)名称,以及这些进程的的本地虚拟机唯一ID(Lcoal Virtual Machine Identifier,LVMID)

​ 虽然功能比较单一,但他确实使用频率最高的JDK命令行工具,因为其他工具大都依赖它查询到的LVMID,来确定具体要监控哪一个虚拟机进程

命令格式:

jps [option] [hostid]

执行示例:

C:\Users\lixuewen>jps -l
12912 -- process information unavailable
13744 com.twsz.mom.oauth.OauthApplication
16128 org.jetbrains.jps.cmdline.Launcher
17216 com.twsz.mom.mm.MaterialApplication
19620 org.apache.catalina.startup.Bootstrap
21236 sun.tools.jps.Jps
10744 com.twsz.mom.system.SystemApplication
12344 com.twsz.mom.gateway.GatewayApplication
14280
11500 com.twsz.mom.prod.ProductionApplication
15196 org.jetbrains.jps.cmdline.Launcher

jps可通过RMI协议,查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的远程主机ip

jps命令主要选项:

  1. -q:只输出LVMID,省略主类名称
  2. -m:只输出虚拟机启动时传递给主类main()函数的函数
  3. -l:输出主类的全名,如果进程执行的是Jar包,输出Jar路径
  4. -v:输出虚拟机进程启动时的虚拟机参数
jstat:虚拟机统计信息监视工具

JVM Statistics Monitoring Tool,用户监视虚拟机各种运行状态信息的工具

可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,没有GUI图形界面,只提供了纯文本信息;

命令格式:

jstat [option vmid [interval [s|ms] ] [count] ]

格式说明:

  1. 如果是本地虚拟机,则vmid和LVMID是一致的;如果是远程虚拟机,则vmid的格式应当是:

    ​ [protocol:] [//] lvmid [ @hostname [: port] /servername ]

  2. interval和count代表间隔和查询次数,若缺省,表明只查询一次;

  3. 假设查询进程10744的垃圾收集情况,每250毫秒一次,共20次,则命令为:

    ​ jstat -gc 10744 250 20

  4. option表示要查询的虚拟机信息,主要分三类:类装载、垃圾收集、运行期编译情况

    1. -class:监视类装载、卸载数量、总空间及装载所耗时间
    2. -gc:监视垃圾收集,Java堆等情况
      1. -gccapacity:主要关注Java堆各区域用到的最大、最小空间
      2. -gcutil:主要关注已用空间占总空间百分比
      3. -gccause:与上个类似,但会输出触发上次GC的原因
      4. -gcnew:监视新生代GC状况
      5. -gcold:监视老年代GC状况
      6. -gcoldcapacity:主要关注,老年代用到的最大、最小空间
      7. -gcpermcapacity:主要关注持久代用到的最大、最小空间
    3. -compiler:输出JIT编译过的方法、耗时等信息
    4. -printcompilation:输出已经被JIT编译的方法

执行示例:

C:\Users\lixuewen>jstat -gcutil 11500
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00  95.10   5.35  40.95  94.34  91.10     45    1.481     5    1.436    2.917

由执行结果可知:

​ 新生代Eden区(E)使用了5.35%的空间,两个Survivor区(S0,S1)则分别占用了各自区域的0%和95.10%,老年代(O)和永久代(M)则分别使用40.95%和94.34%的空间;

​ 程序运行以来,共发生Minor GC(YGC)45次,总耗时1.481秒,Full GC (FGC)5次,共耗时1.436秒,所有GC总耗时(GCT)2.917秒。

jstat提供了纯文本方式展示虚拟机运行信息,后面的工具Virvul VM则提供了可视化的展示页面,可能会更直观。

jinfo:Java配置信息工具

Configration Info for java,可以实时查看和调整虚拟机各项参数;

使用jps -v命令可查看虚拟机启动时,显示指定的参数列表;若想要知道未被显示指定的参数默认值,就能使用jinfo -flag来查询了。

jinfo -sysprops 可把虚拟机进程的System.getProperties()的内容打印出来;

JDK1.6之后,加入了运行期修改虚拟机参数的能力,可使用-flag [+|-] name或-flag name=value来修改一部分运行期可写的虚拟机参数。

jinfo命令格式:

jinfo [option] pid

执行示例:

C:\Users\lixuewen>jinfo -sysprops
Usage:
    jinfo [option] <pid>
        (to connect to running process)
    jinfo [option] <executable <core>
        (to connect to a core file)
    jinfo [option] [server_id@]<remote server IP or hostname>
        (to connect to remote debug server)

where <option> is one of:
    -flag <name>         to print the value of the named VM flag
    -flag [+|-]<name>    to enable or disable the named VM flag
    -flag <name>=<value> to set the named VM flag to the given value
    -flags               to print VM flags
    -sysprops            to print Java system properties
    <no option>          to print both of the above
    -h | -help           to print this help message

C:\Users\lixuewen>jinfo -flag MetaspaceSize 11500
-XX:MetaspaceSize=21807104
jmap:Java内存映像工具

Memorry Map for Java,用于生成堆转储快照(一般称为heapdump或dump文件);也可使用上述

-XX:+HeapDumpOnOutofMemoryError配置参数,在发生OOM时,自动生成dunp文件;

jmap命令不仅仅是唯一获取dump文件,它还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率和

使用哪种收集器等。

命令格式:

jmap [option] vmid

option的合法值和含义如下:

  1. -dump:生成Java堆转储快照;格式为:jmap -dump:live,format=b,file=heap.bin
  2. -finalizerinfo:显示在F-QUEUE中等待Finalizer线程执行finalise方法的对象信息
  3. -heap:显示Java堆详细信息;只在Linux/Solaris下有效
  4. -histo:显示堆中对象统计信息
  5. -permstat:以ClassLoader为统计口径显示永久代内存状态
  6. -F:当虚拟机进程对-dump命令没有响应时,可使用此命令强制生成dump快照;只在Linux/Solaris下有效

执行示例:

C:\Users\snow>jmap -dump:live,format=b,file=heap.bin 11500
Dumping heap to C:\Users\snow\heap.bin ...
Heap dump file created
jhat:虚拟机堆存储快照分析工具

JVM Heap Dump Browser,与jmap搭配使用,用来分析jmap生成的堆转储快照;

jhat内置了一个HTTP/HTML微型服务器,生成dump文件的分析结果后,可用浏览器查看;

实际使用中,一般不直接使用jhat分析dump文件,主要原因有二:

  1. 一般不会在应用服务器上进行dump分析,因为分析是一个耗时且占用硬件资源的过程,尽量将dump文件在其他机器上分析,既然在其他机器上分析,就不必要收到命令行工具限制了
  2. jhat的分析功能比较简陋,后文将介绍到Virual VM,以及其他专业分析工具,将实现更强大更专业的分析功能

执行示例:

C:\Users\lixuewen>jhat heap.bin
Reading from heap.bin...
Dump file created Wed Dec 21 22:24:20 CST 2022
Snapshot read, resolving...
Resolving 9370112 objects...
Chasing references, expect 1874 dots..............................................................................................................................
Eliminating duplicate references..........................................................................................................................................................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

屏幕出现“Server is ready”之后,在浏览器输入“http://localhost:7000/”,就可以看到分析结果:

在这里插入图片描述

jstack:Java堆栈跟踪工具

Stack Trace for Java,用于生成虚拟机当前时刻的线程快照(一般称为threaddump文件或javacore文件);

线程快照就是虚拟机当前时刻每一条线程正在执行的方法的堆栈集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环或请求外部接口时的长时间等待都是常见原因;

命令格式:

jstack [option] vmid

option的合法值和含义如下:

  1. -F:当正常输出的请求不被响应时,强制输出堆栈
  2. -l:除了输出堆栈外,显示关于锁的附件信息
  3. -m:如果调用本地方法的话,可显示C/C++的堆栈

执行示例:

C:\Users\snow>jstack -l 11500
2022-12-22 10:39:35
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.201-b09 mixed mode):

"Keep-Alive-Timer" #957 daemon prio=8 os_prio=1 tid=0x000000002f9d7800 nid=0x4f88 waiting on condition [0x000000007a37f000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(Native Method)
        at sun.net.www.http.KeepAliveCache.run(KeepAliveCache.java:172)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

"logback-2" #802 daemon prio=5 os_prio=0 tid=0x000000002f9e2800 nid=0x22b8 waiting on condition [0x000000006a67f000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000005d8c9c218> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
        - None

"com.alibaba.nacos.client.Worker.longPolling.fixed-10.38.102.16_8848-dev" #625 daemon prio=5 os_prio=0 tid=0x000000002f9d6000 nid=0x3d88 runnable [0x0000000057f1d000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
        at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)
        at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
        - locked <0x0000000736e4c250> (a java.io.BufferedInputStream)
        at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:735)
        at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:678)
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1587)
        - locked <0x0000000736e431a0> (a sun.net.www.protocol.http.HttpURLConnection)
        at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)

在JDK1.5中,Thread类新增了一个getAllStrackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象;使用此方法可通过简单的几行代码就实现jatack的大部分功能;实际中可考虑做个管理员页面,随时可通过浏览器查看Java堆栈信息

hsdis:jit生成代码反汇编

HSDIS是SUN官方提供的Hotpot虚拟机JIT编译代码的反汇编插件;简单来说就是一款反编译工具;

JDK可视化工具

Jconsole是JDK1.5时期,就已经提供的虚拟机监控工具;VisualVM则是在JDK1.6 Update 7中发布,是SUN主推的多合一故障处理工具;

jconsole:Java监视与管理控制台

Jconsole是一种基于JMX的可视化监视、管理工具;

  1. 启动jconsole

    ​ 通过JDK/bin目录下的“jconsole.exe”启动jconsole后,将自动搜索出本机运行的所有虚拟机进程,不需要用户自己使用jps来查询了,如下图所示;

在这里插入图片描述

双击选中一个进程,即可开始监控;也可以使用下面的“远程进程”,来连接远程服务器,监控远程的虚拟机进程

在这里插入图片描述

双击特定线程后,进入jconsole主界面,主界面共包括概述、内存、线程、类、VM概要、MBean;

“概述”显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用量”、“线程”、“类”、“CPU占用率”4种信息的曲线图,这些图是后面内存、线程、类页签信息的汇总;

  1. 内存监控

    “内存”页签,相当于可视化的jstat命令,用于监视受虚拟机管理的内存(Java堆和永久代)的变化趋势;

在这里插入图片描述

  1. 线程监控

    “线程”页签,相当于可视化的jstack命令,遇到线程停顿时,可使用该页签进行监控分析;

在这里插入图片描述

visualvm:多合一可视化故障处理工具

VisualVM是到目前为止,随JDK发布的功能最强大的运行监视和故障处理工具;

官方在VisualVM的说明中描述为“All in one”,表明它除了运行监视、故障处理外,还提供了很多其他的功能,如性能分析等;

VisualVM还有一个很大优点是:不需要被监视的程序基于特殊的Agent来运行,因此它对应用程序的实际性能影响很小,使得它可以直接别应用于生产环境中。

  1. VisualVM兼容范围与插件安装

    ​ VisualVM基于NetBeans平台开发,因此它一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM可以做到:

    1. 显示虚拟机进程及进程的配置、环境信息(jps,jinfo)

    2. 监视应用程序的CPU、GC、堆、方法区和线程信息(jstat,jstack)

    3. dump及分析堆转储快照(jmap,jhat)

    4. 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法

    5. 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照,可以将BUG发送给开发者处进行BUG反馈

    6. 其他plugins提供的无线可能

    ​ 插件可以进行手工安装,在相关网站现在*.nbm包后,依次点击“工具”——>“插件”——>“已下载”,在打开的窗口中指定nbm包路径,即可安装;

    ​ 不过手动安装并不常见,使用VisualVM的自动安装功能,便可找到所需的大部分插件,在有网络的环境下,点击“工具”——>“插件”——>“可用插件”,在弹出的插件列表中,可根据需要安装:

在这里插入图片描述

​ 安装完插件后,在左侧应用程序中,选择一个需要监控的程序,即可进入该程序主页面了:

在这里插入图片描述

VisualVM右侧页签概述、监视、线程、GC等与上述Jconsole介绍类似,可自行体验;

  1. 生成、浏览堆转储快照

    在VisualVM中生成dump堆转储快照,有两种方式

    1. 在“应用程序”窗口中,右键单击指定程序,然后选择“堆Dump”

    2. 在“应用程序”窗口中,双击指定程序,在程序主页面选中“监视”页签,点击“堆Dump”

    生成了dump文件之后,在“应用程序”窗口,该程序下将增加一个[heapdump]的子节点,并在右侧主界面中打开了该dump文件;

在这里插入图片描述

若要把dump文件发送或保存,可在此节点上点击选择另存为,否则VisualVM关闭时,该dump文件将丢失;

若要打开一个外部已存在的dump文件,可在文件菜单上选择“装入”功能,导入外部dump文件;

  1. 分析程序性能

    在Profiler页签中,VisualVM提供了程序运行期间,方法级的CPU分析和内存分析;做Profiling分析肯定会对程序性能有比较大的影响,所以一般不在生产环境使用这项功能

    要开始分析,先选择“CPU”和“内存”按钮中的一个,然后切换到应用程序窗口,选中特定程序,VisualVM会记录这段时间CPU调用过的方法:

    1. 若是CPU分析,将会统计每个方法的执行次数,执行耗时
    2. 若是内存分析,则会统计每个方法关联的对象数,及这些对象所占用的内存空间
    3. 分析结束后,点击“停止”,可结束监控
  2. BTrace动态日志跟踪

    BTrace是一个“有趣”的VisualVM插件,其本身也可以独立运行;

    作用是可在不停止目标程序运行前提下,通过Hotspot虚拟机Hotswap技术动态加入原本不存在的调试代码

    这项功能,对实际生产中的应用非常有意义:在遇到程序出现问题,需要排查关键代码的执行结果信息,但开发时有没加日志的情况,则不得不停掉服务,已增加日志来排查问题;遇到生产环境无法随意停止,导致排查无法进行,将会非常恼人。

    安装BTrace插件后,在应用程序窗口中,右键单击指定程序,选择“Trace Application……”,将进入BTrace面板;

    BTrace的用法还有很多,打印调用堆栈、参数、方法返回值只是基本的应用,在它的网站上有许多用来进行性能监视、定位连接泄露、内存泄露、解决多线程竞争等问题的例子,有兴趣的可去其相关网站查看;

    BTrace使用参数链接:https://www.jianshu.com/p/93e94b724476
    击指定程序,在程序主页面选中“监视”页签,点击“堆Dump”

    生成了dump文件之后,在“应用程序”窗口,该程序下将增加一个[heapdump]的子节点,并在右侧主界面中打开了该dump文件;

    [外链图片转存中…(img-KdwF4SnW-1678267817768)]

    若要把dump文件发送或保存,可在此节点上点击选择另存为,否则VisualVM关闭时,该dump文件将丢失;

    若要打开一个外部已存在的dump文件,可在文件菜单上选择“装入”功能,导入外部dump文件;

  3. 分析程序性能

    在Profiler页签中,VisualVM提供了程序运行期间,方法级的CPU分析和内存分析;做Profiling分析肯定会对程序性能有比较大的影响,所以一般不在生产环境使用这项功能

    要开始分析,先选择“CPU”和“内存”按钮中的一个,然后切换到应用程序窗口,选中特定程序,VisualVM会记录这段时间CPU调用过的方法:

    1. 若是CPU分析,将会统计每个方法的执行次数,执行耗时
    2. 若是内存分析,则会统计每个方法关联的对象数,及这些对象所占用的内存空间
    3. 分析结束后,点击“停止”,可结束监控
  4. BTrace动态日志跟踪

    BTrace是一个“有趣”的VisualVM插件,其本身也可以独立运行;

    作用是可在不停止目标程序运行前提下,通过Hotspot虚拟机Hotswap技术动态加入原本不存在的调试代码

    这项功能,对实际生产中的应用非常有意义:在遇到程序出现问题,需要排查关键代码的执行结果信息,但开发时有没加日志的情况,则不得不停掉服务,已增加日志来排查问题;遇到生产环境无法随意停止,导致排查无法进行,将会非常恼人。

    安装BTrace插件后,在应用程序窗口中,右键单击指定程序,选择“Trace Application……”,将进入BTrace面板;

    BTrace的用法还有很多,打印调用堆栈、参数、方法返回值只是基本的应用,在它的网站上有许多用来进行性能监视、定位连接泄露、内存泄露、解决多线程竞争等问题的例子,有兴趣的可去其相关网站查看;

    BTrace使用参数链接:https://www.jianshu.com/p/93e94b724476

猜你喜欢

转载自blog.csdn.net/weixin_40709965/article/details/129407977