飞天班第7节:深入JVM系列(下)

2020/03/11

1、GC详解

GC的作用域

口诀:关于垃圾回收分代收集算法 : 不同的区域使用不同的算法

Young代:GC频繁区域

Old代:GC次数较少

Perm代:不会产生GC!

一个对象的历程

Young = Eden + s0(from) + s1 (to)

JVM在进行GC时,并非每次都是对三个区域进行扫描的!大部分的时候都是在新生代PSYoungGen!

两个类型:

普通GC:新生代满了,只针对新生代PSYoungGen 【GC】

全局GC:老年代满了,主要是针对老年代ParOldGen,偶尔伴随新生代! 【Full GC】

可以看到普通GC针对新生代PSYoungGen进行垃圾回收,Full GC 针对老年代ParOldGen 和新生代PSYoungGen进行垃圾回收

GC面试题

1、JVM内存模型,每个区中存放什么?

方法区(1.7是永久代实现,1.8移除永久代,使用元空间实现)存放JVM加载的类信息,常量,字符串、静态变量

堆,存放new创建的对象和数组

栈,存放程序运行时的基本类型的变量和对象的引用变量

2、堆中的分区:Eden、Surival( form to)、老年代,请你说说他的特点?

所有的对象都是在Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC;清理完一次垃圾之后,会将活下来的对象,会放入Surival(幸存者区),……. 清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入老年代!运行了几个月之后,老年代满了,就会触发一次 Full GC;99% 的对象在 Eden 都是临时对象;

3、GC的三种收集方法:复制算法(新生代GC算法),标记清除、标记整理(压缩)(老年代GC算法),请你谈谈他的特点?

2、GC四大算法

引用计数法(了解即可)

特点:每个对象都有一个引用计数器,每当对象被引用一次,计数器就+1,如果引用失效,则计数器-1,如果为0,则GC可以清理;

缺点:

  • 计数器维护麻烦!
  • 循环引用无法处理!(互相引用的对象)

JVM 一般不采用这种方式

闲聊:现在一般使用可达性算法,GC Root(对不在引用的对象进行垃圾回收)

复制算法

在新生代中,就是使用复制算法的!

第一次GC后,Eden存活着一些对象,复制到s0区,Eden就是空的了

第二次GC后,Eden和s0都存活着一些对象,全部复制到s1

这时候s0和s1会进行位置互换,这是一个动态变化的区域,也就是所谓的from区和to区,记住谁空谁是to,该算法会保证s0和s1永远会有一个区是空的,它就是to区

这时候每次GC后存活的对象都会从Eden和from区复制到to区,原来的from区清空成为下一次GC的to区,自然现在有存活对象的to区就是from区了,这就是所谓的位置互换

1、一般普通GC 之后,差不多Eden几乎都是空的了!

2、每次存活的对象,都会被从 Eden 区和 from 区等复制到 to区,from 和 to 会发生一次位置交换;记住一个点就好,谁空谁是to,每当幸存一次,就会导致这个对象的年龄+1;如果这个年龄值大于15(默认值也是最大值,后面我们会讲解调整, ),就会进入老年代!

优点:

  • 没有标记和清除的过程!效率高!没有内存碎片!

  • Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!1%的对象就是复制到to区,推荐使用 ==复制算法==

缺点:to区永远是空的,需要浪费双倍的空间

标记清除算法

老年代一般使用这个,但是会和我们后面的整理压缩一起使用!

优点:不需要额外的空间

缺点:两次扫描(第1次扫描标记存活对象,第2次扫描清除没有标记的对象),老年代的空间一般比新生代的空间大一倍以上,要扫描整个空间,耗时较为严重,也会产生内存碎片,不连续!

优化一下算法:整理压缩,将活着对象滑动到一侧,保证空间的完整性,避免了内存碎片

发现与复制算法的优缺点刚好相反

标记清除压缩

减少了上面标记清除的缺点:没有内存碎片!但是耗时可能也较为严重!

那我们什么时候可以考虑使用这个算法呢?

在我们这个要使用算法的空间中,假设这个空间中很少,不经常发生GC,那么可以考虑使用这个算法!

分代清除算法

  • 内存效率:复制算法 > 标记清除算法 > 标记整理(时间复杂度!)

  • 内存整齐度:复制算法=标记整理>标记清除算法

  • 内存利用率:标记整理 = 标记清除算法 > 复制算法

从效率来说,复制算法最好,但是空间浪费较多!为了兼顾所有指标,标记整理会平滑一点,但是效率不尽人意!

难道就没有一种最优的算法吗?思考一下:

答案:没有!没有最好的,只有最合适的!=> 分代收集算法:不同的区域使用不同的算法!

新生代

相对于老年区,对象存活率低!Eden 区,对象存活率极低! 统计:99% 对象都会在使用一次之后,引用失效!

推荐使用 ==复制算法==

老年代

区域比较大,对象存活率较高!不经常发生GC

推荐使用标记清除压缩

天上飞的理念,都会有落地的是实现!

GC算法就是理论,垃圾回收器就是落地的实现

3、GC Root(根对象)

JVM 垃圾回收的时候如何确定垃圾,简单地说,就是不再被引用的对象!

Person person = null; // 不再被引用的对象

如果我们要进行垃圾回收,第一步:判断这个对象是否可以回收!

  • 引用计数法

    Java中,引用和对象都是有关联的,如果要操作对象,就要通过引用进行;

  • 可达性分析算法

什么是GC Root,根有4种

1、虚拟机栈中引用的对象!栈中不可能存在垃圾

2、类中静态属性引用的对象

3、方法区中的常量

4、本地方法栈中 Native 方法引用的对象!

public class GCRoots{
    // private byte[] array = new byte[100*1024*1024]; // 开辟内存空间!在堆里
    // private static GCRoots2 t2; // GC root;
    // private static final GCRoots3 t3 = new GCRoots3(); // GC root;
    
    // 引用远远不止于此,强引用,软引用,弱引用,虚引用! 四个类的使用!
    public static void m1(){
        GCRoots g1 = new GCRoots(); //GC root  根对象
        System.gc();
    }
    
    public static void main(String[] args){
        m1();
    }
}

5、OOM的六种场景

栈溢出

1、java.lang.StackOverflowError

// 方法不断递归调用自己,栈溢出
public static void main(String[] args) {
    a();
}

public static void a(){
    a();
}

堆内存溢出

2、java.lang.OutOfMemoryError: Java heap space

// -Xms8m -Xmx8m
public class OomDemo {
    public static void main(String[] args) {
        String str = "Coding";
        while (true){
            str += str + new Random(1111111111) +  new Random(1111111111);
        }
    }
}

/*
 * -Xmx8m -Xms8m -XX:+PrintGCDetails
 *
 * 分析GC日志:[GC (System.gc()) [PSYoungGen: 1344K->496K(2048K)] 1344K->536K(7680K), 0.0008573 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
 
 * 1、GC 类型  GC:普通的GC,Full GC :重GC
 * 2、1344K 执行 GC之前的大小
 * 3、496K  执行 GC之后的大小
 * 4、(2048K) young 的total大小
 * 5、0.0012643 secs 清理的时间
 * 6、user 总计GC所占用CPU的时间   sys OS调用等待的时间   real 应用暂停的时间
 *
 * GC :串行执行 STW(Stop The World理论,暂停整个世界,就是我在GC的时候,程序是不能运行的,直到我收完垃圾,程序才恢复运行)  
 并行执行   G1
*/
public class Demo02 {
    public static void main(String[] args) {
        System.gc(); // 手动唤醒GC(),但实际上它不会马上GC,它会等待cpu的调用
        String str = "ilovecoding";
        while (true){
            str += str + new Random().nextInt(999999999)
                    + new Random().nextInt(999999999);
        }
        // 最终出现问题:java.lang.OutOfMemoryError: Java heap space
    }
}

执行结果:

可以看到触发了多次GC(轻) 和 Full GC(重),每次Full GC,都会把新生区清零

GC回收时间过长

3、java.lang.OutOfMemoryError: GC overhead limit exceeded

GC 回收时间过长也会导致 OOM;

可能CPU占用率一直是100%,GC 但是没有什么效果!因为它已经清理不出什么了

// -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
public class OomDemo {
    public static void main(String[] args) throws Throwable {
        int i = 0;
        List<String> list = new ArrayList<String>();

        try {
            while (true){
                list.add(String.valueOf(++i).intern());
            }
        } catch (Throwable e) {  // 会抛出Error,Exception无法捕获,要使用父类 Throwable
            System.out.println("i=>"+i);
            e.printStackTrace();
            throw e;
        }
    }
}

执行:

可以发现触发了很多次Full GC 重GC,在循环到149171次后报出OOM异常GC overhead limit exceeded,就是GC垃圾来不及回收,就是OOM的增长速度比GC垃圾回收的速度还要快

基础缓冲区溢出

4、java.lang.OutOfMemoryError: Direct buffer memory 基础缓冲区的错误!

// -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
public class OomDemo {
  public static void main(String[] args) throws Throwable {
    System.out.println("配置的MaxDirectMemorySize"
                       +VM.maxDirectMemory()/(double)1024/1024+"MB");

    TimeUnit.SECONDS.sleep(2);
    // 故意破坏!我们只设置了本地内存最大5m,创建一个6m大的byteBuffer,那它就溢出了
    // ByteBuffer.allocate(); 分配JVM的堆内存,属于GC管辖
    // ByteBuffer.allocateDirect() ; // 分配本地OS内存,不属于GC管辖
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
    // java.lang.OutOfMemoryError: Direct buffer memory
  }
}

执行结果:

因为kafka是java写的,它出现过这样的案例内存溢出,看是否添加了参数-XX:+DisableExplicitGC标志将System.gc() 系统唤醒GC 禁用了,或者可以把MaxDirectMemorySize 基础缓冲区调大

文章链接:https://blog.csdn.net/dreamweaver_zhou/article/details/101269443

解决方案:

# kafka根目录
cd /opt/kafka/bin
vi kafka-run-class.sh
去掉参数-XX:+DisableExplicitGC
添加参数-XX:MaxDirectMemorySize

了解一下-XX:+DisableExplicitGC 与-XX:MaxDirectMemorySize

ByteBuffer有两种:

  • heap ByteBuffer -> -XX:Xmx 一种是heap ByteBuffer,该类对象分配在JVM的堆内存里面,直接由Java虚拟机负责垃圾回收

    JVM堆内存大小可以通过-Xmx来设置

  • direct ByteBuffer -> -XX:MaxDirectMemorySize 一种是direct ByteBuffer是通过JNI(Java Native Interface) 本地方法接口在JVM外分配内存的(JVM无法触碰到本地方法的)。通过jmap无法查看该快内存的使用情况。只能通过top来看它的内存使用情况。

    direct ByteBuffer可以通过-XX:MaxDirectMemorySize来设置,此参数的含义是当direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC。注意该值是有上限的,默认是64M,最大为sun.misc.VM.maxDirectMemory(),

在程序中可以获得-XX:MaxDirectMemorySize的设置的值。

import java.lang.reflect.Field;
import java.nio.ByteBuffer;

public class MaxDirectMemorySize {
  public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
    System.out.println("maxMemoryValue:"+sun.misc.VM.maxDirectMemory()); 

    System.out.println("================================");
    ByteBuffer buffer=ByteBuffer.allocateDirect(0);
    Class<?> c = Class.forName("java.nio.Bits");  
    Field maxMemory = c.getDeclaredField("maxMemory");  
    maxMemory.setAccessible(true);  
    synchronized (c) {  
      Long maxMemoryValue = (Long)maxMemory.get(null);  
      System.out.println("maxMemoryValue:"+maxMemoryValue);  
    }  
  }
}

执行结果:

注意

direct ByteBuffer通过full gc来回收内存的,direct ByteBuffer会自己检测情况而调用System.gc() 唤醒垃圾回收但不会马上执行等待CPU调度,但是如果参数中使用了-XX:+DisableExplicitGC那么就无法回收该快内存了,它会自动将System.gc()调用转换成一个空操作,就是应用中调用System.gc()会变成一个空操作,如果设置了就需要我们手动来回收内存。不回收内存就就出现内存溢出

无法创建本地线程

5、java.lang.OutOfMemoryError: unable to create native Thread

高并发 , unable to create native Thread这个错误更多的时候和平台有关!

1、应用创建的线程太多!

2、服务器不允许你创建这么多线程!

java 是调用本地方法去创建线程的

public class TDemo {
    public static void main(String[] args) {
      // 不断的创建线程,不用轻易在服务器上测试,会把内存
        for (int i = 1; ; i++) {
            System.out.println("i=>"+i);
            new Thread(()->{
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },""+i).start();
        }
    }
}

Exception in thread “main” java.lang.OutOfMemoryError: unable to create new native thread

1、服务器线程不够了,超过了限制,也会爆出OOM异常!

# 编译
javac TDemo.java
# 执行代码
java TDemo

执行结果:

场景:一个用户开一个线程处理,一个秒杀活动,好几万用户进来,咔叽,服务爆内存溢出,无法创建本地线程。

元空间溢出

6、java.lang.OutOfMemoryError: Metaspace

java8 之后使用元空间代替永久代;本地内存!

存储以下东西:

1、虚拟机加载类信息

2、常量池

3、静态变量

4、编译后的代码

模拟元空间溢出、不断的生成类即可!

import org.springframework.cglib.proxy.Enhancer;

// -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
public class OomDemo {
    
    static class OOMTest{}

    public static void main(String[] args) throws Throwable {
        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 o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return method.invoke(o,args);
                    }
                });
                enhancer.create();
            }
        } catch (Throwable e) {
            System.out.println("i=>"+i);
            e.printStackTrace();
        }
        
    }
}

执行结果:

6、垃圾回收器

GC算法(引用计数、复制、标记清除、标记整理算法)方法论,垃圾收集器就是对应的落地实现!

垃圾收集器就是对应的GC算法的落地实现!

串行垃圾回收器

串行(STW:Stop the World)单个GC线程,触发GC,所有在运行的线程都停下,等待GC完成

并行垃圾回收器

多个GC线程工作,也会导致 STW,其他线程都必须停下来),

并发垃圾回收器

在回收垃圾的同时,可以正常执行线程,并行处理,但是如果是单核CPU,只能交替执行!

G1垃圾回收器

与前面3个垃圾回收器本质的区别,将堆内存分割成不同的区域,然后并发的对其进行垃圾回收,在java9之后,默认使用G1垃圾回收器

前面3个垃圾回收器的特点:

  • 年轻代和老年代是各自独立的内存区域
  • 年轻代使用 eden+s0+s1 复制算法
  • 老年代使用标记清除压缩算法,必须扫描整个老年代的区域;

回收原理

垃圾回收器原则:尽可能少而快的执行GC为设计原则!

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

核心:将堆中内存区域(eden、s0、s1、old)打散,默认是2048块

流程:把每个区域中gc后的存活对象复制到空格子(区域),原区域清空释放内存,而且是并行的

使用

-XX:+UseG1GC

自定义垃圾回收时间

这个还不是它最大的亮点,它增加了一些参数,可以自定义垃圾回收的时间!

# 最大的GC停顿时间单位:毫秒,JVM尽可能的保证停顿小于这个时间!
-XX:MaxGCPauseMillis=100

优点:

  • 没有内存碎片
  • 可以精准空垃圾回收时间

我们所使用的这个JVM环境,在不断的被优化! 进化,会越来越简单和高效===> 算法的进阶!

程序 = 数据结构 + 算法!

如何选择垃圾回收器

java 的 gc 回收器主要有哪些?曾经有7种,现在6种

  • DefNew:默认的新生代 =【Serial 串行】
  • Tenured:老年代 =【Serial Old】
  • ParNew:并行新生代 = 【并行ParNew】
  • PSYoungGen:并行清除新生区 = 上图的【Parallel Scavcegn】
  • ParOldGen:并行老年区 = 上图的【Parallel Old】

查看java的默认垃圾回收器

# 查看默认的垃圾回收器,java8默认并行垃圾回收器,java9默认G1垃圾回收器
java -XX:+PrintCommandLineFlags -version

看一个jdk8的垃圾回收过程中

从图中看出jdk8 GC过程中使用的垃圾回收器是PSYoungGenParOldGen正好对应Parallel ScavcegnParallel Old

使用串行垃圾回收器

发现垃圾回收详细打印的是Tenured 和 DefNew

# 串行GC,新生区使用DefNew + 老年区 Tenured
-XX:+UseSerialGC  
# 并行GC,新生区使用ParNew + 老年区 Tenured
-XX:+UseParNewGC  
# JDK8默认的垃圾回收器,新生代PSYoungGen(复制算法) + 老年代ParOldGen(标记清除压缩算法)
-XX:+UseParallelGC 
# JDK9默认的垃圾回收器 
-XX:+UseG1GC 

Server / Client 模式

默认现在都是 Server 模式;Client几乎不会使用;

32位的Window操作系统,默认都是 Client 的 JVM 模式;

64位的默认都是 Server 模式;

选择合适的垃圾回收器

  • 1、单CPU,单机程序,内存小

    使用串行-XX:UseSerialGC

  • 2、多CPU,大的吞吐量、后台计算

    使用并行 -XX:+UseParallelGC

  • 3、多CPU,但是不希望有时间停顿,快速响应!

    使用 -XX:+UseParNewGC 或者 XX:+UseParallelGC

G1 优点

7、引用

强引用

默认的支持方式,假设出现了异常情况和OOM,只要是强引用的对象,都不会被回收!

强引用就是导致内存泄漏的原因之一!

//-Xms5m -Xmx5m -XX:+PrintGCDetails
public class Demo01  {
	public static void main(String[] args) {
		Object o1 = new Object(); // 这养定义的默认就是强引用
		Object o2 = o1;     // 真正的Object 对象信息存在堆中,o1和o2就是引用变量,存在JVM栈中
		o1=null;

		try {
			byte[] bytes = new byte[10 * 1024 * 1024];
		}catch (Throwable e){
			e.printStackTrace();
		}
		finally {
			System.out.println(o1); // null
			System.out.println(o2); // java.lang.Object@6ff3c5b5
		}
	}
}

设置vm参数,执行结果:

发现OOM内存溢出了,强引用的对象依然不会被回收

软引用

相对于强引用弱化了,如果内存充足的化,GC不会回收该对象,但是内存不足的情况下,就会回收该对象

public class Demo02 {
	public static void main(String[] args) {
		Object o1 = new Object(); // 这养定义的默认就是强引用
		SoftReference<Object> o2 = new SoftReference<>(o1);
		System.out.println(o1);
		System.out.println(o2.get()); // 得到引用的值

		o1=null;

		System.gc();

		System.out.println(o1); // null
		System.out.println(o2.get()); // java.lang.Object@6ff3c5b5
	}
}

执行结果:

修改为

//-Xms5m -Xmx5m -XX:+PrintGCDetails
public class Demo02 {
	public static void main(String[] args) {
		Object o1 = new Object(); // 这养定义的默认就是强引用
		SoftReference<Object> o2 = new SoftReference<>(o1);
		System.out.println(o1);
		System.out.println(o2.get()); // 得到引用的值

		o1=null;

		try {
			byte[] bytes = new byte[10 * 1024 * 1024];
		}catch (Throwable e){
			e.printStackTrace();
		}
		finally {
			System.out.println(o1); // null
			System.out.println(o2); // java.lang.Object@6ff3c5b5
		}
	}
}

执行结果:

内存不足了,gc会回收软引用的对象,

结论:使用软引用能节省堆内存

弱引用

不论内存是否充足,只要是GC,就会回收该对象

public class Demo03 {
	public static void main(String[] args) {
		Object o1 = new Object(); // 这养定义的默认就是强引用
		WeakReference<Object> o2 = new WeakReference<>(o1);
		System.out.println(o1);
		System.out.println(o2.get()); // 得到引用的值

		o1=null;
		System.gc();
		System.out.println(o1);
		System.out.println(o2.get()); // 得到引用的值
	}
}

执行结果:

软引用和弱引用的使用场景

假设现在有一个应用,读取大量的本地图片

1、如果每次都从硬盘上读取,影响性能

2、一次加载到内存,可能造成内存溢出

我们的思路:

1、使用一个HashMap保存图片的路径和内容

2、内存足够,不清理

3、内存不足够,清理加载到内存中的数据

HashMap<String,SoftReference<Pic>> stringSoftReferenceHashMap = new HashMap<>();

虚引用(幽灵引用)

主要作用:跟踪对象的垃圾回收状态,

public class Demo04 {
	public static void main(String[] args) throws InterruptedException {
		Object o1 = new Object();  // 这样定义的默认就是强引用
		// 虚引用 PhantomReference 需要结合队列使用
		ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
		PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);

		System.out.println(o1); // 
		System.out.println(phantomReference.get()); // null
		System.out.println(referenceQueue.poll()); // null
    o1=null;
		System.gc();

		TimeUnit.SECONDS.sleep(1000);

		System.out.println(o1);  // null
		System.out.println(phantomReference.get()); // null
    // 通知机制! 好比我们常用的消息队列,消息消费了,ACK
    // 通过队列检测哪些对象被清理了,可以处理一些善后动作
		System.out.println(referenceQueue.poll());
	}
}

8、JVM知识点总结

Post Directory