java堆以及垃圾回收

  |   0 评论   |   0 浏览

java堆

概念

Java堆被所有线程共享,在Java虚拟机启动时创建。是虚拟机管理最大的一块内存。

Java堆是垃圾回收的主要区域,而且主要采用分代回收算法。堆进一步划分主要是为了更好的回收内存或更快的分配内存。

存储内容

Java虚拟机规范的描是:所有的对象实例以及数组都要在堆上分配。

不过随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

存储方式

堆内存空间在物理上可以不连续,逻辑上连续即可。

堆内存划分

  • 新生代

    • Eden空间[伊甸园]
    • From Survivor空间
    • To Survivor空间
  • 老年代

堆大小 = 新生代 + 老年代。

堆的大小可通过参数 –Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定。

  • 其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。默认的,Edem : from : to = 8 : 1 : 1 。(可以通过参数 –XX:SurvivorRatio 来设定 。即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
  • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
  • 新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

image.png

image.png

对象创建

image.png

内存的分配原则

序号介绍
1优先在Eden分配,如果Eden空间不足虚拟机会进行一次MonitoGC
2大对象直接进入老年代,大对象一般指的事很长的字符串或数组
3长期存活的对象进入老年代,每个对象都有一个age,当age达到设定的年龄的时候就会进入老年代,默认是15岁。

内存分配方式

内存分配的方法有两种:指针碰撞(Bump the Pointer)和空闲列表(Free List)

分配方法说明收集器
指针碰撞内存地址是连续的Serial和ParNew收集器
空间列表内存地址不连续CMS收集器和Mark-Sweep收集器

image.png

内存分配安全问题

在分配内存的同时,存在线程安全的问题,即虚拟机给A线程分配内存过程中,指针未修改,B线程可能同时使用了同样一块内存。

在JVM中有两种解决办法:

  1. CAS,比较和交换(Compare And Swap): CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟 机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  2. TLAB,本地线程分配缓冲(Thread Local Allocation Buffer即TLAB): 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

对象的内存布局

对象在内存中存储的布局可以分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。

  1. 对象头

对象头包括两部分信息:

一部分是用于存储对象自身的运行数据,如 哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的 锁,偏向线程ID,偏向时间戳 等。

另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪一个类的实例。当对象是一个java数组的时候,那么对象头还必须有一块用于记录数组长度的数据,因此虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。

  1. 实例数据

    存储的是对象真正有效的信息。

  2. 对齐填充

    这部分并不是必须要存在的,没有特别的含义,在jvm中对象的大小必须是8字节的整数倍,而对象头也是8字节的倍数,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象访问方式

方式优点
句柄稳定,对象被移动只要修改句柄中的地址
直接指针访问速度快,节省了一次指针定位的开销

image.png

image.png

数组的内存分析

一维数组

image.png

int[] arr1 = new int[3];

先把 arr1 压进栈,然后在堆空间中开辟一个空间,并把值初始化为0(arr1为引用变量,但是内部数据是int类型,默认值为 0),最后把 开辟的堆空间地址 赋值给arr1

int[] arr2 = arr1;

把 arr1 中的 地址 赋值给 arr2,此时 arr2 和 arr1 指向同一块空间。

arr2[0] = 20;

此时,arr1[0] 值为 20。

二维数组

image.png

int[][] array = new int[3][];

这条语句会先把 array 压栈,然后在堆中开辟一个空间,初始值为 null(array为引用变量,

第一维同样是引用类型),最后把开辟的堆空间地址赋值给 array。

array[0][] = new int[1]

这条语句会在堆空间中开辟一个 只有一个 int 类型大小的空间,并初始化为 0 ,然后把自己

的地址赋值给array[0][]

array[1][] = new int[2]; 
array[2][] = new int[3];

这两条语句和上一条意义一样,就不再做解释

程序计数器

作用:

程序计数器(Program Counter Register),也叫PC寄存器,是一块较小的内存空间,它可以看作是当前线程所执行的字节码指令的行号指示器。字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(针对多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

存储的数据

如果一个线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;

如果正在执行的是一个Native方法,这个计数器的值则为空。

异常

此内存区域是唯一一个在Java的虚拟机规范中没有规定任何OutOfMemoryError异常情况的区域。

Java虚拟机栈

虚拟机栈也是线程私有,而且生命周期与线程相同,每个Java方法在执行的时候都会创建一个栈帧(Stack Frame)。

image.png

image.png

栈内存为线程私有的空间,每个线程都会创建私有的栈内存。栈空间内存设置过大,创建线程数量较多时

会出现栈内存溢出StackOverflowError。同时,栈内存也决定方法调用的深度,栈内存过小则会导致

方法调用的深度较小,如递归调用的次数较少。

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变

量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一

个栈帧在虚拟机栈里从入栈到出栈的过程。

​ 一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。对于JVM执行引擎来说,在在活

动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法

称为当前方法,定义这个方法的类叫做当前类。

执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。如果当前方法调用了其他方法,或者当前

方法执行结束,那这个方法的栈帧就不再是当前栈帧了。

调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧成为了当前栈帧。方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧(返回给方法调用者),随后虚拟机将会丢弃此栈帧。

关于「栈帧」,我们在看看《Java虚拟机规范》中的描述:

栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派。

栈帧随着方法调用而创建,随着方法结束而销毁——无论方法正常完成还是异常完成都算作方法结束。

栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中,每一个栈帧都有自己的本地变量表 (局部变量表)、操作数栈和指向当前方法所属的类的运行时常量池的引用。

接下来,详细讲解一下栈帧中的局部变量表、操作数栈、动态连接、方法返回地址等各个部分的数据结

构和作用。

局部变量表

存储内容

局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。

一个局部变量可以保存一个类型为 boolean、byte、char、short、int、float、reference和 returnAddress类型 的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。

存储容量

局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该

占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所

需分配的局部变量表的最大容量。(最大Slot数量)

其他

虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从 0~局部变量表最大容量 。如果Slot是

32位的,则遇到一个64位数据类型的变量(如long或double型)时,会连续使用两个连续的Slot来存储。

操作数栈

作用

操作数栈(Operand Stack)也常称为操作栈,它是一个 后入先出栈(LIFO) 。

当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表

或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表

或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈

的过程。

存储内容

操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2

个栈容量。

存储容量

同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的 max_stacks 数据项

中。且在方法执行的任意时刻,操作数栈的深度都不会超过 max_stacks 中设置的最大值。

动态连接

在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的

直接引用,而符号引用存在于方法区中的运行时常量池。

Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的

目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。

这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解

析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。

方法返回

当一个方法开始执行时,可能有两种方式退出该方法:

  • 正常完成出口
  • 异常完成出口

正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过

throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能

会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类

型将根据该方法返回的字节码指令确定。

异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退

出。

无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中
没有搜索到相应的异常处理器,就会导致方法退出。

无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法

返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。

方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局
部变量表和操作数栈,把返回值(如果有的话)压入调用者的操作数栈中,调整PC计数器的值以指
向方法调用指令后的下一条指令。

一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法

异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息.

附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范中没有描述的信息到栈帧之中,例如和调试相关的信

息,这部分信息完全取决于不同的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其

他附加信息一起归为一类,称为栈帧信息。

栈异常

Java虚拟机规范中,对该区域规定了这两种异常情况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出 StackOverflowError 异常;
  2. 虚拟机栈可以动态拓展,当扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
public class StackErrorMock {
    private static int index = 1;

    public void call() {
        index++;
        call();
    }

    public static void main(String[] args) {
        StackErrorMock mock = new StackErrorMock();
        try {
            mock.call();
        } catch (Throwable e) {
            System.out.println("Stack deep : " + index);
            e.printStackTrace();
        }
    }
}

本地方法栈

本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法

栈为虚拟机使用到的Native方法(比如C++方法)服务。

本地方法介绍

什么是本地方法

简单地讲,一个Native Method就是一个java调用非java代码的接口。

"A native method is a Java method whose implementation is provided by non- java code."

一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。

在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体

是由非java语言在外面实现的。下面给了一个示例:

public class IHaveNatives { 
  	native public void Native1( int x ) ; 
  	native static public long Native2() ; 
  	native synchronized private float Native3( Object o ) ; 
  	native void Native4( int[] ary ) throws Exception ; 
}

这些方法的声明描述了一些非java代码在这些java代码里看起来像什么样子。

标识符native可以与所有其它的java标识符连用,但是abstract除外。这是合理的,因为native暗

示这些方法是有实现体的,只不过这些实现体是非java的,但是abstract却显然的指明这些方法无实

现体。

native与其它java标识符连用时,其意义同非Native Method并无差别,比如native static表明

这个方法可以在不产生类的实例时直接调用,这非常方便,比如当你想用一个native method去调用一

个C的类库时。上面的第三个方法用到了native synchronized,JVM在进入这个方法的实现体之前会

执行同步锁机制(就像java的多线程。)

一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。

这些方法的实现体可以制一个异常并且将其抛出,这一点与java的方法非常相似。

当一个native method接收到一些非基本类型时如Object或一个整型数组时,这个方法可以访问这

些非基本型的内部,但是这将使这个native方法依赖于你所访问的java类的实现。有一点要牢牢记

住:我们可以在一个native method的本地实现中访问所有的java特性,但是这要依赖于你所访问的

java特性的实现,而且这样做远远不如在java语言中使用那些特性方便和容易。

native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他

类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。需要注意当我们将

一个本地方法声明为final的情况。用java实现的方法体在被编译时可能会因为内联而产生效率上的提

升。但是一个native final方法是否也能获得这样的好处却是值得怀疑的,但是这只是一个代码优化

方面的问题,对功能实现没有影响。

如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法(这

个似乎看起来有些奇怪),同样的如果一个本地方法被fianl标识,它被继承后不能被重写。

本地方法非常有用,因为它有效地扩充了jvm。事实上,我们所写的java代码已经用到了本地方法,在

sun的java的并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java

程序能够超越java运行时的界限。有了本地方法,java程序可以做任何应用层次的任务。

为什么要使用本地方法

java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意

时,问题就来了。

有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与

一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们

提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。

JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)

和一些连接到本地代码的库组成。然而不管怎 样,它毕竟不是一个完整的系统,它经常依赖于一些底

层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,

我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要

使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

JVM怎样使本地方法跑起来

如果一个方法描述符内有native,这个描述符块将有一个指向该方法的实现的指针。这些实现在一些

DLL文件内,但是它们会被操作系统加载到java程序的地址空间。当一个带有本地方法的类被加载时,

其相关的DLL并未被加载,因此指向方法实现的指针并不会被设置。当本地方法被调用之前,这些DLL才

会被加载,这是通过调用java.system.loadLibrary()实现的。

最后需要提示的是,使用本地方法是有开销的,它丧失了java的很多好处。如果别无选择,我们可以选

择使用本地方法。

本地方法栈的使用流程

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。本地方法可以

通过本地方法接口来访问虚拟机的运行时数据区,但不止如此,它还可以做任何它想做的事情。

本地方法本质上时依赖于实现的,虚拟机实现的设计者们可以自由地决定使用怎样的机制来让Java程序

调用本地方法。

任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压

入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新

的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序

调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值

也以确定的方式传回调用者。同样,这就是虚拟机实现中本地方法栈的行为。

很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的

状态并进入到另一个Java栈。

下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个

Java方法。

这幅图展示了JAVA虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,

操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。 

image.png

该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了

一个本地方法栈。假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地

方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java

方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。

垃圾回收

1.判断算法

主要是2种:引用计数法和根搜索算法

1.1 引用计数法(Reference Counting)

1.1.1 概念

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值

就减1:任何时刻计数器都为0的对象就是不可能再被使用的。

1.1.2 优点

实现简单,判断效率高,大部分情况下都是很不错的算法

1.1.3 缺点

Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环

引用的问题。

class MyObject{ MyObject object; }
public class Test { 
  public static void main(String[] args) { 
    MyObject object1 = new MyObject(); 
    MyObject object2 = new MyObject(); 
    object1.object = object2; 
    object2.object = object1; 
    object1 = null; 
    object2 = null; 
  } 
}

1.2 根搜索算法(GCRoots Tracing)

1.2.1 概念

又叫可达性算法。在主流的商用程序语言中(Java和C#),都是使用根搜索算法判定对象是否存活的。

基本思路就是通过一系列的名为“GCRoot”的对象作为起始点,从这些节点开始向下搜索,搜索
所走过的路径称为引用链(Reference Chain),当一个对象到GCRoot没有任何引用链相连(就
是从GCRoot到这个对象不可达)时,则证明此对象是不可用的。

不可达不一定会被回收,可以用finalize()方法抢救下,但绝对不推荐

1.2.2 可作GCRoots的对象

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

image.png

1.3 对象引用

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用

(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用

强度依次逐渐减弱。

image.png

1.3.1 强引用

代码中普遍存在,类似“Object obj=new Object()”这类引用,只要强引用还在,就不会被

GC。

1.3.2 软引用

非必须引用,内存溢出之前进行回收,如内存还不够,才会抛异常。

Object obj = new Object(); 
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; 
sf.get();//有时候会返回null

1.3.3 弱引用

非必须引用,只要有GC,就会被回收。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj); 
obj = null; 
wf.get();//有时候会返回null 
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃

圾回收时,将返回null。

弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的

isEnQueued方法返回对象是否被垃圾回收器标记。

1.3.4 虚引用

垃圾回收时回收,无法通过引用取到对象值

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不

会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一

目的就是希望能在这个对象被收集器回收时收到一个系统通知。

Object obj = new Object(); 
PhantomReference<Object> pf = new PhantomReference<Object>(obj); 
obj=null; 
pf.get();//永远返回null 
pf.isEnQueued();//返回是否从内存中已经删除

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。

虚引用主要用于检测对象是否已经从内存中删除。

1.4 回收过程

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,

要真正宣告一个对象死亡,至少要经历两次标记过程。

  • 第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第

一次标记;

  • 第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行

finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标

记。

第二次标记成功的对象将真的会被回收,如果对象在 finalize() 方法中重新与引用链建立了关联关

系,那么将会逃离本次回收,继续存活。

package club.jvm;

public class finalizeEscapeGC {
    public static finalizeEscapeGC SAVE_HOOK = null;

    public void isApve() {
        System.out.println("yes, i am still apve :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        finalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new finalizeEscapeGC();
        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isApve();
        } else {
            System.out.println("no, i am dead :(");
        }
        //下面这段代码与上面的完全相同,但是这次自救却失败了
        SAVE_HOOK = null;
        System.gc();
        //因为finalize方法优先级很低,所以暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isApve();
        } else {
            System.out.println("no, i am dead :(");
        }
    }
}

输出:

finalize mehtod executed!
yes, i am still apve :)
no, i am dead :(

如果把finalize()方法中最后一行注释掉//finalizeEscapeGC.SAVE_HOOK = this;

则输出:

finalize mehtod executed!
no, i am dead :(
no, i am dead :(

1.5 方法区回收

1.5.1 概念

方法区也是有垃圾回收的,主要回收废弃常量和无用的类。

即使满足回收条件也不一定真得回收。主要性价比太低

1.5.2 废弃常量

比如字符串常量,没有对象引用即可回收

常量池中的其他类(接口)、方法、字段的符号引用也与此类似

1.5.3 无用的类(需要同时满足3个条件)

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象在任何地方没有被引用,也无法通过反射访问该类的方法。
在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁
自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

2. 回收收集算法

2.1 标记-清除算法(Mark-Sweep)

2.1.1 概念

最基本的算法,主要分为标记和清除2个阶段。首先标记出所有需要回收的对象,在标记完成后统一回

收掉所有被标记的对象

2.1.2 缺点

  1. 效率不高,标记和清除过程的效率都不高
  2. 空间碎片,会产生大量不连续的内存碎片,会导致大对象可能无法分配,提前触发GC 。

image.png

2.2 复制回收算法(Copying)

2.2.1 概念

为解决效率。它将可用内存按容量划分为相等的两块,每次只使用其中的一块。当这一块的内存用完

了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

2.2.2 回收新生代

==现在商业虚拟机都是采用这种收集算法来回收新生代==,当回收时,将Eden和Survivor中还存活着的对

象拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新

生代容量的90%(80%+10%),只有10%的内存是会被“浪费”的。当Survivor空间不够用时,需要依

赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

复制算法的核心流程:

  • step1.对象默认都在Eden区产生,当Eden空间即将满时,触发第一次Minor GC(新生代GC),将Eden区所有存活对象复制到From区,然后一次性清理掉Eden区的所有空间。
  • step2.当Eden区再次即将满的时,触发MinorGC,此时需要将Eden与From区的所有存活对象复制到To区,然后一次清理掉Eden与From的所有空间。之后的新生代GC,重复阶段2(只是From与To来回作为备用区域)

备注:某些对象来回在From与To区交换若干次(默认15次)以上,将其置入老年代空间

这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存浪费了10%。且存活对象增多的话,Copying算法的效率会大大降低。

image.png

2.3 标记-整理算法(Mark-Compact)(老年代的垃圾回收算法)

老年代没有人担保,不能用复制回收算法。可以用标记-整理算法,标记过程仍然与“标记-清除”算法

一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

image.png

2.4 分代回收算法(Generational Collection)

当前商业虚拟机都是采用这种算法。根据对象的存活周期的不同将内存划分为几块。

新生代,每次垃圾回收都有大量对象失去,选择复制算法。

老年代,对象存活率高,无人进行分配担保,就必须采用标记清除或者标记整理算法

一般情况下,我们将堆区划分为新生代和老年代,老年代特点是每次垃圾回收时只有少量对象需要被回收,所以老年代采用标记整理算法;新生代特点是每次垃圾回收都有大量的对象被回收,所以新生代采用复制算法。

注意:当对象在Survivor区躲过一次GC后,其年龄就会+1。默认情况下年龄到达15的对象会被移到老生代中

2.5 内存分配担保

在现实社会中,借款会指定担保人,就是当借款人还不起钱,就由担保人来还钱。

在JVM的内存分配时,也有这样的内存分配担保机制。就是当在新生代无法分配内存的时候,把新生代

的对象转移到老生代,然后把新对象放入腾空的新生代。

现在假设,我们的新生代分为三个区域,分别为eden space,from space和to space。

现在是尝试分配三个2MB的对象和一个4MB的对象,然后我们通过JVM参数 -Xms20M、-Xmx20M、-

Xmn10M(==新生代大小==)

把Java堆大小设置为20MB,不可扩展。 其中10M分配给新生代,另外10M分配给老生代。

然后我们通过 -XX:SurvivorRatio=8 来分配新生代各区的比例,设置为8,表示eden与一个survivor

区的空间比例为8:1。

image.png

2.5.1客户端内存担保

Jvm参数配置

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

这里我们先手动指定垃圾收集器为客户端模式下的Serial+Serial Old的收集器组合进行内存回收。

由于不同的收集器的收集机制不同,为了呈现出内存分配的担保效果,我们这里需要手动指定为

Serial+Serial Old模式。

另外担保机制在JDK1.5以及之前版本中默认是关闭的,需要通过HandlePromotionFailure手动指

定,JDK1.6之后就默认开启。这里我们使用的是JDK1.8,所以不用再手动去开启担保机制。

下面我们新建四个byte数组,前三个分别为2MB大小的内存分配,第四个是4MB的内存分配。代码如

下:

package club.jvm;

public class HandlePromotionFailure {
    private static final  int _1MB = 1024 * 1024;

    /**
     * VM参数
     * -Xms20M
     * -Xmx20M
     * -Xmn10M
     * -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8
     * -XX:+UseSerialGC
     * -XX:HandlePromotionFailure (jdk1.6及以后默认开启了)
     */
    public static void testHandlePromotion(){
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }

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

运行输出日志:

[GC (Allocation Failure) [DefNew: 7984K->470K(9216K), 0.0053893 secs] 7984K->6615K(19456K), 0.0054181 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4731K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000)
  from space 1024K,  45% used [0x00000007bf500000, 0x00000007bf575bf0, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 6144K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)
 Metaspace       used 3075K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 337K, capacity 388K, committed 512K, reserved 1048576K

通过GC日志我们发现在分配allocation4的时候,发生了一次Minor GC,让新生代从7984K变为了

470K,但是你发现整个堆的占用并没有多少变化。这是因为前面三个2MB的对象都还存活着,所以回收

器并没有找到可回收的对象。但为什么会出现这次GC呢?

image.png

如果你算一笔账就知道了,前面三个对象2MB+2MB+2MB=6MB。

虚拟机分配内存优先会分配到新生代的eden space,通过图1我们知道新生代可用内存一共只有

9216KB,现在新生代已经被用去了6MB,还剩下9216KB-6144KB=3072KB,然而第四个对象是4MB,显

然在新生代已经装不下了。

image.png

于是发生了一次Minor GC!

而且本次GC期间,虚拟机发现eden space的三个对象(6MB)又无法全部放入Survivor空间

(Survivor可用内存只有1MB)。

这时候该怎么办呢?第四个对象还要不要分配呢?

此时,JVM就启动了内存分配的担保机制,把这6MB的三个对象直接转移到了老年代。

此时就把新生代的空间腾出来了,然后把第四个对象(4MB)放入了Eden区中,所以你看到的结果是

4096/8192=0.5,也就是约50%:

eden space 8192K,  52% used [0x00000007bec00000, 0x00000007bf0290f0, 0x00000007bf400000)

老年代则被占用了6MB,也就是前三个对象,102423=6144KB,6144KB/10240KB=0.6也就是60%:

the space 10240K,  60% used [0x00000007bf600000, 0x00000007bfc00030, 0x00000007bfc00200, 0x00000007c0000000)

image.png

2.5.2 服务端模式下的担保机制实现

上面我们演示的在客户端模式(Serial+Serial Old)的场景下的结果,接下来我们使用服务端模式

(Parallel Scavenge+Serial Old的组合)来看看担保机制的实现

修改GC组合为:-XX:+UseParallelGC

然后我们运行程序看看GC日志。

第四个对象是4MB的情况下:

[GC (Allocation Failure) [PSYoungGen: 8192K->688K(9216K)] 12288K->4792K(19456K), 0.0010252 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 770K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 1% used [0x00000007bf600000,0x00000007bf614868,0x00000007bfe00000)
  from space 1024K, 67% used [0x00000007bfe00000,0x00000007bfeac010,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 4104K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 40% used [0x00000007bec00000,0x00000007bf002010,0x00000007bf600000)
 Metaspace       used 3293K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 361K, capacity 388K, committed 512K, reserved 1048576K

第四个对象是3MB的情况下:

[GC (Allocation Failure) [PSYoungGen: 8015K->624K(9216K)] 8015K->6776K(19456K), 0.0047603 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 624K->0K(9216K)] [ParOldGen: 6152K->6628K(10240K)] 6776K->6628K(19456K), [Metaspace: 3230K->3230K(1056768K)], 0.0060266 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 3236K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 8192K, 39% used [0x00000007bf600000,0x00000007bf929238,0x00000007bfe00000)
  from space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
  to   space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
 ParOldGen       total 10240K, used 6628K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  object space 10240K, 64% used [0x00000007bec00000,0x00000007bf279268,0x00000007bf600000)
 Metaspace       used 3252K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K

发现当我们使用Server模式下的ParallelGC收集器组合(Parallel Scavenge+Serial Old的组

合)下,担保机制的实现和之前的Client模式下(SerialGC收集器组合)有所变化。在GC前还会进

行一次判断,如果要分配的内存>=Eden区大小的一半,那么会直接把要分配的内存放入老年代中。否

则才会进入担保机制。

这里我们的第四个对象是4MB的时候,也就是(1024KB*4)/8192KB=0.5,刚好一半,于是就这第四个对

象分配到了老年代。

第二次,我们把第四个对象由4MB,改为3MB,此时3MB/8192KB=0.37,显然不到一半,此时发现3MB还

是无法放入,那么就执行担保机制,把前三个对象转移到老生代,然后把第四个对象(3MB)放入eden

区。

总结:

内存分配是在JVM在内存分配的时候,新生代内存不足时,把新生代的存活的对象搬到老生代,然后新

生代腾出来的空间用于为分配给最新的对象。这里老生代是担保人。在不同的GC机制下,也就是不同垃

圾回收器组合下,担保机制也略有不同。在Serial+Serial Old的情况下,发现放不下就直接启动担

保机制;在Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是

>=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制。

3. 垃圾回收器

3.1 概念

垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法、火车算法)的具体实现,不

同种类JVM所提供的垃圾收集器可能会有很大差别,HotSpot虚拟机中的7种垃圾收集器:Serial、

ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1

3.2垃圾收集器组合

image.png

分类

新生代收集器: Serial、ParNew、Parallel Scavenge;

老年代收集器: Serial Old、Parallel Old、CMS;

整堆收集器: G1

搭配使用:

两个收集器间有连线,表明它们可以搭配使用:

  • Serial/Serial Old
  • Serial/CMS
  • ParNew/Serial Old
  • ParNew/CMS
  • Parallel Scavenge/Serial Old
  • Parallel Scavenge/Parallel Old
  • G1

3.3并发垃圾收集器和并行垃圾收集的区别

3.3.1 并行(Parallel)

指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;

如: ParNew、Parallel Scavenge、Parallel Old;

3.3.2 并发(Concurrent)

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上;

如CMS、G1(也有并行)

3.4 Minor GC、Major GC 和Full GC之间的区别

  • Minor GC

新生代GC(包括Eden 和 Survivor区域),Minor GC非常频繁,回收速度也比较快

年轻代满时会触发Minor GC,这里年轻代满指的是Eden代满,Survivor满不会引发GC

  • Major GC

老年代GC,Major GC速度一般比Minor GC慢10倍以上;

  • Full GC

Minor GC + Major GC, 清理整个堆空间,包括年轻代和永久代。

  1. 调用System.gc时,系统建议执行Full GC,但是不必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  5. 年轻代需要把该对象转存到老年代,且老年代的可用内存小于该对象大小

参考:

Java内存管理:Java内存区域 JVM运行时数据区https://blog.csdn.net/tjiyu/article/details/53915869

https://blog.csdn.net/tjiyu/article/details/53982412

https://blog.csdn.net/tjiyu/article/details/53983064

https://blog.csdn.net/tjiyu/article/details/53983650

STW Stop The World 
Serial 
ParNew
	-XX:ParallelGCThreads 
Parallel Scavenge (全局) 
	吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) 
	-XX:MaxGCPauseMillis=n 
	-XX:GCTimeRatio=n 
	-XX:UseAdaptiveSizePolicy GC Ergonomics 
Serial Old 
	CMS备用预案 Concurrent Mode Failusre时使用 
	标记-整理算法 
Parallel Old 标记-整理算法 
CMS 
	标记-清除算法 
	减少回收停顿时间 
	碎片 -XX:CMSInitiatingOccupancyFraction 
	Concurrent Mode Failure 启用Serial Old 
	-XX:+UseCMSCompactAtFullCollection 
	-XX:CMSFullGCsBeforeCompaction 执行多少次不压缩FullGC后 来一次带压缩的 0 表示 每次都压
	-XX:+UseConcMarkSweep 
G1

以下分别介绍7种垃圾收集器

1. Serial收集器(新生代---复制算法--单线程)

Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;

JDK1.3.1前是HotSpot新生代收集的唯一选择;

  1. 特点
  • 针对新生代;
  • 采用复制算法;
  • 单线程收集;
  • 进行垃圾收集时,必须暂停所有工作线程,直到完成; 即会"Stop The World";

Serial/Serial Old组合收集器运行示意图如下:

image.png

  1. 应用场景

依然是HotSpot在Client模式下默认的新生代收集器;

也有优于其他收集器的地方:

  • 简单高效(与其他收集器的单线程相比);
  • 对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
  • 在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的。
  1. 设置参数:

-XX:+UserSerialGC 添加该参数来显示的使用串行垃圾收集器;

  1. Stop The Word说明

JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;

从JDK1.3到现在,从Serial收集器-》Parallel收集器-》CMS-》G1,用户线程停顿时间不断缩短,但仍然无法完全消除;

2. ParNew收集器(新生代---复制算法--多线程)

ParNew收集器是Serial收集器的多线程版本.

  1. 特点

除了多线程外,其余的行为、特点和Serial收集器一样;

如Serial收集器可用控制参数、收集算法、Stop TheWorld、内存分配规则、回收策略等;

两个收集器共用了不少代码;

ParNew/Serial Old组合收集器运行示意图如下:

image.png

  1. 应用场景

在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;

但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

  1. 设置参数

"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器

"-XX:+UseParNewGC":强制指定使用ParNew

"-XX:ParallelGCThreads":指定垃圾收集线程数量,默认收集线程与CPU数量相同

  1. 为什么只有ParNew能与CMS收集器配合

CMS是HotSpot在JDK1.5第一款并发收集器,让垃圾收集线程与用户线程(基本上)同时工作;

CMS作为老年代收集器,但无法与JDK1.4新生代收集器Parallel Scavenge配合工作;

因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余

几种收集器则共用了部分的框架代码;

3. Parallel Scavenge收集器(新生代---复制算法--多线程)

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)

  1. 特点

a. 有一些特点与ParNew收集器相似

新生代收集器

采用复制算法

多线程收集

b.主要特点是:它的关注点与其他收集器不同

CMS等收集器的关注点是缩短垃圾收集时用户线程的停顿时间;

而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)

  1. 应用场景

提高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间

当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互

例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;

  1. 设置参数

Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:

a "-XX:MaxGCPauseMillis"

控制最大垃圾收集停顿时间,大于0的毫秒数;

如果值过小,停顿时间会缩短,但是垃圾收集次数会更频繁,使吞吐量下降;

b "-XX:GCTimeRatio"

设置垃圾收集时间占总时间的比率,0<n<100的整数;相当于设置吞吐量大小;

垃圾收集时间占应用执行时间的比例算法:1 / (1 + n)。默认值是1%--1/(1+99),即n=99;

垃圾收集所花费的时间是年轻一代和老年代收集的总时间;

如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;

此外,还有一个值得关注的参数:

c. "-XX:+UseAdptiveSizePolicy"

开启这个参数后,就不用手工指定一些细节参数,如:

新生代的大小(-Xmn). Eden与Survivor区的比例(-XX:SurvivorRation). 晋升老年代的对

象年龄(-XX:PretenureSizeThreshold)等;

JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时

间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs);

这是一种值得推荐的方式:

(1). 只需设置好内存数据大小(如"-Xmx"设置最大堆);

(2). 然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;

(3). 那些具体细节参数的调节就由JVM自适应完成;

这也是Parallel Scavenge收集器与ParNew收集器一个重要区别

  1. 吞吐量与收集器关注点说明

a. 吞吐量(Throughput)

CPU用于运行用户代码的时间与CPU总消耗时间的比值;

即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);

高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;

b. 垃圾收集器期望的目标(关注点)

  1. 停顿时间

停顿时间越短就适合需要与用户交互的程序;

良好的响应速度能提升用户体验;

  1. 吞吐量高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;

主要适合在后台计算而不需要太多交互的任务;

  1. 覆盖区(Footprint)

在达到前面两个目标的情况下,尽量减少堆的内存空间;

可以获得更好的空间局部性;

4. Serial Old收集器(老年代---标记整理算法--单线程)

Serial Old是 Serial收集器的老年代版本;

  1. 特点

针对老年代;

采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);

单线程收集;

Serial/Serial Old收集器运行示意图如下:

image.png

  1. 应用场景

主要用于Client模式;

而在Server模式有两大用途:

a. 在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭

配)

b. 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);

更多Serial Old收集器信息请参考:

内存管理白皮书 4.3.2节:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

5.Parallel Old收集器(老年代--标记整理算法--多线程)

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;

JDK1.6中才开始提供;

  1. 特点

针对老年代;

采用"标记-整理"算法;

多线程收集;

Parallel Scavenge/Parallel Old收集器运行示意图如下:

image.png

  1. 应用场景

JDK1.6及之后用来代替老年代的Serial Old收集器;

特别是在Server模式,多CPU的情况下;

在注重吞吐量以及CPU资源敏感场景,就有Parallel Scavenge加Parallel Old收集器"给力"组合

  1. 设置参数

"-XX:+UseParallelOldGC":指定使用Parallel Old收集器;

更多Parallel Old收集器收集过程介绍请参考:

《内存管理白皮书》 4.5.2节:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

6. CMS 收集器(老年代-标记清除算法)

可以参考: https://blog.csdn.net/luofenghan/article/details/78511039

并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low

Pause Collector)或低延迟(low-latency)垃圾收集器;

  1. 特点

针对老年代;

基于"标记-清除"算法(不进行压缩操作,产生内存碎片);

以获取最短回收停顿时间为目标;

并发收集、低停顿;

需要更多的内存(看后面的缺点);

是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;

第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

  1. 应用场景

与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度;以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用;

  1. 设置参数

"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;会使用ParNew对新生代的无用对象进行回收

  1. CMS收集器运作过程,可以分为4个步骤:

(A). 初始标记(CMS initial mark)

仅标记一下GC Roots能直接关联到的对象;

速度很快;但需要"Stop The World";

(B). 并发标记(CMS concurrent mark)

进行GC Roots Tracing的过程;

刚才产生的集合中标记出存活对象;

应用程序也在运行;

并不能保证可以标记出所有的存活对象;

(C). 重新标记(CMS remark)

为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

采用多线程并行执行来提升效率;

(D). 并发清除(CMS concurrent sweep)

回收所有的垃圾对象;

整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;

所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;

CMS收集器运行示意图如下:

image.png

  1. CMS收集器3个明显的缺点

a. 对CPU资源非常敏感

并发收集虽不暂停用户线程,但会占用部分CPU资源,导致应用程序变慢,总吞吐量降低。

CMS的默认收集线程数量是=(CPU数量+3)/4;

当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个

时,影响更大,可能无法接受。

增量式并发收集器:(Incremental Concurrent Mark Sweep/i-CMS)

针对这种情况,曾出现了"增量式并发收集器"

类似使用抢占式来模拟多任务机制7的思想,让收集线程和用户线程交替运行,减少收集线

程运行时间;但效果并不理想,JDK1.6后就官方不再提倡用户使用。更多请参考:

官方的《垃圾收集调优指南》8.8节 Incremental Mode:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#CJAGIIEJ

《内存管理白皮书》 4.6.3节可以看到一些描述;

b. 无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败

  1. 浮动垃圾(Floating Garbage)

在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;

这使得并发清除时需预留一定空间,不能像其他收集器在老年代快满再进行收集;

也要可以认为CMS所需要的空间比其他垃圾收集器大;

"-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;

JDK1.5默认值为68%;

JDK1.6变为大约92%;

  1. "Concurrent Mode Failure"失败

如CMS预留空间无法满足需求,就会出现一次"Concurrent Mode Failure"失败;

这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;

这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。

c. 产生大量内存碎片

由于CMS基于"标记-清除"算法,清除后不进行压缩操作;

前面《Java虚拟机垃圾回收(二) 垃圾回收算法》"标记-清除"算法介绍时曾说过:

产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提

前触发另一次Full GC动作。

解决方法:

(1). "-XX:+UseCMSCompactAtFullCollection"

使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;

但合并整理过程无法并发,停顿时间会变长;

默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);

(2). "-XX:+CMSFullGCsBeforeCompaction"

设置执行多少次不压缩的Full GC后,来一次压缩整理;为减少合并整理过程的停顿时间;

默认为0,也就是说每次都执行Full GC,不会进行压缩整理;

由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;

更多关于内存分配方式请参考:《Java对象在Java虚拟机中的创建过程》

总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;

但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;

更多CMS收集器信息请参考:

《垃圾收集调优指南》 8节 Concurrent Mark Sweep (CMS) Collector:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector

《内存管理白皮书》 4.6节 Concurrent Mark-Sweep (CMS) Collector:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

7. G1收集器(包括新生代和老年代,结合多种回收算法)

G1(Garbage-First)是JDK7-u4才推出商用的收集器;

  1. 特点

a. 并行与并发

能充分利用多CPU、多核环境下的硬件优势;

可以并行来缩短"Stop The World"停顿时间;

也可以并发让垃圾收集与用户程序同时进行;

b. 分代收集,收集范围包括新生代和老年代

能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;

能够采用不同方式处理不同时期的对象;

虽然保留分代概念,但Java堆的内存布局有很大差别;

将整个堆划分为多个大小相等的独立区域(Region);

新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;

更多G1内存布局信息请参考:

《垃圾收集调优指南》 9节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection

c. 结合多种垃圾收集算法,空间整合,不产生碎片

从整体看,是基于标记-整理算法;

从局部(两个Region间)看,是基于复制算法;

这是一种类似火车算法的实现;

都不会产生内存碎片,有利于长时间运行;

d. 可预测的停顿:低停顿的同时实现高吞吐量

G1除了追求低停顿处,还能建立可预测的停顿时间模型;

可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;

  1. 应用场景

面向服务端应用,针对具有大内存、多处理器的机器;

最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;

如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;

用来替换掉JDK1.5中的CMS收集器;

在下面的情况时,使用G1可能比CMS好:

(1). 超过50%的Java堆被活动数据占用;

(2). 对象分配频率或年代提升频率变化很大;

(3). GC停顿时间过长(长于0.5至1秒)。

是否一定采用G1呢?也未必:

如果现在采用的收集器没有出现问题,不用急着去选择G1;

如果应用程序追求低停顿,可以尝试选择G1;

是否代替CMS需要实际场景测试才知道。

  1. 设置参数

"-XX:+UseG1GC":指定使用G1收集器;

"-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发

标记阶段;默认为45;

"-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;

"-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约

2048个Region;

更多关于G1参数设置请参考:

《垃圾收集调优指南》 10.5节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#important_defaults

  1. 为什么G1收集器可以实现可预测的停顿

G1可以建立可预测的停顿时间模型,是因为:

可以有计划地避免在Java堆的进行全区域的垃圾收集;

G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;

每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);

这就保证了在有限的时间内可以获取尽可能高的收集效率;

  1. 一个对象被不同区域引用的问题

一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?

在其他的分代收集器,也存在这样的问题(而G1更突出):

回收新生代也不得不同时扫描老年代?

这样的话会降低Minor GC的效率;

解决方法:

无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描:

每个Region都有一个对应的Remembered Set;

每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;

然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集

器:检查老年代对象是否引用了新生代对象);

如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;

当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;

就可以保证不进行全局扫描,也不会有遗漏。

  1. G1收集器运作过程

不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

(A). 初始标记(Initial Marking)

仅标记一下GC Roots能直接关联到的对象;

且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的

Region中创建新对象;

需要"Stop The World",但速度很快;

(B). 并发标记(Concurrent Marking)

进行GC Roots Tracing的过程;

刚才产生的集合中标记出存活对象;

耗时较长,但应用程序也在运行;

并不能保证可以标记出所有的存活对象;

(C). 最终标记(Final Marking)

为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

上一阶段对象的变化记录在线程的Remembered Set Log;

这里把Remembered Set Log合并到Remembered Set中;

需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

采用多线程并行执行来提升效率;

(D). 筛选回收(Live Data Counting and Evacuation)

首先排序各个Region的回收价值和成本;

然后根据用户期望的GC停顿时间来制定回收计划;

最后按计划回收一些价值高的Region中垃圾对象;

回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在

此过程中压缩和释放内存;

可以并发进行,降低停顿时间,并增加吞吐量;


标题:java堆以及垃圾回收
作者:码农路上
地址:http://wujingjian.club/articles/2021/04/26/1619434318013.html