第 3 章 JVM 与 GC

第 3 章 JVM 与 GC

1、JVM 复习串讲

JVM 内存结构

  • JVM 体系结构

第 3 章 JVM 与 GC_第1张图片

  • Java8以后的JVM

第 3 章 JVM 与 GC_第2张图片

第 3 章 JVM 与 GC_第3张图片

GC 的作用域

第 3 章 JVM 与 GC_第4张图片

常见的垃圾收集算法

  1. 引用计数算法

    第 3 章 JVM 与 GC_第5张图片

  2. 复制算法

    第 3 章 JVM 与 GC_第6张图片

    第 3 章 JVM 与 GC_第7张图片

  3. 标记清除算法

    第 3 章 JVM 与 GC_第8张图片

  4. 标记整理算法

    第 3 章 JVM 与 GC_第9张图片

2、谈谈 GC Roots

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

什么是垃圾?

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


引用计数算法

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

第 3 章 JVM 与 GC_第10张图片


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

  1. 为了解决引用计数法的循环引用问题,Java使用了可达性分析的方法。
  2. 所谓“GCroots”或者说tracing GC的“根集合”就是一组必须活跃的引用。
  3. 基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
  4. 也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活;没有被遍历到的就自然被判定为死亡。

第 3 章 JVM 与 GC_第11张图片

第 3 章 JVM 与 GC_第12张图片

那么问题来了,哪些可作为GC Roots的对象

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

3、JVM 系统默认值

JVM常用基本配置参数有哪些?

3.1、JVM参数类型

3.1.1、标配参数

在jdk各个版本之间稳定,很有大的变化

  1. -version
  2. -help
  3. java -showversion

第 3 章 JVM 与 GC_第13张图片

3.1.2、X 参数

X 参数了解即可

  1. -Xint(修改编译模式)
  2. -Xcomp
  3. -Xmixed

第 3 章 JVM 与 GC_第14张图片

3.1.3、XX 参数

1、Boolean 类型

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


示例

  1. 是否打印GC收集细节:
    • -XX:-PrintGCDetails
    • -XX:+PrintGCDetails
  2. 是否使用串行垃圾回收器
    • -XX:-UseSerialGC
    • -XX:+UseSerialGC

2、KV键值对类型

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


示例

  1. -XX:MetaspaceSize=128m
  2. -XX:MaxTenuringThreshold=15

题外话:jinfo 如何查看当前运行程序的配置

公式 1:jinfo -flag 配置项 进程编号

公式 2:jinfo -flags 进程编号


示例

  1. jps -l :得到 JVM 进程编号
  2. jinfo -flag PrintGCDetails pid :查看 PrintGCDetails 属性是否开启(pid 为进程编号)

题外话:两个经典参数 -Xms和-Xmx

  1. -Xms:等价于-XX:InitialHeapSize
  2. -Xmx:等价于-XX:MaxHeapSize

3.2、查看 JVM 默认值

-XX:+PrintFlagsInitial:主要查看初始默认值

公式:

  1. java -XX:+PrintFlagsInitial -version
  2. java -XX:+PrintFlagsInitial

示例

第 3 章 JVM 与 GC_第15张图片

-XX:+PrintFlagsfinal:主要查看修改更新

公式:java -XX:+PrintFlagsFinal -version


示例

  1. = 表示没被修改过
  2. := 表示 JVM 默认加载时修改过或人为修改过

第 3 章 JVM 与 GC_第16张图片

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

  • java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512m T(T 为 Java 类名)

第 3 章 JVM 与 GC_第17张图片

-XX:+PrintCommandLineFlags:打印命令行参数

公式:java -XX:+PrintCommandLineFlags -version

C:\Users\Heygo\Desktop\Interview>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266620736 -XX:MaxHeapSize=4265931776 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocatio
n -XX:+UseParallelGC
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

4、JVM 常用参数

基础知识复习

/**
 * @ClassName HelloGC
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 15:12
 * @Version 1.0
 */
public class HelloGC {

    public static void main(String[] args) {
        long totalMemory = Runtime.getRuntime().totalMemory(); // Java 虚拟机中的内存总量
        long maxMemory = Runtime.getRuntime().maxMemory(); // Java 虚拟机试图使用的最大内存量
        System.out.println("TOTAL_MEMORY:" + totalMemory / (double) 1024 / 1024 + "MB");
        System.out.println("MAX_MEMORY:" + maxMemory / (double) 1014 / 1024 + "MB");
    }

}

常用参数

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

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

  3. -Xss:设置单个线程栈的大小,等价于-XX:ThreadStackSize,一般默认为512~1024K;0代表默认出厂值

    第 3 章 JVM 与 GC_第18张图片

  4. -Xmn:设置年轻代大小

  5. -XX:MetaSpaceSize:设置元空间大小

    • 元空间本质和永久代类似,都是对JVM中方法区的实现。不过元空间与永久代之间最大的区别是:元空间并不在虚拟机中,而是使用本地内存。
    • 因此,默认情况下,元空间的大小仅受本地内存限制,默认大小约为 21M
    • -Xms10m -Xmx10m -XX:MetaSpaceSize=1024m -XX:+PrintFlagsFinal
  6. -XX:+PrintGCDetails:输出详细GC收集日志信息。

    第 3 章 JVM 与 GC_第19张图片

    第 3 章 JVM 与 GC_第20张图片

  7. -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例

    • 默认-XX:SurvivorRatio=8,则Eden:S0:S1 = 8:1:1
    • 假如设置 -XX:SurvivorRatio=4,则新生代中 Eden:S0:S1=4:1:1
    • 即SurvivorRatio值就是设置Eden区的比例占多少,S0/S1相同
  8. -XX:NewRatio:配置年轻代与老年代在堆结构的占比

    • 默认-XX:NewRatop=2,新生代占1,老年代占2,即新生代占整个堆的1/3
    • 假如设置参数-XX:NewRatop=4,新生代占1,老年代占4,即新生代占整个堆的1/5
    • NewRatio就是老年代占比,剩下的1给年轻代
  9. -XX:MaxTenuringThreshold:设置垃圾最大年龄

    • -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。
    • 如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加对象在年轻代被回收的概率

典型设置案例:可以和面试官闲聊的案例

-Xms4096m -Xmx4096m -Xss1024k -XX:MetaSpaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC

5、四中引用类型

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

5.1、整体架构

强、软、弱、虚

第 3 章 JVM 与 GC_第21张图片

5.2、四种引用

5.2.1、强引用

强引用(默认支持模式)

  1. 当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收
  2. 强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象
  3. 在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达。状态,它是不可能被垃圾回收机制回收的,即使该对象以后远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一
  4. 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)
5.2.2、软引用

软引用的含义

  1. 软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
  2. 对于只有软引用的对象来说,当系统内存充足时它不会被回收,当系统内存不足时它会被回收
  3. 软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

软引用代码示例

示例 1:内存够用不会回收软引用对象

  • 代码
/**
 * 内存够用的时候就保留,不够用就回收
 *
 * @ClassName SoftReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 21:54
 * @Version 1.0
 */
public class SoftReferenceDemo {

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

    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(); // 进行 GC
        System.out.println(o1); // 强引用变量为 null 必被回收
        System.out.println(softReference.get()); // 内存够用不会回收软引用对象
    }
    
}
  • 程序运行结果:软引用对象并没有被回收
java.lang.Object@4554617c
java.lang.Object@4554617c
null
java.lang.Object@4554617c

示例 2:内存不够用必定回收软引用对象

  • 代码
/**
 * 内存够用的时候就保留,不够用就回收
 *
 * @ClassName SoftReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 21:54
 * @Version 1.0
 */
public class SoftReferenceDemo {

    public static void main(String[] args) {
        //softRef_Memory_Enough();
        softRef_Memory_NotEnough();
    }

    /*
        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;

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

}
  • 程序运行结果:OOM 之前,必定进行一次 Full GC ,此时会清除软引用对象
[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->640K(5632K), 0.0008207 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
java.lang.Object@4554617c
java.lang.Object@4554617c
[GC (Allocation Failure) [PSYoungGen: 1403K->504K(1536K)] 1539K->752K(5632K), 0.0007773 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 752K->768K(5632K), 0.0005913 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 264K->637K(4096K)] 768K->637K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0055154 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 637K->637K(5632K), 0.0002769 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) java.lang.OutOfMemoryError: Java heap space
	at com.Heygo.SoftReferenceDemo.softRef_Memory_NotEnough(SoftReferenceDemo.java:46)
	at com.Heygo.SoftReferenceDemo.main(SoftReferenceDemo.java:18)
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 637K->620K(4096K)] 637K->620K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0056716 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
null
null
Heap
 PSYoungGen      total 1536K, used 92K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 9% used [0x00000000ffe00000,0x00000000ffe173f8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 620K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 15% used [0x00000000ffa00000,0x00000000ffa9b138,0x00000000ffe00000)
 Metaspace       used 3487K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 382K, capacity 388K, committed 512K, reserved 1048576K
5.2.3、弱引用

弱引用的含义

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存

弱引用代码示例

  • 代码
/**
 * @ClassName WeakReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:09
 * @Version 1.0
 */
public class WeakReferenceDemo {
    public static void main(String[] args) {
        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());
    }
}
  • 程序运行结果:执行 GC,弱引用对象被回收
java.lang.Object@4554617c
java.lang.Object@4554617c
...............
null
null

你知道弱引用的话,谈谈WeakHashMap

  • 代码
/**
 * @ClassName WeakHashMapDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:14
 * @Version 1.0
 */
public class WeakHashMapDemo {
    public static void main(String[] args){
        myHashMap();
        System.out.println("========");
        myWeakHashMap();
    }

    private static void myHashMap(){
        HashMap<Integer,String> map = new HashMap<>();
        Integer key = new Integer(1);
        String value = "HashMap";

        map.put(key,value);
        System.out.println(map);

        // key 置为 null ,关 HashMap 毛事啊,HashMap 已经将数据保存至 Node 节点中了
        key = null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }

    private static void myWeakHashMap(){
        WeakHashMap<Integer,String> map = new WeakHashMap<>();
        Integer key = new Integer(2);
        String value = "WeakHashMap";

        map.put(key,value);
        System.out.println(map);

        key = null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }
}
  • 程序运行结果:如果 WeakHashMap 的 key 为 null ,GC 后该 KV 节点将被回收
{1=HashMap}
{1=HashMap}
{1=HashMap}
========
{2=WeakHashMap}
{2=WeakHashMap}
{}
5.2.4、虚引用

虚引用的作用

  1. 虚引用需要java.lang.ref.PhantomReference类来实现。顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
  2. 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列ReferenceQueue联合使用。
  3. 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize以后,做某些事情的机制。
  4. PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。
  5. 换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
  6. Java 技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

image-20200809222628859


引用队列:我(虚引用)被回收前需要被引用队列保存下

第 3 章 JVM 与 GC_第22张图片


引用队列代码示例

  • 代码
/**
 * @ClassName ReferenceQueueDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:30
 * @Version 1.0
 */
public class ReferenceQueueDemo {
    public static void main(String[] args) throws InterruptedException{
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        WeakReference<Object> weakReference = new WeakReference<>(o1,referenceQueue);
        System.out.println(o1);
        System.out.println(weakReference.get());
        System.out.println(referenceQueue.poll());

        System.out.println("=============");
        o1 = null;
        System.gc();
        Thread.sleep(500);

        System.out.println(o1);
        System.out.println(weakReference.get());
        System.out.println(referenceQueue.poll());
    }
}
  • 程序运行结果:在 GC 回收之前,将待回收的对象放入引用队列中,有点类似 Spring AOP 的后置通知
java.lang.Object@4554617c
java.lang.Object@4554617c
null
=============
null
null
java.lang.ref.WeakReference@74a14482

虚引用代码示例

  • 代码
/**
 * @ClassName PhantomReferenceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:30
 * @Version 1.0
 */
public class PhantomReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);

        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());

        System.out.println("=================");
        o1 = null;
        System.gc();
        Thread.sleep(500);

        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());
    }
}
  • 程序运行结果:对象被回收时,将其放入了引用队列
java.lang.Object@4554617c
null
null
=================
null
null
java.lang.ref.PhantomReference@74a14482

5.3、引用总结

软引用、弱引用适用场景

  1. 假如有一个应用需要读取大量的本地图片:
    • 如果每次读取图片都从硬盘读取则会严重影响性能,
    • 如果一次性全部加载到内存中又可能造成内存溢出。
  2. 此时使用软引用可以解决这个问题。设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题
Map<StringSoftReference<Bitmap>> imageCache = new HashMap<StringSoftReference<Bitmap>>();

GC Roots 和四种引用类型的总结

  1. java提供了4种引用类型,在垃级回收的时候,都有自己各自的特点。
  2. ReferenceQueue是用来配合引用工作的,没有有ReferenceQueue一样可以运行
  3. 创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一种通知机制。
  4. 当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。

第 3 章 JVM 与 GC_第23张图片

6、OOM

面试题:请谈谈你对OOM的认识

第 3 章 JVM 与 GC_第24张图片

java.lang.StackOverflowError

  • 代码:递归调用无结束条件
/**
 * @ClassName StackOverflowErrorDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:48
 * @Version 1.0
 */
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

  • 代码
/**
 * @ClassName JavaHeapSpaceDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 22:53
 * @Version 1.0
 */
public class JavaHeapSpaceDemo {
    public static void main(String[] args){
        String str = "seu";

        while(true){
            str += str + new Random().nextInt(11111111)+new Random().nextInt(22222222);
            str.intern();
        }

    }
}
  • JVM 参数
-Xms10m -Xmx10m
  • 程序运行结果
Exception in thread "main" 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:674)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at com.Heygo.JavaHeapSpaceDemo.main(JavaHeapSpaceDemo.java:17)

java.lang.OutOfMemoryError:GC overhead limit exceeded

  1. GC回收时间过长时会抛出0utOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存
  2. 连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead Limit 错误会发生什么情况呢?
  3. 那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用孩一直是100%,而GC却没有任何成效

第 3 章 JVM 与 GC_第25张图片


  • 代码:将生成的字符串放到字符串常量池中
/**
 * @ClassName GCOverheadDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 23:02
 * @Version 1.0
 */
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 (Throwable e) {
            System.out.println("***************i:" + i);
            e.printStackTrace();
            throw e;
        }

    }
}
  • JVM 参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
  • 程序运行结果:GC 说,老铁,我回收不动啊~~~
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7076K->7076K(7168K)] 9124K->9124K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0304580 secs] [Times: user=0.17 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7078K->7078K(7168K)] 9126K->9126K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0294555 secs] [Times: user=0.11 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7080K->7080K(7168K)] 9128K->9128K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0303605 secs] [Times: user=0.08 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7081K->7081K(7168K)] 9129K->9129K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0273443 secs] [Times: user=0.11 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7083K->7083K(7168K)] 9131K->9131K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0323289 secs] [Times: user=0.13 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7085K->7085K(7168K)] 9133K->9133K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0282554 secs] [Times: user=0.25 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7087K->7087K(7168K)] 9135K->9135K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0279839 secs] [Times: user=0.22 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7088K->7088K(7168K)] 9136K->9136K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0268396 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7090K->7090K(7168K)] 9138K->9138K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0275986 secs] [Times: user=0.14 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7092K->7092K(7168K)] 9140K->9140K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0279635 secs] [Times: user=0.14 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7094K->7094K(7168K)] 9142K->9142K(9728K), [Metaspace: 3472K->3472K(1056768K)], 0.0259208 secs] [Times: user=0.22 sys=0.01, real=0.03 secs] 
***************i:145410
[Full GC (Ergonomics) java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.Integer.toString(Integer.java:403)
	at java.lang.String.valueOf(String.java:3099)
	at com.Heygo.GCOverheadDemo.main(GCOverheadDemo.java:20)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
	at java.lang.Integer.toString(Integer.java:403)
	at java.lang.String.valueOf(String.java:3099)
	at com.Heygo.GCOverheadDemo.main(GCOverheadDemo.java:20)

java.lang.OutOfMemoryError:Direct buffer memory

导致原因:

  1. 写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
    • ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属GC管辖范围,由于需要拷贝所以速度相对较慢
    • ByteBuffer.allocateDirect(capability)第二种方式是分配OS本地内存,不属于GC管辖范围,由子不需要内存拷贝所以速度相对较快。
  2. 但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了

代码示例:查看 MaxDirectMemory

  • 代码
/**
 *
 * @ClassName DirectBufferMemoryDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/9 23:18
 * @Version 1.0
 */
public class DirectBufferMemoryDemo {
    public static void main(String[] args) {
        System.out.println("配置的maxDirectMemory: " + (sun.misc.VM.maxDirectMemory() / (double) 1024 / 1024) + "MB");
        try {
            Thread.sleep(300);
        } catch (Exception e) {
            e.printStackTrace();
        }
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
    }
}
  • 程序运行结果
配置的maxDirectMemory: 3618.0MB

演示 DirectBuffer OOM

  • 代码同上

  • JVM 参数

-Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
  • 程序运行结果
配置的maxDirectMemory: 5.0MB
[GC (Allocation Failure) [PSYoungGen: 2035K->488K(2560K)] 2035K->792K(9728K), 0.0007089 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 564K->496K(2560K)] 868K->912K(9728K), 0.0008965 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 496K->0K(2560K)] [ParOldGen: 416K->705K(7168K)] 912K->705K(9728K), [Metaspace: 3483K->3483K(1056768K)], 0.0051785 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:694)
	at java.nio.DirectByteBuffer.(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.Heygo.DirectBufferMemoryDemo.main(DirectBufferMemoryDemo.java:22)

结论:

写 NIO 和 Netty 程序时要小心

java.lang.OutOfMemoryError:unable to create new native thread

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


导致原因:

  1. 你的应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载极限
  2. 你的服务器并不允许你的应用程序创建这么多线程,Linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报java.Lang.outofMemoryError:unable to create new native thread

解决办法:

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

非root用户登录Linux系统测试

  • 代码
package com.atguigu.Interview.study..jvm.oom;

/**
 * @ClassName UnableCreateNewThreadDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/10 12:59
 * @Version 1.0
 */
public class UnableCreateNewThreadDemo {

    public static void main(String[] args) {
        for (int i = 1; ; i++) {
            System.out.println(i);
            new Thread(() -> {
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  • Linux 编译带包名的 Java源文件
java -d . UnableCreateNewThreadDemo.java
  • Linux 运行带包名的 Java 程序
java com.atguigu.Interview.study..jvm.oom.UnableCreateNewThreadDemo
  • 程序运行结果

image-20200810131353081


终止程序

  • 当前普通用户无法终止该进程,需要 root 用户帮忙
ps -ef|grep java
kill -9 pid

image-20200810130959638


服务器级别调优参数

  • 查看当前用户的可创建的最大线程数
ulimit -u
  • 编辑配置文件:可以看到除了 root 用户无限制,其他用户所能创建的最大线程数为 1024
vim /etc/security/limits.d/90-nproc.conf

第 3 章 JVM 与 GC_第26张图片

  • 我们给张三用户多分配一些线程数

第 3 章 JVM 与 GC_第27张图片

java.lang.OutOfMemoryError:Metaspace

  1. Metaspace是方法区在Hotspot中的实现,它与永久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存,也即在java8中,classe metadata(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的native memory
  2. Java 8及之后的版本使用Metaspace来替代永久代永久代,存放了以下信息:
    • 虚拟机加载的类信息
    • 常量池
    • 静态变量
    • 即时编译后的代码缓存

代码示例

  • 代码:利用 CGLIB 不停地往元空间中加载类
/**
 * 模Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的
 *
 * @ClassName MetaspaceOOMTest
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/10 13:28
 * @Version 1.0
 */
public class MetaspaceOOMTest {

    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(new MethodInterceptor){
                    @Override
                    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                        return methodProxy.invokeSupper(o, args);
                    }
                    enhancer.create();
                }
            }
        } catch (Throwable e) {
            System.out.println("********多少次后发生了异常:" + i);
            e.printStackTrace();
        }
    }

}
  • JVM 参数
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
  • 程序运行结果

image-20200810133839592

7、GC 垃圾回收

GC垃圾回收算法和垃圾收集器的关系?分别是什么?

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

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

  3. 四种主要的垃圾收集器:串行、并行、并发、G1

    第 3 章 JVM 与 GC_第28张图片

串行垃圾回收器(Serial)

它为单线程环境设计,且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境

程序 --> GC --> 程序 --> GC --> 程序 --> GC

并行垃圾回收器(Parallel)

多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理首台处理等弱交互场景

     --> GC         --> GC          --> GC
程序 --> GC --> 程序 --> GC --> 程序 --> GC
     --> GC         --> GC          --> GC

并发垃圾回收器(Concurrent Mark Sweep)

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

     --> GC         --> 程序         --> 程序
程序 --> GC --> 程序 --> GC --> 程序 --> GC
     --> GC         --> GC          --> 程序

G1 垃圾回收器(Garbage first)

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

串行与并行、STW与并发

  1. 串行与并行的垃圾回收器执行 GC 时,都会中断用户线程,STW 时间都会比较长(相对于并发来讲)
  2. 并发垃圾回收器可与用户线程并发执行,降低了 STW 的时间

第 3 章 JVM 与 GC_第29张图片

第 3 章 JVM 与 GC_第30张图片

8、垃圾收集器

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

8.1、查看默认垃圾收集器

命令行指令:java -XX:+PrintCommandLineFlags -version

  • -XX:+UseParallelGC:表示使用 UseParallelGC 垃圾收集器
C:\Users\Heygo\Desktop\Interview>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266620736 -XX:MaxHeapSize=4265931776 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocatio
n -XX:+UseParallelGC
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

8.2、默认的垃圾收集器

  1. Java的GC回收的类型主要有几种:UseSerialGC,UseParallelGC,UseConcMarkSweepGC,UseParNewGC,UseParallelOldGC,UseG1GC
  2. UseSerialOldGC被淘汰了

  • 撕个 OpenJDK 源码

bool Arguments::check_gc_consistency() {
  bool status = true;
  // Ensure that the user has not selected conflicting sets
  // of collectors. [Note: this check is merely a user convenience;
  // collectors over-ride each other so that only a non-conflicting
  // set is selected; however what the user gets is not what they
  // may have expected from the combination they asked for. It's
  // better to reduce user confusion by not allowing them to
  // select conflicting combinations.

  uint i = 0;
  if (UseSerialGC)                       i++;
  if (UseConcMarkSweepGC || UseParNewGC) i++;
  if (UseParallelGC || UseParallelOldGC) i++;
  if (UseG1GC)                           i++;
  if (i > 1) {
    jio_fprintf(defaultStream::error_stream(),
                "Conflicting collector combinations in option list; "
                "please refer to the release notes for the combinations "
                "allowed\n");

    status = false;
  }
  return status;
}

8.3、垃圾收集器细讲

垃圾收集器概述

垃圾收集器就来具体实现这些GC算法并实现内存回收。不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:

第 3 章 JVM 与 GC_第31张图片

垃圾收集器对应的组合关系

第 3 章 JVM 与 GC_第32张图片

8.3.1、部分参数说明
  1. DefNew:Default New Generation
  2. Tenured:Old
  3. ParNew:Parallel New Generation
  4. PSYoungGen:Parallel Scavenge
  5. ParOldGen:Parallel Old Generation
8.3.2、Server & Client
  1. 适用范围:只需要掌握Server模式即可,Client模式基本不会用
  2. 根据操作系统以及硬件区分 Client 和 Server:
    • 32位Window操作系统,不论硬件如何都默认使用Client的JVM模式
    • 32位其它操作系统,2G内存同时有2个cpu以上用Server模式,低于该配置还是Client模式
    • 64位only server模式
8.3.3、新生代垃圾回收

串行GC(Serial)/(Serial Copying)

串行收集器:Serial收集器

  1. 一句话:一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它集结束。
  2. 串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在进行垃圾收集过程中可能会产生较长的停顿(Stop-The-World状态)。
  3. 虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。

第 3 章 JVM 与 GC_第33张图片


参数配置:

  1. 对应JVM参数是:-XX:+UseSerialGC
  2. 开启后会使用:Serial(Young区用)+Serial Old(Old区用)的收集器组合
  3. 表示:新生代、老年代都会使用串行回收收集器,新生代使用复制算法,老年代使用标记-整理算法
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:UseSerialGC

第 3 章 JVM 与 GC_第34张图片

并行GC(ParNew)

ParNew(并行)收集器

  1. 一句话:使用多线程进行垃圾回收,在垃圾收集时,会Stop-the-World暂停其他所有的工作线程直到它收集结束。
  2. ParNew收集器其实就是Serial收集器新生代的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Seria收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。它是很多java虚拟机运行在Server模式下新生代的默认垃圾收集器。

第 3 章 JVM 与 GC_第35张图片


参数配置:

  1. 常用对应JVM参数:-XX:+UseParNewGC启用ParNew收集器,只影响新生代的收集,不影响老年代
  2. 开启上述参数后,会使用:ParNew(Young区用)+Serial Old的收集器组合,新生代使用复制算法,老年代采用标记-整理算法
  3. 但是,ParNew+Tenured这样的搭配,java8已经不再被推荐: 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
  4. -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC

第 3 章 JVM 与 GC_第36张图片

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

Parallel 并行回收

  1. Parallel Scavenge收集器类似ParNew也是一个新生代垃圾收集器,使用复制算法,也是一个并行的多线程的垃圾收集器,俗称吞吐量优先收集器。一句话:串行收集器在新生代和老年代的并行化
  2. 它重点关注的是:可控制的吞吐量(Thoughput=运行用户代码时间/(运行用户代码时间+垃圾收集时间),也即比如程序运行100分钟,垃圾收集时间1分钟,吞吐量就是99%)。高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多交互的任务。
  3. 自适应调节策略也是ParallelScavenge 收集器与ParNew收集器的一个重要区别。(自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间(-XX:MaxGCPauseMilis)或最大的吞吐量。

第 3 章 JVM 与 GC_第37张图片


参数配置:

  1. 常用JVM参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器
  2. 开启该参数后:新生代使用复制算法,老年代使用标记-整理算法
  3. 多说一句:-XX:ParallelGCThreads=数字N表示启动多少个GC线程
    • cpu>8:N=5/8
    • cpu<8:N=实际个数
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
或者
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldlGC

第 3 章 JVM 与 GC_第38张图片

8.3.4、老年代垃圾回收

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

  1. Serial Old是 Serial垃圾收集器老年代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。
  2. 在Server模式下,主要有两个用途(了解):
    • 在JDK1.5之前版本中与新生代的Parallel Scavenge 收集器搭配使用。即垃圾收集器组合为:Parallel Scavenge+Serial Old
    • 在JDK1.5之后作为老年代版中使用CMS收集器的后备垃圾收集方案。

参数配置:

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

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

  1. Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程的标记-整理算法,Parallel Old收集器在JDK1.6才开始提供。
  2. 在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配年老代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。即在JDK1.6之前的垃圾回收器的搭配方案为Parallel Scavenge+Serial Old
  3. Parallel Old正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,JDK1.8后可以优先考虑新生代。Parallel Scavenge和年老代Parallel Old 收集器的搭配策略。在JDK1.8的垃圾回收器的搭配方案为Parallel Scavenge+Parallel Old

参数配置:

  1. JVM常用参数:-XX:+UseParallelOldGC 使用Parallel Old收集器
  2. 设置该参数后,新生代Parallel+老年代Parallel Old
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldlGC
或者
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC

并发标记清除GC(CMS)

  1. CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器。
  2. 适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。
  3. CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
  4. Concurrent Mark Sweep并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行

第 3 章 JVM 与 GC_第39张图片


参数配置:

  1. 开启该收集器的JVM参数:-XX:+UseConcMarkSweepGC
  2. 开启该参数后会自动将-XX:+UseParNewGC打开开启该参数后,使用ParNew(Young区用)+CMS(Old区用)+Serial Old的收集器组合,Serial Old将作为CMS出错的后备收集器
-Xms10m -Xmx10m -XX:PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC 

CMS 的 4 步过程

  1. 初始标记(CMS initial mark):只是标记一下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
  2. 并发标记(CMS concurrent mark)和用户线程一起:进行GCRoots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。主要标记过程,标记全部对象
  3. 重新标记(CMS remark):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
  4. 并发清除(CMS concurrent sweep)和用户线程一起:清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程。基于标记结果,直接清理对象由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS收集器的内存回收和用户线程是一起并发地执行。

CMS 四步骤总结

第 3 章 JVM 与 GC_第40张图片


优点

并发收集停顿时间低


缺点

  1. 并发执行,对CPU压力较大:由于并发进行,CMS在收集与应用线程会同时会增加对堆内存的占用,也就是说,CMS必须要在老年代堆内存用尽之前完成垃圾回收,否则CMS回收失败时,将触发担保机制,串行老年代收集器将会以STW的方式进行一次GC,从而造成较大停顿时间
  2. 采用的标记清除算法会产生大量碎片:标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对堆内存进行压缩。CMS也提供了参数-XX:CMSFulIGCsBeforeCompaction(默认0,即每次都进行内存整理)来指定多少次CMS收集之后,进行一次压缩的Full GC。

Demo 代码

/*
1.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
2.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
3.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
4.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
5.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
6.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
7.-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC (已经没有了)
*/
public class GCDemo {

    public static void main(String[] args) {
        System.out.println("GCDemo...");
        try {
            String str = "bjtu";
            while (true) {
                str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
                str.intern();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

8.4、如何选择垃圾收集器

第 3 章 JVM 与 GC_第41张图片

第 3 章 JVM 与 GC_第42张图片

9、G1 垃圾收集器

9.1、Before G1

以前收集器特点

  1. 年轻代和老年代是各自独立且连续的内存块

  2. 年轻代收集,使用单eden+S0+S1进行复制算法

  3. 老年代收集必须扫描整个老年代区域

  4. 都是以尽可能快速地执行GC为设计原则

9.2、G1 是什么

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

第 3 章 JVM 与 GC_第43张图片


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

  1. 像CMS收集器一样,能与应用程序线程并发执行。
  2. 整理空闲空间更快。
  3. 需要更多的时间来预测GC停顿时间。
  4. 不希望牺牲大量的吞吐性能。
  5. 不需要更大的Java Heap。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色:

  1. G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
  2. G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间

为什么会出现 G1?

  1. CMS垃圾收集器虽然减少了暂停应用程序的运行时间,但是它还是存在着内存碎片问题。于是,为了去除内存碎片问题,同时又保留CMS垃圾收集器低暂停时间的优点,JAVA7发布了一个新的垃圾收集器——G1垃圾收集器。
  2. G1是在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器以替代CMS。它是一款面向服务端应用的收集器,主要应用在多CPU和大内存服务器环境下,极大的减少垃圾收集的停顿时间,全面提升服务器的性能,逐步替换java8以前的CMS收
  3. 主要改变是Eden,Survivor和Tenured等内存区域不再是连续的了,而是变成了一个个大小一样的region,每个region从1M到32M不等。一个region有可能属于Eden,Survivor或者Tenured内存区域。

9.3、G1 特点

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

9.4、G1 底层原理

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

  1. 区域化内存划片Region,整体编为了一些列不连续的内存区域,避免了全内存区的GC操作。
  2. 核心思想是将整个堆内存区域分成大小相同的子区域(Region),在JVM启动时会自动设置这些子区域的大小。
  3. 在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可,每个分区也不会固定地为某个代服务,可以按需在年轻代和老年代之间切换
  4. 启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
  5. 每个Region 小范围在1MB~32MB,最多能设置2048个区域,也即能够支持的最大内存为:32MB*2048=65536MB=64G内存

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

  1. 这些Region的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。
  2. 这些Region的一部分包含老年代,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有CMS内存碎片问题的存在了。

在G1中,还有一种特殊的区域,叫Humongous(巨大的)区域

  1. 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。
  2. 为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

第 3 章 JVM 与 GC_第44张图片

G1 回收步骤

G1收集器下的Young GC针对Eden区进行收集,Eden区耗尽后会被触发,主要是小区域收集+形成连续的内存块,避免内存碎片

  1. Eden区的数据移动到Survivor区,假如出现Survivor区空间不够,Eden区数据会部会晋升到Old区
  2. Survivor区的数据移动到新的Survivor区,部会数据晋升到Old区
  3. 最后Eden区收拾干净了,GC结束,用户的应用程序继续执行。

第 3 章 JVM 与 GC_第45张图片

第 3 章 JVM 与 GC_第46张图片

G1 工作的四大步骤

  1. 初始标记:只标记GC Roots能直接关联到的对象
  2. 并发标记:进行GC Roots Tracing的过程
  3. 最终标记:修正并发标记期间,因程序运行导致标记发生变化的那一部分对象
  4. 筛选回收:根据时间来进行价值最大化的回收

第 3 章 JVM 与 GC_第47张图片

G1 Demo 代码

  • 代码
/**
 * @ClassName GCDemo
 * @Description TODO
 * @Author Heygo
 * @Date 2020/8/10 18:40
 * @Version 1.0
 */
public class GCDemo {

    public static void main(String[] args) {
        System.out.println("GCDemo...");
        try {
            String str = "bjtu";
            while (true) {
                str += str + new Random().nextInt(77777777) + new Random().nextInt(88888888);
                str.intern();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
  • JVM 参数
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
  • 程序运行结果
    • initial-mark:初始标记
    • GC concurrent-mark-start :并发标记
    • GC remark:最终标记
    • GC cleanup:筛选回收
[GC pause (G1 Humongous Allocation) (young) (initial-mark) (to-space exhausted), 0.0032657 secs]
   [Parallel Time: 1.8 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 116.9, Avg: 117.0, Max: 117.2, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.3, Max: 0.6, Diff: 0.5, Sum: 2.4]
      [Update RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8]
         [Processed Buffers: Min: 0, Avg: 1.9, Max: 4, Diff: 4, Sum: 15]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 1.0, Avg: 1.2, Max: 1.3, Diff: 0.3, Sum: 9.8]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4]
         [Termination Attempts: Min: 1, Avg: 6.9, Max: 14, Diff: 13, Sum: 55]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
      [GC Worker Total (ms): Min: 1.5, Avg: 1.7, Max: 1.8, Diff: 0.2, Sum: 13.5]
      [GC Worker End (ms): Min: 118.7, Avg: 118.7, Max: 118.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.2 ms]
   [Other: 1.3 ms]
      [Evacuation Failure: 0.9 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.3 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 5120.0K(6144.0K)->0.0B(1024.0K) Survivors: 0.0B->1024.0K Heap: 7351.7K(10.0M)->6167.8K(10.0M)]
 [Times: user=0.09 sys=0.00, real=0.00 secs] 
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0009353 secs]
[GC concurrent-mark-start]
[GC pause (G1 Humongous Allocation) (young) (to-space exhausted), 0.0037669 secs]
   [Parallel Time: 2.7 ms, GC Workers: 8]
      [GC Worker Start (ms): Min: 122.1, Avg: 122.1, Max: 122.2, Diff: 0.1]
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.4, Max: 2.6, Diff: 2.5, Sum: 3.3]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
         [Processed Buffers: Min: 0, Avg: 0.4, Max: 3, Diff: 3, Sum: 3]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.2, Diff: 0.2, Sum: 0.2]
      [Object Copy (ms): Min: 0.0, Avg: 1.1, Max: 1.5, Diff: 1.5, Sum: 9.0]
      [Termination (ms): Min: 0.0, Avg: 1.0, Max: 1.3, Diff: 1.3, Sum: 7.9]
         [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 2.5, Avg: 2.6, Max: 2.6, Diff: 0.1, Sum: 20.6]
      [GC Worker End (ms): Min: 124.7, Avg: 124.7, Max: 124.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.9 ms]
      [Evacuation Failure: 0.5 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.1 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 1024.0K->0.0B Heap: 7372.3K(10.0M)->6188.4K(10.0M)]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC concurrent-mark-end, 0.0043902 secs]
[GC remark [Finalize Marking, 0.0001364 secs] [GC ref-proc, 0.0002039 secs] [Unloading, 0.0004323 secs], 0.0008988 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC cleanup 7372K->7372K(10M), 0.0005463 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
  • G1 中堆分为两部分:garbage-first heap 和 Metaspace(Non-heap)
Heap
 garbage-first heap   total 10240K, used 4199K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
  region size 1024K, 1 young (1024K), 0 survivors (0K)
 Metaspace       used 3510K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 384K, capacity 388K, committed 512K, reserved 1048576K

9.5、G1 常用参数

常用配置参数(了解)

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

G1 常用配置:

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

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

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

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=100

9.6、G1 的优势

G1 相较于 CMS 的优势

比起CMS有两个优势:

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

10、生产调优

10.1、SpringBoot 调优步骤

SpringBoot 微服务的生产部署和调参优化步骤

  1. IDEA开发完微服务工程
  2. maven进行clean + package
  3. 要求微服务启动的时候,同时配置我们的JVM/GC的调优参数
  4. 公式:java -server jvm的各种参数 -jar jar/war包名字
java -server -Xms1024m -Xmx1024m -XX+UseG1GC -jar springboot2019-1.0-SNAPSHOT.war

10.2、调优思路和性能评估

整机:top(查看 CPU、内存使用率等,也可使用精简版系统性能命令:uptime)

  • 主要查看 CPU、MEM、load average(按 1 可详细查看 CPU 的每个核心状态)

第 3 章 JVM 与 GC_第48张图片

CPU:vmstat(查看 CPU 使用情况)

vmstat -n 2 3

  • 一般vmstat工具的使用是通过两个数字参数来完成的,第一个参数是采样的时间间隔数单位是秒,第二个参数是采样的次数

第 3 章 JVM 与 GC_第49张图片


参数含义

  1. -procs
    • r:运行和等待CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不能超过总核数的2倍,否则代表系统压力过大
    • b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等
  2. -cpu
    • us:用户进程消耗CPU时间百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,需要优化程序
    • sy:内核进程消耗的CPU时间百分比
    • 说明:us +sy参考值为80%,如果us+sy大于80%,说明可能存在CPU不足
    • id:处于空闲的CPU百分比
    • wa:系统等待IO的CPU时间百分比
    • st:来自于一个虚拟机偷取的CPU时间的百分比

其他的查看指令

  1. 查看所有cpu核信息:mpstat -P ALL 2 (每两秒采样一次)

    第 3 章 JVM 与 GC_第50张图片

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

    第 3 章 JVM 与 GC_第51张图片

内存:free(查看应用程序可用内存数)

  1. free:以字节为单位
  2. free -g:以 GB 为单位
  3. free -m:以 GB 为单位

第 3 章 JVM 与 GC_第52张图片


经验值

  1. 应用程序可用内存/系统物理内存>70%内存充足
  2. 应用程序可用内存/系统物理内存<20%内存不足,需要增加内存
  3. 20%<应用程序可用内存/系统物理内存<70%内存基本够用

pidstat -p 进程号 -r 采样间隔秒数,可查看进程占用内存的情况

第 3 章 JVM 与 GC_第53张图片

硬盘:df(查看磁盘剩余空间数)

第 3 章 JVM 与 GC_第54张图片

磁盘io:iostat(磁盘IO性能评估)

磁盘块设备分布

  1. rkB/s每秒读取数据量kB
  2. wkB/s每秒写入数据量kB
  3. svctm I/O请求的平均服务时间,单位毫秒
  4. await I/O请求的平均等待时间,单位毫秒;值越小,性能越好
  5. util一秒中有百分几的时间用于I/O操作。接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘
  6. rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大数据读写,肯定不正常,需要优化程序读取。
  7. svctm的值与await的值很接近,表示几乎没有/O等待,磁盘性能好,如果await的值远高于svctm的值,则表示I/O队列等待太长,需要优化程序或更换更快磁盘。

第 3 章 JVM 与 GC_第55张图片


pidstat -p 进程号 -r 采样间隔秒数,可查看进程磁盘的使用情况

第 3 章 JVM 与 GC_第56张图片

网络io:ifstat(查看网络占用情况)

默认本地没有,下载ifstat

wget http://gael.roualland.free.fr/ifstat/ifstat-1.1.tar.gz

tar xzvf ifstat-1.1.tar.gz 

cd ifstat-1.1

./configure

make 

make install

查看网络 I/O 情况

第 3 章 JVM 与 GC_第57张图片

11、CPU 占用率高

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

下面开始举例啦~~~

先用top命令找出cpu占比最高的,记下PID

  • 第一个是控制台输出,不用管它,第二个才是我们要监控的进程(CPU 占用率高)

第 3 章 JVM 与 GC_第58张图片

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

image-20200811110422503

定位到具体的线程或代码

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

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

第 3 章 JVM 与 GC_第59张图片

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

printf “%x\n”:打印输出有问题的线程ID(用计算器算也可以)

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

-A60 表示打印出前 60 行

第 3 章 JVM 与 GC_第60张图片

12、JVM 分析工具

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

下面开讲

自带的JVM监控和性能分析工具是什么

  • 官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/

第 3 章 JVM 与 GC_第61张图片

自带的JVM监控和性能分析工具怎么用?

  1. jps(虚拟机进程状况工具)

  2. jinfo(java配置信息工具)

  3. jmap(内存映像工具)

  4. jstat(统计信息监视工具)

  5. jstack(堆栈异常跟踪工具)

  6. jvisualvm

  7. jconsole

你可能感兴趣的:(阳哥大厂面试题第二季,java,jvm)