深入JVM学习

一、JVM体系概述:

Java8以后的JVM

二、常见的垃圾回收算法

1.引用计数

2.复制

Java堆从GC的角度还可以细分为: 新生代(Eden 区、From Survivor 区和To Survivor 区)和老年代。

MinorGC的过程(复制->清空->互换):
a. Eden、SurvivorFrom复制到SurvivorTo,年龄+1
首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom区,当Eden区再次触发GC的时候会扫描Eden区和From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果有对象的年龄已经达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1。

b. 清空eden-SurvivorErom
然后,清空Eden和Survivor From中的对象,也即复制之后有交换,谁空谁是To。

c. Survivor To和 Survivor From互换
最后,Survivor To和Survivor From互换,原SurvivorTo成为下一次GC时的Survivor From区。部分对象会在From和To区域中复制来复制去,如此交换15次(由ⅣM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。

3.标记清除

算法分成标记和清除两个阶段,先标记出要回收的对象,然后统一回收这些对象。

 

4.标记整理

三、谈谈你对GCRoots的理解

什么是垃圾?

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

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

  1. 引用计数法
  2. 枚举根节点做可达性分析(根搜索路径)

引用计数法:

  Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。

因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器,

每当有一个地方引用它,计数器值加1,

每当有一个引用失效时,计数器值减1。

任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。

那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。

该算法存在但目前无人用了,解决不掉循环引用的问题,了解即可。

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

  为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法

 

所谓“GC roots”或者说tracing GC的“根集合”就是一组必须活跃的引用。

基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。

Java中可以作为GC Roots的对象:

  1. 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
  2. 方法区中的类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI(Native方法)引用的对象。

四、JVM参数

JVM的参数类型:

  • 标配参数(从娘胎里面出来就带着的)

    • -version java -version
    • -help
  • X参数(了解)

    • -Xint:解释执行
    • -Xcomp:第一次使用就编译成本地代码
    • -Xmixed:混合模式
  • XX参数(下一节)

-XX参数之Boolean类型:

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

如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?

  jps -l 查看一个正在运行中的java程序,得到Java程序号。
  jinfo -flag PrintGCDetails (Java程序号 )查看它的某个jvm参数(如PrintGCDetails )是否开启。
  jinfo -flags (Java程序号 )查看它的所有jvm参数
Case

是否打印GC收集细节

  -XX:-PrintGCDetails
  -XX:+PrintGCDetails
是否使用串行垃圾回收器

  -XX:-UseSerialGC
  -XX:+UserSerialGC

JVM的XX参数之设值类型

公式:-XX:属性key=属性值value

Case

-XX:MetaspaceSize=128m
-XX:MaxTenuringThreshold=15

JVM的XX参数之XmsXmx坑题

两个经典参数:

-Xms等价于-XX:InitialHeapSize,初始大小内存,默认物理内存1/64
-Xmx等价于-XX:MaxHeapSize,最大分配内存,默认为物理内存1/4

JVM盘点家底查看初始默认值

查看初始默认参数值

-XX:+PrintFlagsInitial

公式:java -XX:+PrintFlagsInitial

查看修改过的jvm参数值 

-XX:PringFlagsFinal

公式:java -XX:+PrintFlagsFinal

查看修改后的变更值

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

java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m Test

打印命令行参数

-XX:+PrintCommandLineFlags

五、JVM常用参数详解

堆内存初始大小快速复习
JDK 1.8之后将最初的永久代取消了,由元空间取代。

 

在Java8中,永久代已经被移除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。

元空间(Java8)与永久代(Java7)之间最大的区别在于:永久带使用的JVM的堆内存,但是Java8以后的元空间并不在虚拟机中而是使用本机物理内存。

因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

public class Test01_Xms_Xmx {
    public static void main(String[] args) throws InterruptedException {
        // 返回Java虚拟机中内存的总量
        long totalMemory = Runtime.getRuntime().totalMemory();

        // 返回Java虚拟机中试图使用的最大内存量
        long maxMemory = Runtime.getRuntime().maxMemory();

        System.out.println(String.format("TOTAL_MEMORY(-Xms): %d B, %.2f MB.", totalMemory, totalMemory / 1024.0 / 1024));
        System.out.println(String.format("MAX_MEMORY(-Xmx): %d B, %.2f MB.", maxMemory, maxMemory / 1024.0 / 1024));
    }
}

输出结果:

TOTAL_MEMORY(-Xms): 126877696 B, 121.00 MB.
MAX_MEMORY(-Xmx): 1873805312 B, 1787.00 MB.

-Xms:初始化堆内存大小  

初始化大小内存,默认为物理内存的1/64

等价于:-XX:InitalHeapSize

-Xmx:最大堆内存大小

-Xmx:最大分配内存,默认为物理内存1/4

等价于-XX:MaxHeapSize

-Xss:设置单个线程栈的大小

-Xss:设置单个线程栈的大小,默认为512k~1024k

等价于:-XX:ThreadStackSize

官网文档:

启动测试设置线程堆栈大小:

 -Xmn:设置新生代堆区初始大小(很少需要改动)

官网:

 

 

MetaspaceSize讲解

-Xmn:设置年轻代大小

-XX:MetaspaceSize 设置元空间大小

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制

典型设置案例

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

常用基础参数PrintGCDetails回收前后对比讲解

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

设置参数 -Xms10m -Xmx10m -XX:+PrintGCDetails 运行以下程序

public class Test {
    public static void main(String[] args) {
        byte[] bytes = new byte[50 * 1024 * 1024];
    }
}

输出结果:爆OOM

[GC (Allocation Failure) [PSYoungGen: 1595K->488K(2560K)] 1595K->684K(9728K), 0.0009240 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 488K->488K(2560K)] 684K->751K(9728K), 0.0004532 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 263K->640K(7168K)] 751K->640K(9728K), [Metaspace: 3052K->3052K(1056768K)], 0.0049474 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 640K->640K(9728K), 0.0002450 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 640K->623K(7168K)] 640K->623K(9728K), [Metaspace: 3052K->3052K(1056768K)], 0.0045375 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 2560K, used 153K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 7% used [0x00000000ffd00000,0x00000000ffd26470,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 623K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 8% used [0x00000000ff600000,0x00000000ff69bd68,0x00000000ffd00000)
 Metaspace       used 3131K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 341K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

参数详解:

 

 

SurvivorRatio:幸存区

调节新生代中 eden 和 S0、S1的空间比例,默认为 -XX:SuriviorRatio=8,Eden:S0:S1 = 8:1:1

假如设置成 -XX:SurvivorRatio=4,则为 Eden:S0:S1 = 4:1:1

SurvivorRatio值就是设置eden区的比例占多少,S0和S1相同。

NewRatio讲解

配置年轻代new 和老年代old 在堆结构的占比

默认:-XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3

  -XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5,

  NewRadio值就是设置老年代的占比,剩下的1个新生代。

新生代特别小,会造成频繁的进行GC收集。

MaxTenuringThreshold讲解

晋升到老年代的对象年龄。

  SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,

如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代。

这里就是调整这个次数的,默认是15,并且设置的值 在 0~15之间。

  -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。

如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概念。

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

强引用(Reference)

  指的是Reference类以及继承派生的类

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。

public static void main(String[] args) {
        // 默认就是强引用
        Object o1 = new Object();
        // 引用赋值
        Object o2 = o1;
        // 置空
        o1 = null;
        // 建议垃圾回收器回收垃圾
        System.gc();
        System.out.println(o2);
    }

输出结果是:o2对象能够正常打印,说明o1并没有被垃圾回收器进行回收处理。

  强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。

在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

软引用SoftReference

  软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

对于只有软引用的对象来说,

当系统内存充足时它不会被回收,
当系统内存不足时它会被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

  当内存充足的时候,软引用不用回收:

//  创建一个强引用
        Object o1 = new Object();
        //  软引用
        SoftReference<Object> reference = new SoftReference<>(o1);
        System.out.println(o1);
        // 返回此参考对象的参考对象。 如果已通过程序或垃圾收集器清除了此引用对象,则此方法返回null
        System.out.println(reference.get());

        // 置空
        o1 = null;
        System.out.println(o1);
        System.out.println(reference.get());

   内存不足情况:

//  创建一个强引用
        Object o1 = new Object();
        //  软引用
        SoftReference<Object> reference = new SoftReference<>(o1);
        System.out.println(o1);
        // 返回此参考对象的参考对象。 如果已通过程序或垃圾收集器清除了此引用对象,则此方法返回null
        System.out.println(reference.get());
        // 置空
        o1 = null;
        try {
            // 创建30m大对象,模拟oom
            byte[] bytes = new byte[30 * 1024 * 1024];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(o1);
            System.out.println(reference.get());
        }

弱引用(WeakReference)

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,

对于只有弱引用的对象来说,只要垃圾回收机制一运行不管JVM的内存空间是否足够,都会回收该对象占用的内存。

// 弱引用
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1);
System.out.println(o1);
System.out.println(weakReference.get());
o1 = null;
System.gc();
System.out.println("==============");
System.out.println(o1);
System.out.println(weakReference.get());

WeakHashMap

  WeakHashMap 继承于AbstractMap,实现了Map接口。

HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null。

不过WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除

更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了、

  测试:HashMap跟WeakHashMap的区别

public static void main(String[] args) {
    myHashMap();
    System.out.println("===================");
    myWeakHashMap();
}
public static void myHashMap() {
    Map<Integer, String> map = new HashMap<>();
    Integer key = new Integer("1");
    map.put(key, "hashMap");
    System.out.println(map);
    // Integer置空
    key = null;
    // 通知gc
    System.gc();
    System.out.println(map);
}
public static void myWeakHashMap() {
    Map<Integer, String> map = new WeakHashMap<>();
    Integer key = new Integer("1");
    map.put(key, "hashMap");
    System.out.println(map);
    // Integer置空
    key = null;
    // 通知gc
    System.gc();
    System.out.println(map);
}

测试结果:在WeakHashMap中当某个键不再正常使用时,会被从WeakHashMap中被自动移除,通常在使用WeakHashMap做高速缓存

虚引用介绍

  虚引用需要java.lang.ref.PhantomReference类来实现。

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。

虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。

PhantomReference的gei方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比fihalization机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

引用队列(ReferenceQueue)

回收前需要被引用的,会用队列保存下。

public static void main(String[] args) {
    // 引用队列
    Object object = new Object();
    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    WeakReference<Object> weakReference = new WeakReference<>(object, referenceQueue);
    // 1.GC 执行前
    System.out.println(object);
    System.out.println(weakReference.get());
    System.out.println(referenceQueue.poll());
    object = null;
    System.gc();
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 2.GC执行后
    System.out.println("=======GC执行后=========");
    System.out.println(object);
    System.out.println(weakReference.get());
    // 在GC之前,会被放到引用队列中保存一段时间
    System.out.println(referenceQueue.poll());
}
public static void myHashMap() {
    Map<Integer, String> map = new HashMap<>();
    Integer key = new Integer("1");
    map.put(key, "hashMap");
    System.out.println(map);
    // Integer置空
    key = null;
    // 通知gc
    System.gc();
    System.out.println(map);
}

虚引用

public static void main(String[] args) throws InterruptedException {
        /**
         * 虚引用,相当于aop中的后置通知,言简意赅就是死之前还想干啥。
         */
        Object obj = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);

        System.out.println(obj);
        // 返回此参考对象的参考对象。 因为幻影引用的被引用者始终不可访问,所以此方法始终返回null
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());

        System.out.println("========GC后=========");
        obj = null;
        System.gc();
        Thread.sleep(500);

        System.out.println(obj);
        // 返回此参考对象的参考对象。 因为幻影引用的被引用者始终不可访问,所以此方法始终返回null
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
    }
}

测试结果:

总结:

七、谈谈你对OOM的理解

栈溢出错误(StackOverFlowError)

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

自己调自己,就会把栈空间给撑爆:

堆溢出错误(OutOfMemoryError)

设置jvm参数大小:-Xms10m -Xmx20m

// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
byte[] bytes = new byte[50 * 1024 * 1024]; //50MB

给jvm堆区最大设置为20mb,我们创建一个50mb的大对象,一下就可以将堆区填满!

超出GC开销限制(overhead limit exceeded)

  GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC 都只回收了不到2%的极端情况下才会抛出。

假如不抛出GC overhead limit错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使cc再次执行。这样就形成恶性循环,CPU使用率一直是100%,而Gc却没有任何成果。

/**
 * 虚拟机参数:
 * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m
 *
 * @param args
 */
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;
    }
}

OOM之Direct buffer memory

导致原因:

  写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避兔了在Java堆和Native堆中来回复制数据。

  ByteBuffer.allocate(capability) 第一种方式是分配VM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢。
ByteBuffer.allocateDirect(capability) 第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。

/**
 * JVM参数:
 *  -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 *
 * @param args
 * @throws InterruptedException
 */
public static void main(String[] args) throws InterruptedException {
    System.out.println(String.format("配置的maxDirectMemory: %.2f MB", sun.misc.VM.maxDirectMemory() / 1024.0 / 1024));
    TimeUnit.SECONDS.sleep(3);
    ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}

输出结果:

OOM之unable to create new native thread

  不能够创建更多的新的线程了,也就是说创建线程的上限达到了

高并发请求服务器时,经常会出现异常java.lang.OutOfMemoryError:unable to create new native thread,准确说该native thread异常与对应的平台有关

导致原因:

  应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread

解决方法:

  想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改Linux服务器配置,扩大linux默认限制

public class OOMEUnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("************** i = " + i);
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

上面程序在Linux OS(CentOS)运行,会出现下列的错误,线程数大概在900多个

Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread

OOM之Metaspace

  使用java -XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:MetaspaceSize为21810376B(大约20.8M)

Java 8及之后的版本使用Metaspace来替代永久代。

Metaspace是方法区在Hotspot 中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存也即在Java8中, classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace native memory。

永久代(Java8后被原空向Metaspace取代了)存放了以下信息:

虚拟机加载的类信息
常量池
静态变量
即时编译后的代码
模拟Metaspace空间溢出,我们借助CGLib直接操作字节码运行时不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的。

首先添加CGLib依赖

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.10</version>
</dependency>
import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class OOMEMetaspaceDemo {
    // 静态类
    static class OOMObject {}

    /**
     * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
     * 
     * @param args
     */
    public static void main(final String[] args) {
        // 模拟计数多少次以后发生异常
        int i =0;
        try {
            while (true) {
                i++;
                // 使用Spring的动态字节码技术
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMObject.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("发生异常的次数:" + i);
            e.printStackTrace();
        } finally {

        }

    }
}

输出结果:

发生异常的次数:569
java.lang.OutOfMemoryError: Metaspace
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
	at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
	at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
	at com.lun.jvm.OOMEMetaspaceDemo.main(OOMEMetaspaceDemo.java:37)  

八、JVM中垃圾收集器回收种类

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

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

4种主要垃圾收集器:

串行垃级回收器(Serial) :

  它为单线程环境设计且值使用一个线程进行垃圾收集,会暂停所有的用户线程,只有当垃圾回收完成时,才会重新唤醒主线程继续执行。所以不适合服务器环境。

并行垃圾回收器(Parallel) :

  多个垃圾收集线程并行工作,此时用户线程也是阻塞的,适用于科学计算 / 大数据处理等弱交互场景,也就是说Serial 和 Parallel其实是类似的,不过是多了几个线程进行垃圾收集,但是主线程都会被暂停,但是并行垃圾收集器处理时间,肯定比串行的垃圾收集器要更短。

并发垃圾回收器(CMS) :

  用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,互联网公司都在使用,适用于响应时间有要求的场景。

G1垃圾回收器:

  G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

垃圾回收面试题:

怎么查看服务器默认的垃圾收集器是那个?

java -XX:+PrintCommandLineFlags -version
L:\IDEA_Code\JavaSE>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=131690304 -XX:MaxHeapSize=2107044864 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)

谈谈你对垃圾收集器的理解了

Java中一共有7大垃圾收集器

年轻代GC

  UserSerialGC:串行垃圾收集器
  UserParallelGC:并行垃圾收集器
  UseParNewGC:年轻代的并行垃圾回收器
老年代GC

  UserSerialOldGC:串行老年代垃圾收集器(已经被移除)
  UseParallelOldGC:老年代的并行垃圾回收器
  UseConcMarkSweepGC:(CMS)并发标记清除
老嫩通吃

  UseG1GC:G1垃圾收集器

GC之7大垃圾收集器概述

  垃圾收集器就来具体实现这些GC算法并实现内存回收。

不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:

GC之Serial收集器

一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束。

串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World”状态)。

虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

对应JVM参数是:-XX:+UseSerialGC

开启后会使用:Serial(Young区用) + Serial Old(Old区用)的收集器组合

表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法

VM参数:(启用UseSerialGC)

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC 
[GC (Allocation Failure) [DefNew: 2659K->320K(3072K), 0.0013540 secs] 2659K->953K(9920K), 0.0018882 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2387K->248K(3072K), 0.0017281 secs] 3020K->1636K(9920K), 0.0017620 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2306K->0K(3072K), 0.0007182 secs] 3694K->2382K(9920K), 0.0007386 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 1006K->1K(3072K), 0.0006250 secs] 3387K->3375K(9920K), 0.0006414 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2053K->2K(3072K), 0.0008402 secs] 5427K->5363K(9920K), 0.0008579 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2061K->2061K(3072K), 0.0000109 secs][Tenured: 5361K->2879K(6848K), 0.0024099 secs] 7422K->2879K(9920K), [Metaspace: 3088K->3088K(1056768K)], 0.0026777 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 2007K->1K(3072K), 0.0007402 secs][Tenured: 4865K->3874K(6848K), 0.0018438 secs] 4886K->3874K(9920K), [Metaspace: 3112K->3112K(1056768K)], 0.0026255 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 3874K->3603K(6848K), 0.0027574 secs] 3874K->3603K(9920K), [Metaspace: 3112K->3112K(1056768K)], 0.0027900 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 3072K, used 176K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  eden space 2752K,   6% used [0x00000000ff600000, 0x00000000ff62c170, 0x00000000ff8b0000)
  from space 320K,   0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  to   space 320K,   0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
 tenured generation   total 6848K, used 3603K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
   the space 6848K,  52% used [0x00000000ff950000, 0x00000000ffcd4ce0, 0x00000000ffcd4e00, 0x0000000100000000)
 Metaspace       used 3197K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K

GC之ParNew收集器

  一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-The-World暂停其他所有的工作线程直到它收集结束。

ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Seria收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。

常用对应JVM参数:-XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代。

开启上述参数后,会使用:ParNew(Young区)+ Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法

但是,ParNew+Tenured这样的搭配,Java8已经不再被推荐

JVM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 
[GC (Allocation Failure) [ParNew: 2702K->320K(3072K), 0.0007029 secs] 2702K->1272K(9920K), 0.0007396 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 2292K->37K(3072K), 0.0010829 secs] 3244K->2774K(9920K), 0.0011000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 2005K->9K(3072K), 0.0008401 secs] 4742K->5624K(9920K), 0.0008605 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 1974K->1974K(3072K), 0.0000136 secs][Tenured: 5615K->3404K(6848K), 0.0021646 secs] 7589K->3404K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0022520 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 1918K->2K(3072K), 0.0008094 secs] 5322K->5324K(9920K), 0.0008273 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [ParNew: 1970K->1970K(3072K), 0.0000282 secs][Tenured: 5322K->4363K(6848K), 0.0018652 secs] 7292K->4363K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0019205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 4363K->4348K(6848K), 0.0023131 secs] 4363K->4348K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0023358 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
 par new generation   total 3072K, used 106K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
  eden space 2752K,   3% used [0x00000000ff600000, 0x00000000ff61a938, 0x00000000ff8b0000)
  from space 320K,   0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
  to   space 320K,   0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
 tenured generation   total 6848K, used 4348K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
   the space 6848K,  63% used [0x00000000ff950000, 0x00000000ffd8f3a0, 0x00000000ffd8f400, 0x0000000100000000)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release

GC之Parallel收集器

Parallel / Parallel Scavenge

  Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化。

它重点关注的是:

  可控制的吞吐量(Thoughput=运行用户代码时间(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99% )。

高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。

自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMillis)或最大的吞吐量。

常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器。

开启该参数后:新生代使用复制算法,老年代使用标记-整理算法。

多说一句:-XX:ParallelGCThreads=数字N 表示启动多少个GC线程

cpu>8 N= 5/8

cpu<8 N=实际个数

VM参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC

输出结果:

-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
[GC (Allocation Failure) [PSYoungGen: 2009K->503K(2560K)] 2009K->803K(9728K), 0.7943182 secs] [Times: user=0.00 sys=0.00, real=0.79 secs] 
[GC (Allocation Failure) [PSYoungGen: 2272K->432K(2560K)] 2572K->2214K(9728K), 0.0020218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2448K->352K(2560K)] 4230K->3122K(9728K), 0.0017173 secs] [Times: user=0.11 sys=0.02, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1380K->0K(2560K)] [ParOldGen: 6722K->2502K(7168K)] 8102K->2502K(9728K), [Metaspace: 2657K->2657K(1056768K)], 0.0039763 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2016K->0K(2560K)] [ParOldGen: 6454K->6454K(7168K)] 8471K->6454K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0049598 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6454K->6454K(9728K), 0.0008614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6454K->6440K(7168K)] 6454K->6440K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0055542 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOfRange(Arrays.java:3664)
	at java.lang.String.<init>(String.java:207)
	at java.lang.StringBuilder.toString(StringBuilder.java:407)
	at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
 PSYoungGen      total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14810,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 6440K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 89% used [0x00000000ff600000,0x00000000ffc4a1c8,0x00000000ffd00000)
 Metaspace       used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 286K, capacity 386K, committed 512K, reserved 1048576K

GC之如何选择垃圾收集器

组合的选择

单CPU或者小内存,单机程序
  -XX:+UseSerialGC
多CPU,需要最大的吞吐量,如后台计算型应用
  -XX:+UseParallelGC(这两个相互激活)
  -XX:+UseParallelOldGC
多CPU,追求低停顿时间,需要快速响应如互联网应用
  -XX:+UseConcMarkSweepGC
  -XX:+ParNewGC

GC之G1垃圾收集器:

设置G1垃圾收集参数:

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC

以前收集器特点:

  • 年轻代和老年代是各自独立且连续的内存块;
  • 年轻代收集使用单eden+s0+s1进行复机算法;
  • 老年代收集必须扫描整个老年代区域;
  • 都是以尽可能少而快速地执行GC为设计原则。

G1是什么?

 G1 (Garbage-First)收集器,是一款面向服务端应用的收集器:

从官网的描述中,我们知道G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。

另外,它还具有以下特性:

  • 像CMS收集器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 需要更多的时间来预测GC停顿时间。
  • 不希望牺牲大量的吞吐性能。
  • 不需要更大的Java Heap。
  • G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。

G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器-G1垃圾收集器。

G1是在2012年才在jdk1.7u4中可用。oracle官方计划在JDK9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收集器。

主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region ,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

特点:

  • G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW。
  • G1整体上采用标记-整理算法,局部是通过复制算法,不会产生内存碎片。
  • 宏观上看G1之中不再区分年轻代和老年代。把内存划分成多个独立的子区域(Region),可以近似理解为一个围棋的棋盘。
  • G1收集器里面讲整个的内存区都混合在一起了,但其本身依然在小范围内要进行年轻代和老年代的区分,保留了新生代和老年代,但它们不再是物理隔离的,而是一部分Region的集合且不需要Region是连续的,也就是说依然会采用不同的GC方式来处理不同的区域。
  • G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换。

G1垃圾收集器底层原理

Region区域化垃圾收集器 - 最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。

核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小,在堆的使用上,G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

大小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为: 32 M B ∗ 2048 = 65536 M B = 64 G 32MB*2048=65536MB=64G 32MB∗2048=65536MB=64G内存

G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。

这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。

这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了

在G1中,还有一种特殊的区域,叫Humongous区域。

如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。

为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

GC之G1参数配置及和CMS的比较

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

开发人员仅仅需要声明以下参数即可:

三步归纳:开始G1+设置最大内存+设置最大停顿时间

  • -XX:+UseG1GC
  • -Xmx32g
  • -XX:MaxGCPauseMillis=100
  • -XX:MaxGCPauseMillis=n:最大GC停顿时间单位毫秒,这是个软目标,JVM将尽可能(但不保证)停顿小于这个时间

G1和CMS比较

  • G1不会产生内碎片
  • 是以精准控制停顿。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

八、关于系统监测的Linux常用命令

top - 整机性能查看

参数说明

  • d : 改变显示的更新速度,或是在交谈式指令列( interactive command)按 s
  • q : 没有任何延迟的显示速度,如果使用者是有 superuser 的权限,则 top 将会以最高的优先序执行
  • c : 切换显示模式,共有两种模式,一是只显示执行档的名称,另一种是显示完整的路径与名称
  • S : 累积模式,会将己完成或消失的子行程 ( dead child process ) 的 CPU time 累积起来
  • s : 安全模式,将交谈式指令取消, 避免潜在的危机
  • i : 不显示任何闲置 (idle) 或无用 (zombie) 的行程
  • n : 更新的次数,完成后将会退出 top
  • b : 批次档模式,搭配 "n" 参数一起使用,可以用来将 top 的结果输出到档案内

主要看load average, CPU, MEN三部分

load average表示系统负载,即任务队列的平均长度。 三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。

load average: 如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了

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

CPU查看之:vmstat

procs

  • r:运行和等待的CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不超过总核数的2倍,否则代表系统压力过大,我们看蘑菇博客测试服务器,能发现都超过了2,说明现在压力过大
  • b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等

cpu

  1. us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,优化程序
  2. sy:内核进程消耗的CPU时间百分比
  3. us + sy 参考值为80%,如果us + sy 大于80%,说明可能存在CPU不足,从上面的图片可以看出,us + sy还没有超过百分80,因此说明蘑菇博客的CPU消耗不是很高
  4. id:处于空闲的CPU百分比
  5. wa:系统等待IO的CPU时间百分比
  6. st:来自于一个虚拟机偷取的CPU时间比

2 3 表示:每隔2s打印一次数据,打印3次结束

查看内存状态:free

参数:

  • -b  以Byte为单位显示内存使用情况。
  • -k  以KB为单位显示内存使用情况。
  • -m  以MB为单位显示内存使用情况。
  • -h  以合适的单位显示内存使用情况,最大为三位数,自动计算对应的单位值。单位有:

    B = bytes
    K = kilos
    M = megas
    G = gigas
    T = teras
  • -o  不显示缓冲区调节列。
  • -s<间隔秒数>  持续观察内存使用状况。
  • -t  显示内存总和列。
  • -V  显示版本信息。

查看内存使用情况:

每2s执行一次命令:

硬盘查看命令:df

Size:总大小

User:已使用

Avail:剩余

Use:已使用的内存百分比

CPU内存占用过高思路分析:

结合Linux和JDK命令一块分析:

测试代码:运行这段死循环代码

public class Test{
    public static void main(String[] args){
        while(true){
            System.out.println("hello");
        }
    }
}

案例步骤

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

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

3、定位到具体线程

ps -mp 进程 -o THREAD,tid,time

-m 显示所有的线程
-p pid进程使用cpu的时间
-o 该参数后是用户自定义格式

4、定位到具体的代码

将需要的线程ID转换为16进制格式(英文小写格式),命令printf %x 172 将172转换为十六进制
jstack 进程ID | grep tid(16进制线程ID小写英文)-A60

 

 

 到此,排查CPU内存占用过高就已经结束!

 

posted @ 2021-05-02 21:37  Java小白的搬砖路  阅读(115)  评论(0编辑  收藏  举报