JVM知识点(一)

目录

一、介绍JVM的内存区域(运行时数据区)

参考资料

JVM内存划分

JVM 内存模型概述

必知必会JVM二-运行时数据区

JVM:一、运行时数据区域

JAVA 方法区与堆–java7前,java7,java8各不相同

img

img

img

img

è¿éåå¾çæè¿°

JVM中内存分为若干部分:堆、方法区、虚拟机栈、本地方法栈、程序计数器。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些数据区域可以分为两个部分:一部分是线程共享的,一部分则是线程私有的。其中堆和方法区是线程共享的部分,其他是线程隔离的。

注意:JVM内存区域或Java运行时数据区与Java内存模型(JMM)不是一回事,java的内存模型是JMM(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式,如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。

1. 堆 Heap

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

Java堆的唯一目的就是存放对象实例(Java虚拟机规范中的描述时:所有的对象实例以及数组都要在堆上分配)。

由于存在逃逸分析技术(分析这个对象不会被其他方法或线程调用),也可以分布在栈上,随着出栈而销毁。

同时,Java堆也是垃圾回收的主要区域,由于现在垃圾收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代

  • 从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB);
  • 从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代,在细致一点的有Eden空间,From Survivor空间,To Survivor空间等。

Java堆在物理上可以不连续,只要逻辑连续就好。在堆上分配对象的方法又:指针碰撞和空闲列表,前者是在堆内存规整的情情景下,所有用过和空闲的内存中间有明确的分界线,而后者用空闲列表来记录内存的使用情况,规整是由垃圾回收器是否压缩整理决定。空间不足抛出OutOfMemoryError(OOM异常)。

堆上对象的访问方式:通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄直接指针两种。句柄而言,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。直接指针而言,reference中存储的直接就是对象的地址。前者稳定,后者效率高。

2. 方法区 Method Area

方法区与Java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码、运行时常量池等数据,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载

HotSpot虚拟机中该区域叫永久代(PermGen space),即方法区是虚拟机规范,而永久代是HotSpot实现的方法区。

其中运行时常量池,用于存放编译期生成的各种字面量和符号引用(字面量:1.文本字符串;2.八种基本类型值;3.被声明为final的常量等。符号引用:1.类和方法的全限定名;2.字段的名称和描述符;3.方法的名称和描述符。)。运行时常量池相对于Class常量池具有动态性,可以在运行期间利用intern方法将常量放入池中,空间不足抛出OutOfMemoryError。

引申:JDK7以前、JDK7和JDK8的方法区(永久代)

jdk1.6常量池放在方法区(也即是Perm空间),jdk1.7,jdk1.8常量池放在堆内存。所以导致string的intern方法因为以上变化在不同版本会有不同表现。

  • java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
  • java7中,存储在永久代的部分数据就已经转移到Java Heap或者Native memory。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap。
  • java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中

Native memory:本地内存,也称为C-Heap,是供JVM自身进程使用的。当Java Heap空间不足时会触发GC,但Native memory空间不够却不会触发GC。

img

思考

(1)为什么移除永久代?

1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、永久代大小不容易确定,PermSize指定太小容易造成永久代OOM。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

(2)在JDK1.7中,已经把原本放在永久代的字符串常量池移出, 放在堆中,为什么这样做呢?
因为使用永久代来实现方法区不是个好主意,很容易遇到内存溢出的问题。我们通常使用PermSize和MaxPermSize设置永久代的大小,这个大小就决定了永久代的上限,但是我们不是总是知道应该设置为多大的,如果使用默认值容易遇到OOM错误。

类的元数据,字符串池,类的静态变量将会从永久代移除,放入Java heap或者native memory。其中建议JVM的实现中将类的元数据放入 native memory,将字符串池和类的静态变量放入java堆中。这样可以加载多少类的元数据就不再由MaxPermSize控制,而由系统的实际可用空间来控制。

为什么这么做呢?减少OOM只是表因,更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有一个叫永久代的东西,但是运行良好,也不需要开发运维人员设置这么一个永久代的大小。当然不用担心运行性能问题了,在覆盖到的测试中,程序启动和运行速度降低不超过1%,但是这一点性能损失换来了更大的安全保障。

3. 虚拟机栈 JVM Stacks

虚拟机栈是Java方法执行的内存模型,线程私有,每个方法执行都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接(运行时将方法区的符号引用转化为直接引用,真的可以调用了)和方法出口等信息,每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。StackOverflowError:线程请求的栈深度大于虚拟机所允许的栈深度。OutOfMemoryError:如果虚拟机可以动态扩展,而扩展时无法申请到足够的内存。

4. 本地方法栈 Native Method Stack

JVM调用本地方法,可以为其他语言提供接口。

与虚拟机栈的作用非常相似,区别是:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法。

备注:HotSpot直接把本地方法栈和虚拟机栈合二为一。本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

5. 程序计数器 Program Counter Register

**程序计数器是一块较小的内存空间,可以看做当前线程所执行的字节码的行号指示器。**字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

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

备注:

(a)如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则为空。

(b)此内存区域是唯一在Java虚拟机规范中没有规定任何OOM情况的区域。

6. 补充

(1)运行时常量池和Class文件常量池

Runtime Constant Pool是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性。

备注:有OOM异常

(2)直接内存

Direct Memory,并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。

在jdk1.4加入的NIO类,引入了一种基于通道(Chanel)与缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

备注:本机直接内存的分配不会受到Java堆大小的限制,受到本机总内存和处理器寻址空间的限制,有OOM异常。

运行时内存区域外规定的堆外内存:直接使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。

7. 对象的创建、内存布局、访问定位

基于虚拟机HotSpot深入探讨Java堆中对象分配、布局和访问的全过程。

7.1 对象的创建

(1)虚拟机遇到new指令后,首先检查new指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载,解析和初始化过。如果没有,那必须先执行类的加载过程;

(2)类加载检查通过之后,虚拟机将为新生对象分配内存(对象所需内存的大小在类加载完成后可完全确定);

(3)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值;

(4)对对象进行必要的设置,例如这个对象是哪个类的实例,如果才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄等信息(这些对象存放在对象的对象头Object Header中)

(5)执行init方法,把对象按照程序员的意愿进行初始化。

指针碰撞空闲列表两种分配方式:选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的GC是否带有压缩整理功能决定;因此,在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法时指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

此外还有对象创建的并发问题:

(1)堆分配内存空间的动作进行同步处理–采用CAS配上失败重试的方式保证更新操作的原子性;

(2)把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB)

注:指针碰撞:内存是规整的,空闲列表:内存不是规整的

7.2 对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充。

img

对象头(存储在堆)包括两部分:

  • 存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标识,线程持有的锁,偏向线程ID,偏向时间戳等,官方称为Mark Word。Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用 自己的存储空间。
存储内容标志位状态
对象哈希码,对象分代年龄01未锁定
指向锁记录的指针00轻量级锁定
指向重量级锁的指针10膨胀(重量级锁定)
空,不需要记录信息11GC标记
偏向线程ID,偏向时间戳,对象分代年龄01可偏向
  • 类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐填充并不是必然存在的,也没有特别的含义。HotSpot自动内存管理系统要求对象起始地址必须是8字节的倍数,也就是说对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,因此,当对象实例数据不符没有对齐时,就需要通过对齐填充来补全。

7.3 对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位,访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而言的。目前主流的访问方式有使用句柄直接指针两种。

(1)句柄访问

Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

img

(2)直接指针,reference中存储的是对象地址,而对象实例的对象头中有类型指针,指向类型数据。

img

两种对象访问各有优势,使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

直接指针最大好处就是速度更快,节省了一次指针定位的时间开销。

HotSpot使用了直接指针方式进行对象访问。

备注:reference应该指向堆中Java对象的内存地址,而对象的内存布局里面包括:对象头和对象数据,对象头里面有对象类型指针。

对象实例数据(堆):对象中各个实例字段的数据

对象类型数据(方法区):对象的类型、父类、实现的接口、方法等

二、如何判断对象已经死亡

JVM怎么判断对象已经死亡

【JAVA】如何判断对象已经死亡?

引用****计数法可达性分析法

1. 引用计数法

程序给对象添加一个引用计数器,每有一个变量引用它时,计数器加1。当引用断开时,计数器减1。当计数器为0时,代表着没有任何变量引用它,该对象就是死亡状态,JVM需要对此类对象进行回收。

引用计数法的实现简单,效率也很高。

但绝大数主流的虚拟机并没有采取此计数算法来管理内存,原因是此计数算法无法回收那些具有相互循环引用的对象,此类对象确实已经不再被使用,但由于互相引用着对方,导致各自的计数器都不为0,因此JVM无法回收它们。

public class Test {

    private static final int _1MB = 1024 *1024;

    private Object instance = null;

    public static void testGC(){
        Test objectA = new Test();
        Test objectB = new Test();

        objectA.instance = objectB;
        objectB.instance = objectA;

        objectA = null;
        objectB = null;

        // 假如这里发生GC,objectA和objectB会不会被回收

    }
}

2. 可达性分析法

这个算法的基本思路是通过一系列称为“GC Roots”(一组必须活跃的引用)作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链时候,那么证明此对象是不可用的。

Java语言中,可以作为GC Roots的对象包括以下几种:

1)虚拟机栈(栈帧中的本地变量表)中引用的对象。

2)方法区中类静态属性引用的对象。

3)方法区中常量引用的对象。

4)本地方法栈JNI(即一般说的Native方法)引用的对象。

大多数主流的JVM都采用这样的算法来管理内存,它能够解决对象之间的循环引用的问题。对象与对象之间虽然有循环引用,当他们到GC Roots没有任何引用链,系统还是判定它们为可回收对象。

3. 真的死了?

即使在可达性分析中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经理两次标记过程:

1)如果对象在可达性分析算法中不可达,那么它会被第一次标记并进行一次刷选,刷选的条件是是否需要执行finalize()方法(当对象没有覆盖finalize()或者finalize()方法已经执行过了(对象的此方法只会执行一次)),虚拟机将这两种情况都会视为没有必要执行)。

2)如果这个对象有必要执行finalize()方法会将其放入F-Queue队列中,稍后GC将对F-Queue队列进行第二次标记,如果在重写finalize()方法中将对象自己赋值给某个类变量或者对象的成员变量,那么第二次标记时候就会将它移出“即将回收”的集合。

三、简述强、软、弱、虚引用

参考资料

Java JVM 引用有什么用

Java 7之基础 - 强引用、弱引用、软引用、虚引用

Java 关于强引用,软引用,弱引用和虚引用的区别与用法

强软弱虚—强引用、软引用、弱引用、虚引用(这个里面有图示和代码演示,可以细看)

无论是通过引用计数器算法判断对象的引用数量,还是通过可达性分析算法判断对象引用链是否可达,判断对象是否可活都离不开引用。

众所周知,Java中是JVM负责内存的分配和回收,这是它的优点(使用方便,程序不用再像使用c那样操心内存),但同时也是它的缺点(不够灵活)。为了解决内存操作不灵活这个问题,可以采用软引用等方法。

在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这 就像在日常生活中,从商店购买了某样物品后,如果有用,就一直保留它,否则就把它扔到垃圾箱,由清洁工人收走。一般说来,如果物品已经被扔到垃圾箱,想再 把它捡回来使用就不可能了。

但有时候情况并不这么简单,你可能会遇到类似鸡肋一样的物品,食之无味,弃之可惜。这种物品现在已经无用了,保留它会占空间,但是立刻扔掉它也不划算,因 为也许将来还会派用场。对于这样的可有可无的物品,一种折衷的处理办法是:如果家里空间足够,就先把它保留在家里,如果家里空间不够,即使把家里所有的垃 圾清除,还是无法容纳那些必不可少的生活用品,那么再扔掉这些可有可无的物品。

从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

img

1. 强引用

是指程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在对象就不会被回收。

如果一个对象具有强引用,垃圾回收器绝不会回收它,当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError,使程序异常终止。

2. 软引用

软引用是用来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将于发生内存溢出异常之前,将会把这些对象列进回收范围中进行二次回收。

如果一个对象只具有软引用,那就类似于可有可物的生活用品。**如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。**只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

public class Test {  

    public static void main(String[] args){  
        System.out.println("开始");            
        A a = new A();            
        SoftReference<A> sr = new SoftReference<A>(a);  
        a = null;  
        if(sr!=null){  
            a = sr.get();  
        }  
        else{  
            a = new A();  
            sr = new SoftReference<A>(a);  
        }            
        System.out.println("结束");     
    }       

}  

class A{  
    int[] a ;  
    public A(){  
        a = new int[100000000];  
    }  
}  

当内存足够大时可以把数组存入软引用,取数据时就可从内存里取数据,提高运行效率

软引用在实际中有重要的应用,例如浏览器的后退按钮

按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;

(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出;

这时候就可以使用软引用。

3. 弱引用

如果一个对象只具有弱引用,那就类似于可有可物的生活用品。**弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。**不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

Object c = new Car(); //只要c还指向car object, car object就不会被回收
WeakReference<Car> weakCar = new WeakReference(Car)(car);

当要获得weak reference引用的object时,首先需要判断它是否已经被回收:

weakCar.get();

如果此方法为空,那么说明weakCar指向的对象已经被回收了。

public class Car {
  private double price;
  private String colour;

  public Car(double price, String colour){
    this.price = price;
    this.colour = colour;
  }

  public double getPrice() {
    return price;
  }
  public void setPrice(double price) {
    this.price = price;
  }
  public String getColour() {
    return colour;
  }
  public void setColour(String colour) {
    this.colour = colour;
  }

  public String toString(){
    return colour +"car costs $"+price;
  }

}


public class TestWeakReference {


  public static void main(String[] args) {

    Car car = new Car(22000,"silver");
    WeakReference<Car> weakCar = new WeakReference<Car>(car);

    int i=0;

    while(true){
      if(weakCar.get()!=null){
        i++;
        System.out.println("Object is alive for "+i+" loops - "+weakCar);
      }else{
        System.out.println("Object has been collected.");
        break;
      }
    }
  }

}

在上例中,程序运行一段时间后,程序打印出”Object has been collected.” 说明,weak reference指向的对象的被回收了。

如果要想打出的是

"Object is alive for " + i + " loops - " + weakCar

那么只要在这句话前面加上

System.out.println("car====" + car);

因为在此强引用了car对象。

如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。

当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。

这个引用不会在对象的垃圾回收判断中产生任何附加的影响。

4. 虚引用

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

5. 虚与软、弱引用的区别

软、弱引用都是在引用对象被垃圾回收后,Java虚拟机才把引用加入到与之关联的引用队列中,而虚引用对象在垃圾回收前,Java虚拟机就把引用加入到与之关联的引用队列中。

6. 总结

这里写图片描述

img

说白了就是jvm就像一个国家,gc就是城管,强引用就是当地人,软引用就是移民的人,弱引用就是黑户口,哪天城管逮到就遣走,虚引用就是一个带病的黑户口,指不定哪天自己就挂了。

  • 强引用:最传统的「引用」的定义,是指在程序代码之中普遍存在的引用赋值,即类似Object obj=new Object()这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
    • 软引用:描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用:是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

四、简述垃圾回收算法以及各自特点

参考资料

1. 标记-清除算法(Mark-Sweep)

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它是最基础的收集算法。

带来两个明显问题:

(1)效率问题;

(2)空间问题(标记清除后会产生大量不连续的碎片);

之所以说它是最基础的是因为它最容易实现,思想也是最简单的。

标记-清除算法分为两个阶段:标记阶段和清除阶段。

  • 标记阶段的任务是标记出所有需要被回收的对象;

  • 清除阶段就是回收被标记的对象所占用的空间。

具体过程如下图所示:

img

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2. 复制算法(Copying)

强行设置边界。

将内存分为大小相同的两块,每次使用其中的一块。当这一块内存使用完后,就将还存活的对象复制到另一块去,然后把使用的空间一次清理掉。这样就使每次内存回收都是对内存区间的一半进行回收。

解决了Mark-Sweep算法的缺陷,Copying算法不容易出现内存碎片的问题。具体过程如下图所示:

img

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

所以一般不会1:1划分边界,可以分一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中一块Survivor空间。

当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor上,最后清理Eden和Survivor空间。大小比例一般是8:1:1,每次浪费10%的Survivor空间。

但是这里有一个问题就是如果存活的大于10%怎么办?

这里采用一种分配担保策略:多出来的对象直接进入老年代。

640?wx_fmt=png

3. 标记-整理算法(Mark-Compact)(压缩法)

整理出边界。

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

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。

该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

img

4. 分代收集算法(Generational Collection)

结合以上方法。

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。

它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代**(Tenured Generation)和新生代(Young Generation)**,老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取复制算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,只需要付出少量对象的复制成本就可以完成每次垃圾收集;

而由于老年代的对象存活几率较高,每次回收都只回收少量对象,一般使用的是标记-整理算法(压缩法)

五、常见垃圾收集器

参考资料

Java虚拟机垃圾回收(三) 7种垃圾收集器

深入理解7种垃圾收集器

JVM参数设置(吞吐量和响应时间性能调优)

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾收集器主要有:Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1。

JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:

img

(A)图中展示了7种不同分代的收集器:

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

(B)而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

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

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

整堆收集器:G1;

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

​ Serial / Serial Old

​ Serial / CMS

​ ParNew / Serial Old

​ ParNew / CMS

​ Parallel Scavenge / Serial Old

​ Parallel Scavenge / Parallel Old

​ G1

(D)其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案(后面介绍);

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

(1)并行(Parallel)

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

​ 如ParNew、Parallel Scavenge、Parallel Old;

(2)并发(Concurrent)

​ 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);

用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上

​ 如CMS、G1(也有并行);

Minor GC和Full GC的区别

(1)Minor GC

​ 又称新生代GC,指发生在新生代的垃圾收集动作;

​ 因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

(2)Full GC

​ 又称Major GC或老年代GC,指发生在老年代的GC;

​ 出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);

Major GC速度一般比Minor GC慢10倍以上;

吞吐量(Throughput)

吞吐量就是CPU用于运行用户代码的时间CPU总消耗时间的比值,即

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

假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

1. Serial收集器(Minor GC)

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

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

(1)特点

针对新生代

采用复制算法

单线程收集;

进行垃圾收集时,必须暂停所有工作线程,直到完成(Stop The World);

(这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接收的)

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

img

img

(2)应用场景

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

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

  • 简单高效(与其他收集器的单线程相比);

  • 对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,专心做垃圾收集,可以获得最高的单线程收集效率;

  • 在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的,所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

(3)设置参数

“-XX:+UseSerialGC”:添加该参数来显式的使用串行垃圾收集器;

(4)Stop TheWorld说明

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

会带给用户不良的体验;

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

2. ParaNew收集器(Minor GC)

ParNew垃圾收集器是Serial收集器的多线程版本。

(1)特点

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

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

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

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

img

img

(2)应用场景

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

但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。

(3)设置参数

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

“-XX:+UseParNewGC”:强制指定使用ParNew;

“-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

(4)为什么除了Serial只有ParNew能与CMS收集器配合

CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

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

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

3. Parallel Scavenge收集器(Minor GC)

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

(1)特点

(A)、有一些特点与ParNew收集器相似

新生代收集器;

采用复制算法;

多线程收集;

(B)、主要特点是:它的关注点与其他收集器不同

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

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

(2)应用场景

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

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

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

(3)设置参数

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

(A)、"-XX:MaxGCPauseMillis"

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

MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;

因为可能导致垃圾收集发生得更频繁;

(B)、"-XX:GCTimeRatio"

设置垃圾收集时间占总时间的比率,0<n<100的整数;

GCTimeRatio相当于设置吞吐量大小;

垃圾收集执行时间占应用程序执行时间的比例的计算方法是:

​ 1 / (1 + n)

例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%–1/(1+19);

默认值是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收集器的一个重要区别。

另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。

(4)吞吐量与收集器关注点说明

(A)、吞吐量(Throughput)

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

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

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

(B)、垃圾收集器期望的目标(关注点)

(1)、停顿时间

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

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

(2)、吞吐量

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

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

(3)、覆盖区(Footprint)

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

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

4. Serial Old收集器(Full GC)

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

(1)特点

针对老年代;

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

单线程收集;

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

img

img

(2)应用场景

主要用于Client模式;

而在Server模式有两大用途:

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

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

5. Parallel Old收集器(Full GC)

Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本。 JDK1.6中才开始提供。

(1)特点

针对老年代;

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

多线程收集;

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

img

img

(2)应用场景

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

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

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

(3)设置参数

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

6. CMS收集器(Full GC)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。

(1)特点

针对老年代;

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

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

并发收集、低停顿;

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

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

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

(2)应用场景

与用户交互较多的场景;

希望系统停顿时间最短,注重服务的响应速度;

以给用户带来较好的体验;

如常见WEB、B/S系统的服务器上的应用;

(3)设置参数

“-XX:+UseConcMarkSweepGC”:指定使用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收集器的运作步骤中并发和需要停顿的时间:

img

img

(5)优点

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)

(6)缺点

(A)、对CPU资源非常敏感

在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低;

CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降;

但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);

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

但效果并不理想,JDK1.6后就官方不再提倡用户使用。

(B)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生

(1)、浮动垃圾(Floating Garbage)

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

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

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

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

JDK1.5默认值为68%;

JDK1.6变为大约92%;

(2)、"Concurrent Mode Failure"失败

如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;

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

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

(C)、产生大量内存碎片

CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,从而需要提前触发另一次Full GC动作。

解决方法:

(1)、"-XX:+UseCMSCompactAtFullCollection"

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

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

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

(2)、"-XX:+CMSFullGCsBeforeCompaction"

设置执行多少次不压缩的Full GC后,来一次压缩整理;

为减少合并整理过程的停顿时间;

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

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

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

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

7. G1收集器(Minor GC + Full GC)

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

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。

(1)特点

(A)、并行与并发

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

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

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

(B)、分代收集,收集范围包括新生代和老年代

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

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

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

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

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

(C)、结合多种垃圾收集算法,空间整合,不产生碎片

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

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

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

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

(D)、可预测的停顿:低停顿的同时实现高吞吐量

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

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

(2)应用场景

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

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

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

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

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

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

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

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

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

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

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

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

(3)设置参数

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

“-XX:InitiatingHeapOccupancyPercent”:当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;

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

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

(4)为什么G1收集器可以实现可预测的停顿

G1可以建立可预测的停顿时间模型,是因为G1可以有计划地避免在Java堆进行全区域的垃圾收集:

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

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

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

(5)一个对象被不同区域引用的问题

一个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;

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

(6)运作流程

不计算维护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,并且在此过程中压缩和释放内存;

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

img

img

8. 总结

收集器串行、并行or并发新生代/老年代算法目标适用场景
Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式
Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式、CMS的后备预案
ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合
Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务
Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务
CMS并发老年代标记-清除响应速度优先集中在互联网站或B/S系统服务端上的Java应用
G1并发both标记-整理+复制算法响应速度优先面向服务端应用,将来替换CMS

(1)串行回收算法:会停止当前应用进程,回收垃圾,停顿时间久,吞吐量大,响应时间长

(2)并行回收算法: 是多个线程同时执行串行回收算法(多核),也会使应用停顿,吞吐量大,响应时间长,用户体验差;

(3)并发回收算法:应用和垃圾回收多个线程并发执行,吞吐量相对小,响应时间短,用户体验好;

(4)G1 : 并发 + 并行回收 + 标记管理;

9. 思考:吞吐量优先和响应优先的垃圾收集器如何选择

(1)吞吐量优先

新生代采用Parallel Scavenge,老年代采用Parallel Old,并配置多个线程进行回收,设置参数调整最大垃圾收集停顿时间和吞吐量的大小;

(2) 响应时间优先

设置老年代CMS(最短时间),新生代采用ParNew(多线程,且能和CMS搭配)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值