JVM, GC, OOM, 性能监控

JVM内存结构

1.启动类加载器:这个类加载器负责放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。

2.扩展类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现。它负责<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。用户可以直接使用。

3.应用程序类加载器:这个类由sun.misc.Launcher$AppClassLoader实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。

4.自定义加载器:用户自己定义的类加载器。

JVM在加载类时默认采用的是双亲委派机制//TODO//。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

  • 程序计数器(Program Counter Register):是线程私有的一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。 
  • Java虚拟机栈(Java Virtual Machine Stack):也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息//TODO//。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出StackOverflowError异常;如果Java虚拟机栈容量可以动态扩展[2],当栈扩 展时无法申请到足够的内存会抛出OutOfMemoryError异常。
  • 本地方法栈(Native Method Stacks):与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机 栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失 败时分别抛出StackOverflowError和OutOfMemoryError异常。
  • Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。当前主流的Java虚拟机都是按照可扩 展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
  • 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把 方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区 分开来。这区域的内存回 收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError异常。
  • 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。当常量池无法再申请到内存 时会抛出OutOfMemoryError异常。
  • 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。JDK1.4 引入NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。JDK1.8将永久代替换为元空间(metaspace)使用直接内存。

GC作用域

垃圾收集算法 

在Java堆划分出不同的区域之后,垃圾收集器才可以每次只回收其中某一个或者某些部分的区域 ——因而才有了“Minor GC”“Major GC”“FullGC”这样的回收类型的划分;也才能够针对不同的区域安 排与里面存储对象存亡特征相匹配的垃圾收集算法——因而发展出了“标记-复制算法”“标记-清除算法”“标记-整理算法”等针对性的垃圾收集算法。

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为: 
■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。 
■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。 
■混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。 
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

1. 标记清除

最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法,首先标记出所有需要回 收的对象,在标记完成后,统一回收掉所有被标记的对象。它的主要缺点有两个:第一个是执行效率不稳定,如果Java堆中包含大量对 象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低;第二个是内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找 到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2. 标记-复制算法

为了解决标记-清除算法面对大量可回收对象时执行效率低 的问题,一种称为“半区复制”(Semispace Copying)的垃圾收集算法,它将可用 内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样实现简单,运行高效,不过其缺陷 也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点。

 3. 标记-整理算法

标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果 不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存 活的极端情况,所以在老年代一般不能直接选用这种算法。针对老年代对象的存亡特征,提出了另外一种有针对性的“标记-整 理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

 JVM+GC

1、JVM垃圾回收的时候如何确定垃圾?是否知道什么是 GC Roots

什么是垃圾?简单的说就是内存中已经不再被使用到的空间就是垃圾

要进行垃圾回收,如何判断一个对象是否可以被回收?

  • 引用计数法

Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行
因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,
每当有一个地方引用它,计数器值加1
每当有一个引用失效时,计数器值减1。
任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象
那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题

  • 枚举根节点做可达性分析(根搜索路径)

Java 可以做GCRoots的对象

  1.  虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中的类静态属性引用的对象。
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI( Native方法)引用的对象
/**
 * @author Shawn
 * @date 2020/7/5 0:38
 * @title 在Java中,可以作为GC Roots的对象有:
 *      1. 虚拟机栈(栈帧中的本地变量表)中引用的对象    如:demo1 引用 new GCRootDemo()
 *      2. 方法区中的类静态属性引用的对象  如:demo2 引用 new GCRootDemo()
 *      3. 方法区中常量引用的对象  如:demo3 引用 new GCRootDemo()
 *      4. 本地方法栈JNI(Native方法)中引用的对象
 *
 *
 */
public class GCRootDemo {
    private byte[] bytes = new byte[100*1024*1024];
    private static GCRootDemo demo2 = new GCRootDemo();
    private static final GCRootDemo demo3 = new GCRootDemo();

    public static void main(String[] args) {
        m1();
    }

    private static void m1() {
        GCRootDemo demo1 = new GCRootDemo();
        System.gc();
        System.out.println("第一次GC完成");
    }
}

2、你说你做过JVM调优和参数配置,请问如何盘点查看MM系统默认值

  • JVM的参数类型

标配参数(-verison,-help,java -showversion)

X参数(了解,-Xint解释执行,-Xcomp第一次使用就编译成本地代码,-Xmixed混合模式)

XX参数

Boolean类型:
    公式:-XX:+或者-  某个属性值(+表示开启,-表示关闭)

    例子:
        是否打印GC收集细节-XX:+PrintGCDetails或-XX:-PrintGCDetails
        是否使用串行垃圾收集器-XX:-UseSerialGC或-XX:+UseSerialGC


KV设值类型:
    公式:-XX:属性key=属性值value

    例子:
        设置元空间大小:-XX:MetaspaceSize=128m
        设置年轻代向老年代转换的存活次数:-XX:MaxTenuringThreshold=15

jinfo举例,如何查看当前运行程序的配置
    公式:jinfo -flag 配置项 进程编号
    

两个经典参数:-Xms和-Xmx
    -Xms 等价于 -XX:InitialHeapSize
    -Xmx 等价于 -XX:MaxHeapSize

 查看JVM默认值

-XX:+PrintFlagsInitial
    查看初始默认值
    公式:java -XX:+PrintFlagsInitial -version
          java -XX:+PrintFlagsInitial

-XX:+PrintFlagsFinal
    主要查看修改更新
    公式:java -XX:+PirntFlagsFinal
         java -XX:+PirntFlagsFinal -version
    

PrintFlagsFinal举例,运行Java命令的同时打印出参数

 -XX:+PrintCommandLineFlags

启动后,打印出一下内容:

-XX:InitialHeapSize=133404352

-XX:MaxHeapSize=2134469632

-XX:+PrintCommandLineFlags

-XX:+UseCompressedClassPointers

-XX:+UseCompressedOops

-XX:-UseLargePagesIndividualAllocation

-XX:+UseParallelGC 
++++++++++hello GC+++++++++++

 3、你平时工作用过的JVM常用基本配置参数有哪些?

常用参数

-Xms初始大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize

-Xmx最大分配内存,默认为物理内存1/4,等价于-XX:MaxHeapSize

-Xss设置单个线程的大小,0代表系统出场默认值(和OS有关),一般默认设置为512K~1024K,等价于-XX:ThreadStackSize

-Xmn设置年轻代大小(一般不调整)

-XX:MetaspaceSize设置元空间大小,默认为21M,-Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal

典型设置案例:

-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC

-Xms1024m -Xmx1024m -Xss512k -XX:+UseG1GC -XX:MaxGCPauseMillis=150 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintClassHistogram -XX:+PrintAdaptiveSizePolicy -XX:+ -XX:+PrintCommandLineFlags

-XX:+PrintGCDetails输出详细GC收集日志信息 

[GC (Allocation Failure) [PSYoungGen: 1601K->504K(2560K)] 1601K->652K(9728K), 0.0037567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 504K->504K(2560K)] 652K->652K(9728K), 0.0031953 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 148K->599K(7168K)] 652K->599K(9728K), [Metaspace: 3164K->3164K(1056768K)], 0.0110456 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 599K->599K(9728K), 0.0009283 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 599K->581K(7168K)] 599K->581K(9728K), [Metaspace: 3164K->3164K(1056768K)], 0.0099542 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 122K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1e8b8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 581K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 8% used [0x00000000ff600000,0x00000000ff6915b8,0x00000000ffd00000)
 Metaspace       used 3224K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.okgo.interview.jvm.SetMemoryDemo.main(SetMemoryDemo.java:22)

-XX:SurvivoRatio

-XX:NewRatio

-XX:MaxTenuringThreshold设置垃圾最大年龄,可设置范围为0-15

4、强引用、软引用、弱引用、虚引用分别是什么? 

  • 强引用Reference:

/**
 * @author Shawn
 * @date 2020/8/8 10:22
 * @title Function
 */
public class StrongReferenceDemo {
    public static void main(String[] args) {
        Object obj1 = new Object(); // 这样的引用,默认为强引用
        Object obj2 = obj1; // obj2引用复制
        obj1 = null; // 赋值
        System.gc();
        System.out.println(obj2); // java.lang.Object@1b6d3586
    }
}
  • 软引用SoftReference(maybatis缓存的内部类就用的软引用):

package com.okgo.interview.reference;

import java.lang.ref.SoftReference;

/**
 * @author Shawn
 * @date 2020/8/8 10:29
 * @title Function
 */
public class SoftReferenceDemo {

    /**
     * 内存够用就保留,不够用就回收
     */
    public static void softRef_Memory_Enough(){
        Object o1 = new Object();
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        System.gc();
        System.out.println(o1);
        System.out.println(softReference.get());
    }

    /**
     * JVM配置,故意产生大对象并配置小内存,让它内存不够用导致产生OOM,看软引用的回收情况
     * -Xms5m -Xmx5m -XX:+PrintGCDetails
     */
    public static void softRef_Memory_NotEnough(){
        Object o1 = new Object();
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        System.gc();
        try {
            byte[] bytes = new byte[30*1024*1024];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(o1);
            System.out.println(softReference.get());
        }
    }

    public static void main(String[] args) {
//        softRef_Memory_Enough();
        softRef_Memory_NotEnough();
    }
}
  • 弱引用WeakReference:

/**
 * @author Shawn
 * @date 2020/8/8 10:47
 * @title Function
 */
public class WeakReferenceDemo {

    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference<Object> softReference = new WeakReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());

        o1 = null;
        System.gc();
        System.out.println(o1);
        System.out.println(softReference.get());
        
//        java.lang.Object@1b6d3586
//        java.lang.Object@1b6d3586
//        null
//        null

    }
}

软引用和弱应用的适用场景

你知道弱引用的话,能谈谈WeakHashMap吗?

  • 虚引用 PhantomReference:

  • GCRoots和四大引用的小总结

5、请谈谈你对ooM的认识

  • Java.lang.StackOverflowError
/**
 * @author Shawn
 * @date 2020/8/8 14:42
 * @title Function
 */
public class StackOverFlowErrorDemo {
    public static void main(String[] args) {
        stackOverFlowError();
    }

    private static void stackOverFlowError() {
        stackOverFlowError(); // Exception in thread "main" java.lang.StackOverflowError
    }
}
  •  Java.lang.OutOfMemoryError:Java heap space
/**
 * @author Shawn
 * @date 2020/8/8 14:49
 * @title Function
 */
public class JavaHeapSpaceDemo {
    public static void main(String[] args) {
//        byte[] bytes = new byte[40 * 1024 * 1024];
        String str = "test";
        while (true){
            str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999);
            str.intern();
        }
        // Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    }
}
  • Java.lang.OutOfMemeoryError:GC overhead limit exceeded

程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高

/**
 * @author Shawn
 * @date 2020/8/8 14:58
 * @title
 * JVM参数配置:
 *      -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 *      GC回收时间过长会抛出OOM。过长是指:超过98%的时间都用来做GC,并且回收不到2%的内存,连续多次GC都只回收不到2%的极端情况下会抛出这种错误。
 * 假设不抛出GC Overhead limit 错误会发生什么情况呢?
 *      GC清理的少量内存很快被再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直都是100%,而GC没有任何成果。
 *
 * [Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7096K->7096K(7168K)] 9144K->9144K(9728K), [Metaspace: 3297K->3297K(1056768K)], 0.0478792 secs] [Times: user=0.08 sys=0.00, real=0.05 secs]
 * [Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7117K->611K(7168K)] 9165K->611K(9728K), [Metaspace: 3327K->3327K(1056768K)], 0.0145574 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
 * Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
 */
public class GCOverheadDemo {
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();

        try {
            while (true) {
                list.add(String.valueOf(++i).intern());
            }
        } catch (Exception e) {
            System.out.println("++++++++++++++++i: "+i);
            e.printStackTrace();
            throw e;
        }

    }
}
  •  Java.lang.OutOfMemeoryError:Direct buffer memory

  •  Java.lang.OutOfMemeoryError:unable to create new native thread

 

 非root用户登录Linux系统进行测试

服务器级别参数调优

  • Java.lang.OutOfMemeoryError:Metaspace

/**
 * @author Shawn
 * @date 2020/8/8 15:54
 * @title
 * JVM调参
 *      -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
 */
public class MetaspaceOOMDemo {
    static class OOMTest{}

    public static void main(String[] args) {
        int i = 0;
        try {
            while (true){
                i++;
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMTest.class);
                enhancer.setUseCache(false);
                enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o,args));
                enhancer.create();
            }
        } catch (Exception e) {
            System.out.println("===========发生异常i: "+i);
            e.printStackTrace();
        }
    }
}

6、GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈

GC算法(引用计数/复制/标清/标整)是内存回收的方法论,垃圾收集器就是算法落地实现

因为目前为止还没有完美的收集器出现,更加没有万能的收集器,只是针对具体应用最合适的收集器,进行分代收集

4种主要垃圾收集器:

  • 串行垃圾回收器(Serial):它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境。
  • 并行垃圾回收器(Parallel):多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景。
  • 并发垃圾回收器(Concurrent Mark Sweep (CMS) Collector,CMS):用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用于对响应时间有要求的场景。
  • G1垃圾回收器(Garbage-First Garbage Collector):G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收

7、怎么查看服务器默认的垃圾收集器是那个?生产上如何配置垃圾收集器的?谈谈你对垃圾收集器的理解

  • 怎么查看默认的垃圾收集器是哪个?

  • 默认的垃圾收集器有哪些

  • 垃圾收集器

  • 部分参数预先说明

DefNew--Default New Generation

Tenured--Old

ParNew--Parallel New Generation

PSYoungGen--Parallel Scavenge

ParOldGen--Parallel Old Generation

  • Server/Client模式分别是什么意思

  •  新生代

串行GC(Serial)/(Serial Coping)

 

并行GC(ParNew)

 

并行回收GC(Parallel)/(Parallel Scavenge)

 

  • 老年代

串行回收GC(Serial Old)/(Serial MSC)

并行GC(Parallel Old)/(Parallel MSC)

并发标记清除GC(CMS)

 

 CMS4步过程,初始标记、重新标记这两个步骤仍然需要“Stop The World”。:

1. 初始标记(CMS initial mark)

2. 并发标记(CMS concurrent mark)和用户线程一起

3. 重新标记(CMS remark)

4. 并发清除(CMS concurrent sweep)和用户线程一起 

4步概述

CMS 优缺点:

优:并发收集低停顿

缺:

1. 首先,CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏 感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计 算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量 +3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的 处理器运算资源,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能 力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。

2. 然后,由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。在CMS的并发标记和并发清理阶 段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分 垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集 时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运 行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果 在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值 来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK 6时,CMS收集器的启动 阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满 足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不 得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致 大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。

3. 最后,CMS是一款基于“标记-清除”算法实现的收集器,就可能想到这意味着收集结束时会有大量空间碎片产生。空间 碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找 到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。为了解决这个问题, CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从 JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个 内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解 决了,但停顿时间又会变长,因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore- Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量 由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表 示每次进入Full GC时都进行碎片整理)。

  • 垃圾收集器配置代码总结

底层代码

如何选择垃圾收集器

G1

开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

G1是一款主要面向服务端应用的垃圾收集器。JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则 沦落至被声明为不推荐使用(Deprecate)的收集器。可预测的停顿时间模型的收集器能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过M毫秒这样的目标。

那具体要怎么做才能实现这个目标呢?首先要有一个思想上的改变,在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老 年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理 论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以
根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区 域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作 为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免 在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃 圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。 这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获 取尽可能高的收集效率。

 

  • 底层原理

Region区域化垃圾收集器

 

 最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可

回收步骤:

G1收集器下的YoungGC

4步过程

G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。

case案例

和CMS相比的优势

  • G1收集器要比其他的传统垃 圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额 外内存来维持收集器工作。
  • G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 G1 中维护记忆集的成本较高,带来了更高的执行负载,影响效率。
  • CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB。
  • 回收阶段(Evacuation)其实本也有想过设计成与用户程序 一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控 制的,所以并不迫切去实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC) 中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保 证吞吐量所以才选择了完全暂停用户线程的实现方案。

G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作 为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃 圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获 取尽可能高的收集效率。

设置 的“期望值”必须是符合实际的,不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。它默认的停顿目标为两百毫秒,一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速 度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获 得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常 把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

Region里面存在的跨Region引用对象如何解决?解决的思 路我们已经知道(见3.3.1节和3.4.4节):使用记忆集避免全堆作为GC Roots扫描,但在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。

在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?CMS收集器采用增量更新算法实现,而G1 收集器则是通过原始快照(SATB)算法来实现的。与CMS中 的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度, G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。

常用配置参数(了解)

  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=n : 设置G1区域的大小。值是2的幂,范围是1M到32M。目标是根据最小的Java堆大小划分出约2048个区域
  • -XX:MaxGCPauseMillis=n : 最大停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿时间小于这个时间
  • -XX:InitiatingHeapOccupancyPercent=n  堆占用了多少的时候就触发GC,默认是45
  • -XX:ConcGCThreads=n  并发GC使用的线程数
  • -XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%

 9、生产环境服务器变慢,诊断思路和性能评估谈谈?

  • 整机:top

uptime,系统性能命令的精简版

  • CPU:vmstat

 查看CPU(包含不限于)

 

查看额外

查看所有CPU核信息:mpstat -P ALL 2

每个进程使用cpu的用量分解信息:pidstat -u 1 -p 进程编号

  • 内存:free

应用程序可用内存数

查看额外

 pidstat -p 进程号 -r 采样间隔秒数

  • 硬盘:df

查看磁盘剩余空闲数

  • 磁盘IO:iostat

查看额外

pidstat -d 采样间隔秒数 -p 进程号

  • 网络IO:ifstat

默认本地没有,下载ifstat

查看网络IO 

10、假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位 

结合Linux和JDK命令一块分析

案例步骤:

1.  先用top命令找出CPU占比最高的

2.  ps -ef或者jps进一步定位,得知是一个怎么样的一个后台程序 

3.  定位到具体线程或者代码: ps -mp 进程 -o THREAD,tid,time

参数解释:

-m 显示所有线程

-p pid进程使用cpu的时间

-o 该参数后是用户自定义格式

4.  将需要的线程ID转换为16进制格式(英文小写格式)

printf "%x\n"  有问题的线程ID

5.  jstack 进程ID | grep tid(16进制线程ID小写英文) -A60

11、对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的?

是什么

 性能监控工具

  • jps(虚拟机进程状况工具)
  • jinfo(Java配置信息工具)
  • jmap(内存映像工具)
  • jstat(统计信息监控工具)

12、Jconsole, Jvisualvm

安装插件Visual GC

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot中,可以使用多种方式进行内存溢出(OOM监控。下面是其中两种常见的方式: 1. 使用Java虚拟机(JVM)参数进行监控:在启动Spring Boot应用程序时,可以通过设置JVM参数来监控内存使用情况。例如,可以设置-Xmx参数来限制应用程序的最大堆内存大小,并设置-XX:+HeapDumpOnOutOfMemoryError参数来在应用程序发生OOM时生成堆转储文件。使用这些参数可以让我们在应用程序发生OOM时,能够获取到堆转储文件进行分析,了解内存溢出的原因和位置,并进行相关的调优和修复。 2. 使用监控工具进行实时监控:除了使用JVM参数进行静态监控之外,还可以使用各种监控工具进行实时监控。例如,可以使用JConsole、VisualVM、Grafana等监控工具来监控Spring Boot应用程序的内存使用情况。这些工具可以提供实时的内存使用量、GC活动、线程情况等信息,帮助我们及时发现内存溢出的问题。 无论使用哪种方式进行监控,我们需要关注以下几个方面: 1. 内存使用量:监控应用程序使用的堆内存、非堆内存以及总内存的使用情况。通过监控内存使用量,可以及时发现内存溢出的可能性。 2. GC活动:监控GC(垃圾回收)活动,了解GC的次数、持续时间等信息。如果频繁进行GC,可能会导致应用程序的性能下降,甚至发生OOM。 3. 线程情况:监控应用程序的线程情况,包括线程数、线程状态以及可能的死锁情况。线程过多或者出现死锁等问题,也可能导致内存溢出。 总之,在Spring Boot中,通过合理设置JVM参数和使用监控工具,我们可以及时发现内存溢出的问题,并进行相应的优化和修复。这样可以提高应用程序的性能和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值