JAVA垃圾回收机制-史上最容易理解看这一篇就够了

目录

1. JDK构成

2. JAVA执行过程

2.1 第一步:编译阶段

2.2 第二步:运行阶段

3. JAVA内存管理

3.1 JAVA的内存模型

3.2 JVM1.7和1.8区别

3.3 JAVA堆

3.4 内存分配策略

4. JVM垃圾回收

4.1 哪些对象需要回收

4.2 什么时候回收

4.2.1 方法区的回收

4.2.2 强制垃圾回收

4.3 如何回收

4.3.1 标记-清除算法

4.3.2 复制算法

4.3.3 标记-整理算法

4.3.4 分代收集算法

5. JVM垃圾收集器

6. JVM调优常见参数


1. JDK构成

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第1张图片

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第2张图片

2. JAVA执行过程

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第3张图片

Java程序从源文件到程序运行要经过两大步骤:

1、编译源代码xxx.java文件生成字节码文件xxx.class
2、JVM类加载器把字节码文件加载到内存,JVM中的执行引擎找到入口方法main(),执行其中的方法

2.1 第一步:编译阶段

创建完源文件之后,程序先要被JVM中的java编译器进行编译为.class文件。
1、java编译一个类时,如果这个类所依赖的类还没有被编译,编译器会自动的先编译这个所依赖的类,然后引用。
2、如果java编译器在指定的目录下找不到该类所依赖的类的 .class文件或者 .java源文件,就会报
“Cant found sysbol”的异常错误。
3、编译后的字节码文件格式主要分为两部分:常量池和方法字节码。
常量池记录的是代码出现过的(常量、类名、成员变量等)以及符号引用(类引用、方法引用,成员变量引用等);
方法字节码中放的是各个方法的字节码。

下面是原java代码:

public class Test {   

    private String str = "hello";   

    void print(){   

        System.out.println(str);   

    }   

javap -v Test.class,经过反编译后部分信息如下

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第4张图片JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第5张图片

 一份字节码文件包含了魔数与 Class 文件的版本,常量池,访问标志等等信息,总之,字节码文件很简单很强大,它存放了这个类的各种信息:字段、方法、父类、实现的接口等各种信息。

2.2 第二步:运行阶段

JVM程序在第一次运行时主动使用类的时候,才会立即去加载。换言之,JVM并不是在运行时就会把所有使用到的类都加载到内存中,而是用到的时候,才加载进来,而且只加载一次!
java类运行的过程大概分为两个步骤:
(1)类的加载
(2)类的执行。
根据上面的程序,详解该程序运行的详细步骤:

1、类的加载

在类路径下找到编译好的 java 程序中得到 MainApp.class 字节码文件后,在命令行上敲 java MainApp,系统就会启动一个 JVM 进程,JVM进程从classpath路径下找到一个名为MainApp.class的二进制文件,将MainApp.class文件中的类信息加载到运行时数据区的方法区中,这一过程叫做类的加载。(只有类信息在方法区中,才能创建对象,使用类中的成员变量)

2、JVM 找到main方法的主函数入口

JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。

在主函数入口持有一个指向当前类常量池的指针,并且常量池中的第一项是发现是一个对Animal对象的符号引用。
并且main方法中第一条指令是Animal animal = new Animal(“super_yc”),就是让JVM创建一个Animal对象。
但是方法区中还没有Animal类的类信息,于是JVM就要马上的加载Animal类,将Animal类信息放入到方法区中。
于是JVM 以一个直接指向方法区 Animal类的指针替换了常量池中第一项的符号引用。

3、实例化对象

加载完Animal类的信息以后,JVM虚拟机就会在堆内存中为一个Animal类实例分配内存,然后调用其构造函数初始化Animal实例,
这个实例持有指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。(animal指向了Animal对象的引用会自动的放在栈中,字符串常量”super_yc”会自动的放在方法区的常量池中,对象会自动的放入堆区)。

4、程序运行

public class Main {

    public static void main(String[] args) {

        Animal animal = new Animal("super_yc");

        animal.printName();

    }

}

class Animal{

    private String name;

    public Animal(String name) {

        super();

        this.name = name;

    }

     

    public void printName(){

        System.out.println("Animal = " this.name);

    }

}

当使用animal.printName()的时候,JVM根据栈中animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息方法表,获得printName()函数的字节码地址,然后开始运行函数。

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第6张图片

3. JAVA内存管理

与C/C++相比,JAVA并不要求我们去人为编写代码进行内存回收和垃圾清理。JAVA提供了垃圾回收器(garbage collector)来自动检测对象的作用域,可自动把不再被使用的存储空间释放掉,也就是说,GC机制可以有效地防止内存泄露以及内存溢出。

JAVA 垃圾回收器的主要任务是:

  • 分配内存
  • 确保被引用对象的内存不被错误地回收
  • 回收不再被引用的对象的内存空间

凡事都有两面性。垃圾回收器在把程序员从释放内存的复杂工作中解放出来的同时,为了实现垃圾回收,garbage collector必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放之后还需要处理堆中的碎片, 这样做必定会增加JVM的负担。

为什么要了解JAVA的GC机制? 综上所述,除了作为一个程序员,精益求精是基本要求之外,深入了解GC机制让我们的代码更有效率,尤其是在构建大型程序时,GC直接影响着内存优化和运行速度。

3.1 JAVA的内存模型

根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第7张图片

各个内存区域的作用和特征如下:

名称

特征

作用

配置参数

异常

程序计数器 占用内存小,线程私有

字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

Java虚拟机栈 线程私有,使用连续的内存空间

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

-Xss OOM,stackOverFlow
本地方法栈 线程私有 区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。 OOM,stackOverFlow
线程共享

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

-Xmn

-Xms

-Xmx

OOM java heap space
方法区 线程共享 属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

XX:PermSize:

16M

-XX:MaxPermSize

64M

1.7 OOM PermGen space

1.8 OOM Metaspace

运行时常量池 方法区的一部分 属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。
直接内存 非虚拟机运行时数据区的部分

在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。

OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。

私有内存区伴随着线程的产生而产生,一旦线程中止,私有内存区也会自动消除,因此我们讨论的内存回收主要是针对共享内存区

3.2 JVM1.7和1.8区别

JAVA1.8不存在方法区,将方法区的实现给去掉了,而是在本地内存中新加入元数据区(元空间)。其实,移除永久代的工作从JDK 1.7就开始了。JDK 1.7中,存储在永久代的部分数据就已经转移到Java Heap或者Native Heap。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)、类的静态变量转移到了Java heap。

JDK1.8和JDK1.7的jvm最大的区别是:在1.8中方法区是由元空间(元数据区)来实现的,常量池移到堆中。元空间存储class 信息,类的信息,方法的定义,静态变量等,而常量池放到堆里存储。

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第8张图片

3.3 JAVA堆

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

现在垃圾收集器大部分都基于分带收集理论设计的,堆空间细分为:

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第9张图片

  •  jdk1.7 堆中的结构

在这里插入图片描述

  • jdk 1.8 堆中的结构

在这里插入图片描述

新生代:刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。

老年代:如果某个对象经历了几次垃圾回收之后还存活,就会被存放到老年代中。老年代的空间一般比新生代大。

不过实际运行中,Major GC会伴随至少一次 Minor GC,因此也不必过多纠结于到底是哪种GC(在有些资料中看到把full GC和Minor GC等价的说法)。

那么,当我们创建一个对象后,它会被放在堆内存的哪个部分呢?

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第10张图片

如果Major GC之后还是老年代不足,JVM会抛出内存不足的异常。

3.4 内存分配策略

(1)对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(前面篇章中有介绍过Minor GC)。但也有一种情况,在内存担保机制下,无法安置的对象会直接进到老年代。

(2)大对象直接进入老年代

大对象时指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。目的就是避免在Eden区及两个Survivor区之间发生大量的内存复制。

(3)长期存活的对象将进入老年代

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1 。对象在Survivor区中没经过一次Minor GC,年龄就加1岁,当年龄达到15岁(默认值),就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

接下来我们来回答JVM的分代年龄为什么是15?而不是16,20之类的呢?

真的不是为什么不能是其它数(除了15),着实是臣妾做不到啊!

事情是这样的,HotSpot虚拟机的对象头其中一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark word”。

例如,在32位的HotSpot虚拟机中,如果对象处于未被锁定的状态下,那么Mark Word的32bit空间中25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0 。

明白是什么原因了吗?对象的分代年龄占4位,也就是0000,最大值为1111也就是最大为15,而不可能为16,20之类的了。

(4)动态对象年龄判定

为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求兑现过的年龄必须达到了MaxTenuringThreshold才能晋升老年代。

满足如下条件之一,对象能晋升老年代:

  1. 对象的年龄达到了MaxTenuringThreshold(默认15)能晋升老年代。
  2. 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

很多文章都只是注意到了上面描述的情况(包括阿里中间件公众号发的一篇文章里也只是这么简单的介绍),但如果只是这么认识的话,会发现在实际的内存回收中有悖于此条规定。

举个小栗子,如对象年龄5的占34%,年龄6的占36%,年龄7的占30%,按那两个标准,对象是不能进入老年代的,但Survivor都已经100%了啊?

大家可以关注这个参数TargetSurvivorRatio,目标存活率,默认为50%。大致意思就是说年龄从小到大累加,如加入某个年龄段(如栗子中的年龄6)后,总占用超过Survivor空间TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即栗子中的年龄6,7对象)。动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。而且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。

(5)空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC 。

上面说的风险是什么呢?我们知道,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。

4. JVM垃圾回收

  • 哪些对象需要回收
  • 什么时候回收
  • 如何回收

4.1 哪些对象需要回收

垃圾收集器如何确定某个对象是“垃圾”?JAVA通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。

可作为 GC Roots 的对象:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

引用判断
在JDK1.2 之后,引用可分为以下4个

强引用:直接 new ,如 new Object(); 只要这类强引用还在,对象就不会回收
软引用:(SoftReference类)用来描述一些还有用但非必需的对象;当将来发生内存溢出之前,系统会把这些有软引用的对象列入回收范围中进行二次回收,如果这次回收还没有足够的内存,则内存溢出报异常;
弱引用:(WakeReference类) 被弱引用的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器开始工作时,无论内存是否足够,都会对这些对象进行回收。
虚引用(幽灵引用/幻影引用):(PhantomReference类) 一个虚引用的对象,它的唯一目的就是能在这个对象被收集器回收时收到一个系统通知,即一个对象是否有虚引用,完全不会对其生存时间构成影响。

4.2 什么时候回收

在可达性分析算法不可达的对象,也不一定"非死不可’,它会经历两次标记,一是当不可达 GC Roots 时,标记一次并筛选,筛选的条件是该对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或者已经执行过 finalize 方法时,则认为此对象会被回收。如下图

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第11张图片

 这里的执行,是指虚拟机会触发这个 finalize 方法,但并不会等待它结束,这是因为 finalize 方法执行缓慢,可能会导致 F-Queue其他对象处于等待,甚至是崩溃。
如果在执行 finalize 时,对象重新和其他对象关联上了,则成功拯救了自己
注意:任何一个对象的 finalize() 只会被系统调用一次,下次不会再执行。

4.2.1 方法区的回收

上面都是对 Java 堆进行回收,虽说在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。但方法区同样可以回收一些资源,方法区主要回收两个部分:废弃常量和无用的类。
废弃常量:当前系统没有任何一个 String 对象引用这个 “abc” 的常量池,也没有其他地方引用了这个字面量,这时可以判断这个常量是可以废弃回收的;其他常量池中的接口,字段的符号引用也以此类似
无用的类:无用类的回收,需要满足三个条件

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.class 对象没有任何地方被引用,也无法在任何地方通过反射访问该类的方法

当然,这里跟Java堆一样,也只是 "可以回收"了,是否对类进行回收,可以对虚拟机的参数进行设置,这里就不细讲了。

4.2.2 强制垃圾回收

程序只能控制一个对象何时不再被任何引用变量引用,但是不能控制何时被回收

强制系统进行垃圾回收——只是通知系统进行垃圾回收

强制垃圾回收有两种方式:

System.gc()

RunTime.getRunTime().gc()

public class test1 {

    public static void main(String[] args) {   

        for(int i=0;i<10;i++){

            new test1();

        }

    }

    @Override

    protected void finalize() throws Throwable {

        // TODO Auto-generated method stub

        System.out.println("调用finalize");

    }

}

运行上面代码,没有任何输出,系统并没有调用finalize()

public class test1 {

    public static void main(String[] args) {   

        for(int i=0;i<10;i++){

            new test1();

            //System.gc();

            Runtime.getRuntime().gc();

        }

    }

    @Override

    protected void finalize() throws Throwable {

        // TODO Auto-generated method stub

        System.out.println("调用finalize");

    }

}

加上 //System.gc(); Runtime.getRuntime().gc();任何一句会调用finalize(),但是只是建议回收机制回收,可能不会立即回收。

4.3 如何回收

4.3.1 标记-清除算法

顾名思义,标记-清除算法分为两个阶段,标记(mark)和清除(sweep)。

标记:遍历所有的GC Roots,然后将所有的GC Roots可达的对象标记为存活的对象。

清除:清除的过程将遍历所有堆中的对象,将没有标记的对象全部清除。

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第12张图片

 对上图中的黄色部分进行垃圾回收,回收后的截图如下所示: 

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第13张图片

从图中可知,进行标记清理后,可用内存增加,但是清除垃圾后的内存地址不连接,出现垃圾碎片。

缺点:

1、执行效率不稳定,如果Java堆中包含大量对象,而且大部分是需要被回收的,这时必须记性大量标记及清除动作,导致标记和清除两个过程执行效率都随对象数量增长而降低。

2、内存空间碎片化的问题,标记、清除后会产生大量的不连续内存碎片,空间碎片太可能会导致当以后需要分配大对象时无法找到足够的连续内存二不得不提前触发另一次垃圾收集动作。

4.3.2 复制算法

复制算法将内存分为两个区间,这两个区间是动态的,在任意一个时间点,所有分配的对象内存只能在其中一个区间(活动区间),另外一个区间就是空闲区间。
当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址一次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。这个时候空闲内存已经变成了活动区间,垃圾对象全部在原来的活动区间,清理掉垃圾对象,原活动区间就变成了空闲区间。

这种方式内存的代价太高,每次基本上都要浪费一半的内存。于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存是Eden区,其余是两块较小的内存区叫Survior区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。(java堆又分为新生代和老年代)。

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第14张图片

 优点:

1、很好地解决了“标记-清除”算法,内存布局混乱的缺点。

缺点:
1、浪费一半的内存。

2、假设对象存活率为100%,那么“标记-复制”算法的GC过程就是重复的把对象复制一遍,而且将所有的引用地址重置一遍。可以预见的复制所消耗的时间随着对象存活率达到一定程度将会变成灾难。所以“标记-复制”算法使用的场景是可以忍受只是用50%内存,对象存活率非常低

4.3.3 标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第15张图片

 优点:

1、弥补了“标记-清除”算法,内存区域分散的缺点
2、弥补了“标记-复制”算法内存减半的代价

缺点:

1、效率不高,对于“标记-清除”而言多了整理工作。

4.3.4 分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述三种算法整合了一下。具体如下:
根据各个年代的特点采取最适当的收集算法

1、在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
2、老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。

5. JVM垃圾收集器

收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。

不同的厂商会考虑使用不同的JVM,不同的JVM会使用不同的垃圾收集器。
(1).截止JDK 1.8,一共有7款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选用不同的垃圾收集器

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第16张图片

  • 串行垃圾回收器:它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程(“stop the world”),所以不适合服务器环境。
  • 并行垃圾回收器:多个垃圾收集线程并行工作,此时用户线程时暂停的,适用于科学计算/大数据处理首台处理等弱交互场景。
  • 并发垃圾回收器:用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用于对响应时间有要求的场景。

(2).不同厂商、不同版本的虚拟机实现差别很大。HotSpot 虚拟机在JDK7/8后所有收集器及组合(连线),如下图:

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第17张图片

 JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第18张图片

 ZGC(Java11,12新增的)

6. JVM调优常见参数

java启动参数共分为三类

其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容,非标准化的参数在将来的版本中可能会改变。
其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,这些都是不稳定的并且不推荐在生产环境中使用。将来可能会随时取消,需要慎重使用;上都被实现),而且如果在新版本有什么改动也不会发布通知。
对响应时间要求很高的系统来说,良好掌握JVM关于GC调优的参数是很重要的。比如一个高流量地延迟的电子交易平台,他要求的响应时间都是毫秒级的。要获得适合的参数组合需要大量的分析和不断的尝试,更依赖于交易系统的特性。

关于JVM选项的几点:
1) 布尔型参数选项:-XX:+ 打开, -XX:- 关闭。(比如-XX:+PrintGCDetails)
2) 数字型参数选项通过-XX:=设定。数字可以是m/M(兆字节),k/K(千字节),g/G(G字节)。
比如:32K表示32768字节。(比如-XX:HeapDumpPath=./java_pid.hprof)
3) 字符行参数选项通过-XX:=设定,通常用来指定一个文件,路径,或者一个命令列表。(比如-XX:+PrintGCDetails)
命令 java -help可以列出java 应用启动时标准选项(不同的JVM实现是不同的)。
java -X可以列出不标准的参数(这是JVM的扩展特性)。-X相关的选项不是标准的,被改变也不会通知。

不管是YGC还是Full GC,GC过程中都会对导致程序运行中中断,正确的选择不同的GC策略,调整JVM、GC的参数,可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java程序的工作效率。但是调整GC是以个极为复杂的过程,由于各个程序具备不同的特点,如:web和GUI程序就有很大区别(Web可以适当的停顿,但GUI停顿是客户无法接受的),而且由于跑在各个机器上的配置不同(主要cup个数,内存不同),所以使用的GC种类也会不同(如何选择见GC种类及如何选择)。本文将注重介绍JVM、GC的一些重要参数的设置来提高系统的性能。

JVM参数的含义 

参数名称 含义 默认值
-Xms 初始堆大小 物理内存的1/64(<1GB) 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
-Xmx 最大堆大小 物理内存的1/4(<1GB) 默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
-Xmn 年轻代大小(1.4or lator) 注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。
整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.
增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8
-XX:NewSize 设置年轻代大小(for 1.3/1.4)
-XX:MaxNewSize 年轻代最大值(for 1.3/1.4)
-XX:PermSize 设置持久代(perm gen)初始值 物理内存的1/64
-XX:MaxPermSize 设置持久代最大值 物理内存的1/4
-Xss 每个线程的堆栈大小 JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)
和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"”
-Xss is translated in a VM flag named ThreadStackSize”
一般设置这个值就可以了。
-XX:ThreadStackSize Thread Stack Size (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]
-XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatio Eden区与Survivor区的大小比值 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:LargePageSizeInBytes 内存页的大小不可设置过大, 会影响Perm的大小 =128m
-XX:+UseFastAccessorMethods 原始类型的快速优化
-XX:+DisableExplicitGC 关闭System.gc() 这个参数需要严格的测试
-XX:MaxTenuringThreshold 垃圾最大年龄 如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率
该参数只有在串行GC时才有效.
-XX:+AggressiveOpts 加快编译
-XX:+UseBiasedLocking 锁机制的性能改善
-Xnoclassgc 禁用垃圾回收
-XX:SoftRefLRUPolicyMSPerMB 每兆堆空闲空间中SoftReference的存活时间 1s softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap
-XX:PretenureSizeThreshold 对象超过多大是直接在旧生代分配 0 单位字节 新生代采用Parallel Scavenge GC时无效
另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.
-XX:TLABWasteTargetPercent TLAB占eden区的百分比 1%
-XX:+CollectGen0First FullGC时是否先YGC false

并行收集器相关参数

-XX:+UseParallelGC Full GC采用parallel MSC
(此项待验证)

选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证)

-XX:+UseParNewGC 设置年轻代为并行收集 可与CMS收集同时使用
JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值
-XX:ParallelGCThreads 并行收集器的线程数 此值最好配置与处理器数目相等 同样适用于CMS
-XX:+UseParallelOldGC 年老代垃圾收集方式为并行收集(Parallel Compacting) 这个是JAVA 6出现的参数选项
-XX:MaxGCPauseMillis 每次年轻代垃圾回收的最长时间(最大暂停时间) 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值.
-XX:+UseAdaptiveSizePolicy 自动选择年轻代区大小和相应的Survivor区比例 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开.
-XX:GCTimeRatio 设置垃圾回收时间占程序运行时间的百分比 公式为1/(1+n)
-XX:+ScavengeBeforeFullGC Full GC前调用YGC true Do young generation GC prior to a full GC. (Introduced in 1.4.1.)

CMS相关参数

-XX:+UseConcMarkSweepGC 使用CMS内存收集 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置.???
-XX:+AggressiveHeap 试图是使用大量的物理内存
长时间大内存使用的优化,能检查计算资源(内存, 处理器数量)
至少需要256MB内存
大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)
-XX:CMSFullGCsBeforeCompaction 多少次后进行内存压缩 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理.
-XX:+CMSParallelRemarkEnabled 降低标记停顿
-XX+UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的压缩 CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。
可能会影响性能,但是可以消除碎片
-XX:+UseCMSInitiatingOccupancyOnly 使用手动定义初始化定义开始CMS收集 禁止hostspot自行触发CMS GC
-XX:CMSInitiatingOccupancyFraction=70 使用cms作为垃圾回收
使用70%后开始CMS收集
92 为了保证不出现promotion failed(见下面介绍)错误,该值的设置需要满足以下公式CMSInitiatingOccupancyFraction计算公式
-XX:CMSInitiatingPermOccupancyFraction 设置Perm Gen使用到达多少比率时触发 92
-XX:+CMSIncrementalMode 设置为增量模式 用于单CPU情况
-XX:+CMSClassUnloadingEnabled

辅助信息

-XX:+PrintGC

输出形式:

[GC 118250K->113543K(130112K), 0.0094143 secs]
[Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails

输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps
-XX:+PrintGC:PrintGCTimeStamps 可与-XX:+PrintGC -XX:+PrintGCDetails混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]
-XX:+PrintGCApplicationStoppedTime 打印垃圾回收期间程序暂停的时间.可与上面混合使用 输出形式:Total time for which application threads were stopped: 0.0468229 seconds
-XX:+PrintGCApplicationConcurrentTime 打印每次垃圾回收前,程序未中断的执行时间.可与上面混合使用 输出形式:Application time: 0.5291524 seconds
-XX:+PrintHeapAtGC 打印GC前后的详细堆栈信息
-Xloggc:filename 把相关日志信息记录到文件以便分析.
与上面几个配合使用

-XX:+PrintClassHistogram

garbage collects before printing the histogram.
-XX:+PrintTLAB 查看TLAB空间的使用情况
XX:+PrintTenuringDistribution 查看每次minor GC后新的存活周期的阈值

Desired survivor size 1048576 bytes, new threshold 7 (max 15)
new threshold 7即标识新的存活周期的阈值为7。

GC性能方面的考虑

       对于GC的性能主要有2个方面的指标:吞吐量throughput(工作时间不算gc的时间占总的时间比)和暂停pause(gc发生时app对外显示的无法响应)。

1. Total Heap

       默认情况下,vm会增加/减少heap大小以维持free space在整个vm中占的比例,这个比例由MinHeapFreeRatio和MaxHeapFreeRatio指定。

一般而言,server端的app会有以下规则:

  • 对vm分配尽可能多的memory;
  • 将Xms和Xmx设为一样的值。如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。
  • 处理器核数增加,内存也跟着增大。

2. The Young Generation

       另外一个对于app流畅性运行影响的因素是young generation的大小。young generation越大,minor collection越少;但是在固定heap size情况下,更大的young generation就意味着小的tenured generation,就意味着更多的major collection(major collection会引发minor collection)。

       NewRatio反映的是young和tenured generation的大小比例。NewSize和MaxNewSize反映的是young generation大小的下限和上限,将这两个值设为一样就固定了young generation的大小(同Xms和Xmx设为一样)。

       如果希望,SurvivorRatio也可以优化survivor的大小,不过这对于性能的影响不是很大。SurvivorRatio是eden和survior大小比例。

一般而言,server端的app会有以下规则:

  • 首先决定能分配给vm的最大的heap size,然后设定最佳的young generation的大小;
  • 如果heap size固定后,增加young generation的大小意味着减小tenured generation大小。让tenured generation在任何时候够大,能够容纳所有live的data(留10%-20%的空余)。

 有兴趣可以关注我的微信公众号“自动化测试全栈”,微信号:QAlife,学习更多自动化测试技术。

JAVA垃圾回收机制-史上最容易理解看这一篇就够了_第19张图片

也可加入我们的自动化测试技术交流群,QQ群号码:301079813

主要探讨loadrunner/JMeter测试、Selenium/RobotFramework/Appium自动化测试、接口自动化测试,测试工具等测试技术,让我们来这里分享经验、交流技术、结交朋友、拓展视野、一起奋斗!

你可能感兴趣的:(JVM,jvm)