天天看点

深入理解JVM(二)

GC详解

GC的作用域

GC的作用域如下图所示。

深入理解JVM(二)

关于垃圾回收,只需要记住

分代回收算法

,即不同的区域使用不同的算法。

不同区域的GC频率也不一样:

  • 年轻代:GC频繁区域。
  • 老年代:GC次数较少。
  • 永久代:不会产生GC。
一个对象的历程

一个对象的历程的如下图所示。

深入理解JVM(二)

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

新生代

进行GC。

GC有两种类型:

  • 普通GC(GC):只针对新生代 。
  • 全局GC(Full GC):主要是针对老年代,偶尔伴随新生代。

GC的四大算法

引用计数法

引用计数法只需要了解即可,JVM 一般不采用这种方式进行GC。它的原理如下图所示。

深入理解JVM(二)

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

缺点:

  • 计数器维护比较麻烦。
  • 循环引用无法处理。
复制算法

年轻代中GC使用的就是复制算法。

深入理解JVM(二)

原理:

  • 一般普通GC之后,Eden区几乎都是空的了。
  • 每次存活的对象,都会被从from区和Eden区等复制到to区,from区和to区会发生一次交换,每当GC后幸存一次,就会导致这个对象的年龄+1,如果这个年龄值大于15(默认GC次数,可以修改),就会进入养老区。记住一个点就好,谁空谁是to。复制算法的原理如下图所示。
    深入理解JVM(二)

优点:

  • 没有标记和清除的过程,效率高。
  • 不会产生内存碎片。

由于Eden区对象存活率极低!,据统计99% 对象都会在使用一次之后引用失效,因此在该区中推荐使用复制算法。

标记清除算法

老年代一般使用这个GC算法,但是会和后面的标记整理压缩算法一起使用。其原理如下图所示。

深入理解JVM(二)

原理:

  • 先扫描一次,对存活的对象进行标记。
  • 再次扫描,回收没有被标记的对象。

优点:不需要额外的空间。

缺点:

  • 需要两次扫描,耗时严重。
  • 会产生内存碎片,导致内存空间不连续。
标记清除压缩算法

标记清除压缩算法,也叫标记整理算法,该算法是在标记清除算法的基础上进行改进的算法,解决了标记清除算法会产生内存碎片的问题,但是相应的耗时可能也较为严重。其原理如下图所示。

深入理解JVM(二)

原理:

  • 先扫描一次,对存活的对象进行标记。
  • 第二次扫描,回收没有被标记的对象。
  • 压缩,再次扫描,将活着的对象滑动到一侧,这样就能让空出的内存空间是连续的。

当一个空间很少发生GC,可以考虑使用此算法。

GC算法小结

内存效率:复制算法>标记清除算法>标记整理算法

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

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

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

实际上,所有的算法,无非就是以空间换时间或者以时间换空间。没有最好的算法,只有最合适的算法。所以上面说的分代收集算法,并不是指一种算法,而是在不同的区域使用不同的算法。

综上所述:

  • 年轻代,相对于老年代,对象存活率较低,特别是在Eden区,对象存活率极低,99% 对象都会在使用一次之后引用失效,因此推荐使用复制算法。
  • 老年代,区域比较大,对象存活率较高,推荐使用标记清除压缩算法。

JVM 垃圾回收的时候如何确定垃圾?GC Roots又是什么?

什么是垃圾?简单的说,就是不再被引用的对象。,如:

Object object=null;
           

如果我们要进行垃圾回收,首先必须判断这个对象是否可以回收。

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

可达性分析算法

可达性分析算法,简单来说就是通过从GC Root这个对象开始一层层往下遍历,能够遍历到的对象就是可达的,不能被遍历到的对象就是不可达的,不可达对象就是要被回收的垃圾。其原理如下图所示。

深入理解JVM(二)

一切都是从 GC Root 这个对象开始遍历的,只要在这里面的就不是垃圾,反之就是垃圾。

什么是GC Root?
  • 虚拟机栈中引用的对象。
  • 类中静态属性引用的对象。
  • 方法区中的常量。
  • 本地方法栈中Native方法引用的对象。

如下代码所示:

public class GCRoots{
		
    private byte[] array = new byte[100*1024*1024]; // GC root,开辟内空间!
    private static GCRoots2 t2; // GC root;
    private static final GCRoots3 t3 = new GCRoots3(); // GC root;
    
    public static void m1(){
        GCRoots g1 = new GCRoots(); //GCroot
        System.gc();
    }
    
    public static void main(String[] args){
        m1();
    }
}
           

总结:

  • 对于数组,如果只是在类成员中进行定义而没有声明数组大小,不是GC Root;如果已经声明了数组大小,则是GC Root,因为此时它已经开辟了内存空间。
  • 对于静态成员对象属性,只要定义了,不管初始化值是null还是new出了对象,都是GC Root。

JVM常用参数

JVM只有三种参数类型:

标配参数

X参数

XX参数

标配参数

标配参数是指在JVM各个版本之间都非常稳定,很少有变化的参数。如:

java -version
java -help
java -showversion
           
深入理解JVM(二)
X参数

X参数只要了解即可,如下X参数用于修改JVM的运行模式。

-Xint          # 解释执行
-Xcomp         # 第一次使用就编译成本地的代码
-Xmixed        # 混合模式(Java默认)
           
深入理解JVM(二)
XX参数之布尔型(重点)

-XX: +或者-某个属性值

, + 代表开启某个功能,- 表示关闭了某个功能。

如以下代码让程序睡眠21亿秒:

package com.wunian.gc;

//jps -l 查看堆栈信息,获得当前java程序端口号
//jinfo -flag PrintGCDetails  5360  查看运行中的java程序,某项虚拟机参数是否开启(输出+号表示开启,-表示关闭)
//jinfo -flag MetaspaceSize  6312 查看元空间大小
//jinfo -flag MaxTenuringThreshold  6312 查看控制新生代中对象需要经历多少次GC晋升到老年代,默认为15
//jinfo -flags 6312 查看指定端口的所有信息
//java -XX:+PrintFlagsInitial 查看java环境初始默认值
public class GCDemo {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Hello World");
        Thread.sleep(Integer.MAX_VALUE);
    }
}
           

程序运行后,打开DOS窗口,执行

jps -l

命令查看堆栈信息。得到当前程序运行的端口号,再执行

jinfo -flag PrintGCDetails 端口号

命令来查看刚刚运行的Java程序的PrintGCDetails参数是否开启,如果输出参数-XX:后面是-开头,表示没有开启,+开头表示已经开启了。

深入理解JVM(二)

关闭程序,在IDEA配置中添加JVM参数

-XX:+PrintGCDetails

,再次启动程序,使用刚才的命令再次查看一下PrintGCDetails参数是否开启,输出参数-XX:后面是+开头,说明已经开启了该参数。

深入理解JVM(二)
XX参数之key=value型

设置元空间大小为128M:

-XX:MetaspaceSize=128m

执行

jinfo -flag MetaspaceSize 端口号

可以查看指定程序的元空间大小。

深入理解JVM(二)

设置进入老年区的存活年限(默认是15年):

-XX:MaxTenuringThreshold=15

该参数主要是控制新生代需要经历多少次GC晋升到老年代中的最大阈值。在JVM中用4个bit存储(放在对象头中),所以其最大值是15。

执行

jinfo -flag MaxTenuringThreshold

可以查看进入老年区的存活年限。

深入理解JVM(二)

查看某个端口的所有信息的默认值:

jinfo -flags 端口号

-XX:+UseParallelGC

表示默认使用的是并行GC回收器。

深入理解JVM(二)

经典面试题:

-Xms

,

-Xmx

,是XX参数还是X参数?

1.

-Xms

表示设置初始堆的大小,等价于:

-XX:InitialHeapSize

2.

-Xmx

表示设置最大堆的大小,等价于:

-XX:MaxHeapSize

因此,

-Xms

,

-Xmx

是XX参数,这种写法只不过是语法糖,方便书写。一般最常用的东西都是有语法糖的。

初始的默认值

查看Java 环境初始默认值:

-XX:+PrintFlagsInitial

,只要在这里面显示的值,都可以手动赋值,但是不建议修改,了解即可。

深入理解JVM(二)

=

表示是默认值。

:=

表示值被修改过。

查看被修改过的值:

java -XX:+PrintFlagsFinal -Xss128k GCDemo   # 查看被修改过的值!启动的时候判断
           
深入理解JVM(二)

查看用户修改过的配置的XX选项:

java -XX:+PrintCommandLineFlags -version

深入理解JVM(二)

常用的JVM调优参数

  • -Xms

    :设置初始堆的大小。
  • -Xmx

    :设置最大堆的大小。
  • -Xss

    :线程栈大小设置,默认为512k~1024k。
  • -Xmn

    : 设置年轻代的大小,一般不用改动。
  • -XX:MetaspsaceSize

    :设置元空间的大小,这个在本地内存中。
  • -XX:+PrintGCDetails

    :输出详细的垃圾回收信息。
  • -XX:SurvivorRatio

    :设置新生代中的 Eden/s0/s1空间的比例。例如:

    uintx SurvivorRatio = 8

    表示Eden:s0:s1 = 8:1:1

    uintx SurvivorRatio = 4

    表示Eden:s0:s1 = 4:1:1
  • -XX:NewRatio

    :设置年轻代与老年代的占比。例如:

    NewRatio = 2

    表示新生代:老年代=1:2,默认新生代整个堆的1/3。

    NewRatio = 4

    表示新生代:老年代=1:4,默认新生代整个堆的1/5。
  • -XX:MaxTenuringThreshold

    :进入老年区的存活阈值。例如:

    MaxTenuringThreshold = 15

    表示GC15次后存活的对象进入老年区。

常见的几种OOM

java.lang.StackOverflowError

栈溢出,最常见的OOM之一,方法调用自身,示例代码如下:

package com.wunian.gc;
/**
 * 栈溢出 java.lang.StackOverflowError
 * 方法调用自身
 */
public class OOMDemo {

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

    public static void a(){
        a();
    }
}
           
java.lang.OutOfMemoryError: Java heap space

堆溢出,最常见的OOM之一,字符串无限拼接,示例代码如下:

package com.wunian.gc;

import java.util.Random;
/**
 * 堆溢出  java.lang.OutOfMemoryError: Java heap space
 * -Xms10m -Xmx10m
 */
public class OOMDemo2 {

    public static void main(String[] args) {
        String str="coding";
        while(true){
            str+=str+new Random(1111111111)+new Random(1111111111);
        }
    }
}
           
java.lang.OutOfMemoryError: GC overhead limit exceeded

GC回收时间过长(次数过多)也会导致 OOM,可能CPU占用率一直是100%,频繁GC但是没有什么效果。示例代码如下:

package com.wunian.gc;

import java.util.ArrayList;
import java.util.List;
/**
 *  GC回收时间(次数)过长也会导致 OOM; java.lang.OutOfMemoryError: GC overhead limit exceeded
 *   -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
 */
public class OOMDemo3 {

    public static void main(String[] args) {
        int i=0;
        List<String> list =new ArrayList<>();
        try {
            while(true){
                list.add(String.valueOf(++i).intern());
                /**
                 *   String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。
                 * 当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,
                 * 若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,
                 * 然后返回这个String在常量池中的引用。
                 */
            }
        } catch (Exception e) {
            System.out.println("i=>"+i);
            e.printStackTrace();
            throw e;
        }
    }
}
           
java.lang.OutOfMemoryError: Direct buffer memory

基础缓冲区错误,使用NIO方法分配的本地内存超出了JVM参数设置的最大堆外内存。设置最大Java堆外内存大小:

-XX:MaxDirectMemorySize=5m

,示例代码如下:

import sun.misc.VM;

import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
/**
 *  基础缓冲区的错误! java.lang.OutOfMemoryError: Direct buffer memory
 *  -XX:MaxDirectMemorySize可以设置java堆外内存的峰值
 * -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
 */
public class OOMDemo4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("配置的MaxDirectMemorySize"+ VM.maxDirectMemory()/(double)1024/1024+"MB");
        TimeUnit.SECONDS.sleep(2);
        //故意破坏
        //ByteBuffer.allocate();分配 JVM的堆内存,属于GC管辖
        //ByteBuffer.allocateDirect();//分配本地OS内存,不属于GC管辖
        分配了6M内存,但是jvm参数设置了最大堆外内存是5M
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
    }
}
           
java.lang.OutOfMemoryError: unable to create new native thread

高并发环境下,此错误更多的时候和平台有关,出现此错误的可能原因有:

  • 应用创建的线程太多。
  • 服务器不允许你创建这么多线程。

示例代码如下:

package com.wunian.gc;

/**
 * 服务器线程不够了,超过了限制,也会爆出OOM异常
 * java.lang.OutOfMemoryError: unable to create new native thread
 */
public class OOMDemo5 {
        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();
        }
    }
}
           
java.lang.OutOfMemoryError: Metaspace

Java8之后使用元空间代替永久代,使用的是本地内存。元空间主要用于存储:

  • 虚拟机加载类信息
  • 常量池
  • 静态变量
  • 编译后的代码

要模拟元空间溢出,只需要不断的生成类即可,这里需要用到Spring中的Enhancer类,示例代码如下:

package com.wunian.gc;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;
/**
 * 元空间溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
 */
public class OOMDemo6 {

    static class OOMTest{}

    public static void main(String[] args) {
        int i=0;//模拟计数器
        try {
            //不断的加载对象!底层使用Spring的cglib动态代理
            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 (Exception e) {
            System.out.println("i=>"+i);
            e.printStackTrace();
        }
    }
}
           

深入理解垃圾回收器

GC算法如引用计数算法、复制算法、标记清除算法、标记整理算法都是方法论,垃圾回收器就是这些算法对应的落地的实现。

四种垃圾回收器

1、串行垃圾回收器,单线程工作,执行GC时会停止所有的线程直到GC结束(STW:Stop the World)。其原理如下图所示。

深入理解JVM(二)

2、并行垃圾回收器,多线程工作,也会导致STW。其原理如下图所示。

深入理解JVM(二)

3、并发垃圾回收器,在回收垃圾的同时,可以正常执行线程,并行处理,但是如果是单核CPU,只能交替执行。其原理如下图所示。

深入理解JVM(二)

4、G1垃圾回收器,将堆内存分割成不同的区域,然后并发的对其进行垃圾回收。Java9以后为默认的垃圾回收器。其原理如下图所示。

深入理解JVM(二)

查看默认的垃圾回收器:

java -XX:+PrintCommandLineFlags -version

深入理解JVM(二)
Java的垃圾回收器有哪些?

Java曾经由7种垃圾回收器,现在有6种。主要垃圾回收器的位置分布和关系如下图所示。

深入理解JVM(二)

上图中,红色箭头表示新生区中使用了对应的垃圾回收器,在老年区只能使用对应箭头指向的垃圾回收器。蓝色箭头表示曾经的垃圾回收器有过的对应关系。

6种垃圾回收器名称分别是:

  • DefNew : 默认的新一代 【Serial 串行】
  • Tenured : 老年代 【Serial Old】
  • ParNew : 并行新一代 【并行ParNew】
  • PSYoungGen : 并行清除年轻代 【Parallel Scavcegn】
  • ParOldGen: 并行老年区
    深入理解JVM(二)
JVM的Server/Client模式

现在的JVM默认都是Server模式,Client几乎不会使用。以前32位的Windows操作系统,默认都是Client的 JVM 模式,64位的默认都是 Server模式。

垃圾回收器之间的组合关系

上述6种垃圾回收器都是组合使用的,新生区使用了某种垃圾回收器,养老区会使用与之对应的垃圾回收器,并不是自由搭配的。如下图所示。

深入理解JVM(二)
如何选择垃圾回收器

1、单核CPU,单机程序,内存小。选择

-XX:UseSerialGC

2、多核CPU,吞吐量大,后台计算。选择

XX:+UseParallelGC

3、多核CPU,不希望有时间停顿,能够快速响应。选择

-XX:+UseParNewGC

或者

XX:+UseParallelGC

##G1垃圾回收器

以往垃圾回收器的特点

1、年轻代和老年代是各自独立的内存区域。

2、年轻代使用Eden+s0+s1复制算法。

3、老年代垃圾收集必须扫描整个老年代的区域。

4、垃圾回收器原则:尽可能少而快的执行GC。

G1垃圾回收器的原理

G1(Garbage-First)垃圾回收器 ,是面向服务器端的应用的回收器。其原理如下图所示。

深入理解JVM(二)

原理:将堆中的内存区域打散,默认分成2048块。不同的区间可以并行处理垃圾,在GC过程中,幸存的对象会复制到另一个空闲分区中,由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩)。

使用G1垃圾回收器:

-XX:+UseG1GC

G1垃圾回收器最大的亮点是可以自定义垃圾回收的时间。设置最大的GC停顿时间(单位:毫秒):

XX:MaxGCPauseMillis=100

,JVM会尽可能的保证停顿小于这个时间。

G1垃圾回收器的优点
  • 没有内存碎片。
  • 可以精准的控制垃圾回收时间。

强引用、软引用,弱引用和虚引用

主要学习三个引用类:

SoftReference

WeakReference

PhantomReference

深入理解JVM(二)
强引用

假设出现了异常或OOM,只要是强引用的对象,都不会被回收。强引用就是导致内存泄露的原因之一。

package com.wunian.ref;
/**
 * 强引用
 * -XX:+PrintGCDetails -Xms5m -Xmx5m
 */
public class StrongRefDemo {

    public static void main(String[] args) {
        Object o1=new Object();//这样定义的默认就是强引用
        Object o2=o1;
        o1=null;

        System.gc();
        System.out.println(o1);//null
        System.out.println(o2);//[email protected]
    }
}
           
软引用

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

package com.wunian.ref;

import java.lang.ref.SoftReference;
/**
 * 软引用
 *  -XX:+PrintGCDetails -Xms5m -Xmx5m
 */
public class SoftRefDemo {

    public static void main(String[] args) {
        Object o1=new Object();//这样定义的默认就是强引用
        //Object o2=o1;
        SoftReference<Object> o2=new SoftReference<>(o1);//软引用
        System.out.println(o1);//[email protected]
        System.out.println(o2.get());//得到引用的值  [email protected]
        o1=null;
        try {
            byte[] bytes=new byte[10*1024*1024];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(o1);//null
            System.out.println(o2.get());//null  //由于堆内存不足被回收
        }
        //System.gc();
    }
}
           
弱引用

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

package com.wunian.ref;

import java.lang.ref.WeakReference;

/**
 * 弱引用
 * -XX:+PrintGCDetails -Xms5m -Xmx5m
 */
public class WeakRefDemo {

    public static void main(String[] args) {
        Object o1=new Object();//这样定义的默认就是强引用
        WeakReference<Object> o2 = new WeakReference<>(o1);

        System.out.println(o1);//[email protected]
        System.out.println(o2.get());//得到引用的值  [email protected]

        o1=null;
        System.gc();

        System.out.println(o1);//null
        System.out.println(o2.get());//null
    }
}
           
软引用、弱引用的使用场景

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

1、如果每次读取图片都要从硬盘中读取,影响性能。

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

我们的思路:

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

2、内存足够,不清理。

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

虚引用

虚就是虚无,虚引用就是没有这个引用。虚引用需要结合队列使用,其主要作用是跟踪对象的垃圾回收状态。

package com.wunian.ref;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.TimeUnit;
/**
 * 虚引用
 */
public class PhantomRefDemo {

    public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        //虚引用需要结合队列使用
        ReferenceQueue<Object> referenceQueue=new ReferenceQueue<>();
        PhantomReference<Object> objectPhantomReference=new PhantomReference<>(o1,referenceQueue);

        System.out.println(o1);//[email protected]
        System.out.println(objectPhantomReference.get());//null
        System.out.println(referenceQueue.poll());//null

        o1=null;
        System.gc();
        TimeUnit.SECONDS.sleep(1);

        System.out.println(o1);//null
        System.out.println(objectPhantomReference.get());//null
        //这好比是一个垃圾桶,通过队列来检测哪些对象被清理了,可以处理一些善后工作
        System.out.println(referenceQueue.poll());//[email protected]
    }
}