JVM
java虚拟机的基本结构如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qb1Y32Ye-1606446816789)(D:\笔记\img\JVM.png)]
1)类加载子系统负责从文件系统或者网络中加载Class信息,加载的类信息存放于一块称为方法区的内存空间方法区中可能还会存放运行时常量池信息,包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。
2)java堆在虚拟机启动的时候建立,它是java程序最主要的内存工作区域。几乎所有的java对象实例都存放在java堆中。堆空间是所有线程共享的。
3)java的NIO库允许java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通常访问直接内存的速度会优于java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
4)垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。其中,java堆是垃圾收集器的工作重点。java中所有的对象空间释放都是隐式的,也就是说,java中没有类似free()或者delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括java堆、方法区和直接内存中的全自动化管理。
5)每一个java虚拟机线程都有一个私有的java栈,一个线程的java栈在线程创建的时候被创建,java栈中保存着帧信息,java栈中保存着局部变量、方法参数,同时和java方法的调用、返回密切相关。
6)本地方法栈和java栈非常类似,最大的不同在于java栈用于方法的调用,而本地方法栈则用于本地方法的调用,作为对java虚拟机的重要扩展,java虚拟机允许java直接调用本地方法(通常使用C编写)
7)PC(Program Counter)寄存器也是每一个线程私有的空间,java虚拟机会为每一个java线程创建PC寄存器。在任意时刻,一个java线程总是在执行一个方法,这个正在被执行的方法称为当前方法。如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。如果当前方法是本地方法,那么PC寄存器的值就是undefined
8)执行引擎是java虚拟机的最核心组件之一,它负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后再执行。
类装载系统
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking)
类装载的条件
Class只有在必须要使用的时候才会被装载,Java虚拟机不会无条件的装载Class类型。Java虚拟机规定:一个类或者接口在初次使用时,必须进行初始化。
主动引用
- 遇到new、get static、put static或invoke static这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。
- 当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
- 当使用jdk7+的动态语言支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发器 初始化。
被动引用
一个类如果是被动引用的话,该类不会触发初始化过程
- 1)通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义该字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。
- 2)通过数组定义来引用类,不会触发此类的初始化。
- 3)常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
类装载的整个过程
加载类
加载类时,JVM必须完成:
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数据流为方法区内的数据结构
- 创建java.lang.Class类的实例,表示该类型
建立连接
-
验证类:
当类被加载到系统后,就开始连接操作,验证是连接的第一步。
主要目的是保证加载的字节码是符合规范的。
-
准备
当一个类验证通过后,虚拟机就会进入准备阶段,在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置初始值。
final 不会赋值,在初始化阶段显式赋值
-
解析类
在准备阶段完成后,就进入了解析阶段。
解析阶段的任务就是将类、接口、字段和方法的符号引用转为直接引用。
符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关。比较容易理解的就是在Class类文件中,通过常量池进行大量的符号引用。
初始化
初始化时类装载的最后一个阶段。如果前面的步骤没有出现问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。
初始化阶段的重要工作是执行类的初始化方法。方法是由编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。
已加锁
类加载器分类
引导类加载器
bootClassLoder 启动加载器 引导类加载器
引导类类加载器使用c/c++实现,加载java 核心类库
加载 核心库
自定义类加载器
ExtClassLoder类 扩展类加载器
AppClassClassLoder类 系统类加载器
所有派生ClassLoder抽象类的类
为什么需要自定义加载类
- 隔离加载类
- 修改类加载的方式
- 扩展加载源
- 防止源码泄露
双亲委派机制
加载类时,先交给父类的加载器加载
优势:
避免类的重复加载
保护程序安全,防止类核心api被随意篡改
沙箱安全机制:对于核心类库的保护
PC寄存器
pc寄存器概念:用于储存下一条指令地址,由执行引擎读取
问题:pc寄存器储存字节码指令有什么用
多线程,pc寄存器可以知道要继续在哪里运行
为什么pc要记录当前线程执行地址
让jvm执行引擎明确下一条的指令
java堆
java堆是和应用程序关系最为密切的内存空间,几乎所有的对象都存放在堆上。并且java堆是完全自动化管理的,通过垃圾回收机制,垃圾对象会被自动清理,而不需要显示的释放。
在堆 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中)。而对象实例在Heap中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
根据java回收机制的不同,java堆有可能拥有不同的结构。最为常见的一种构成是将整个java堆分为新生代和老年代。其中新生代存放新生对象或者年龄不大的对象,老年代则存放老年对象。新生代有可能分为eden区、s0区、s1区,s0区和s1区也被称为from和to区,他们是两块大小相同、可以互换角色的内存空间。
java栈
-Xss 设置栈的大小
java栈是一块线程私有的内存空间。,每次函数调用的数据都是通过java栈传递的。每个方法被执行的时候都会同时创建一个栈帧,用于存放局部变量表(基本类型、对象引用)、操作数栈、方法返回、常量池指针等信息。
java栈与数据结构上的栈有着类似的含义,它是一块先进后出的数据结构,只支持出栈和进栈两种操作,在java栈中保存的主要内容为栈帧。每一次函数调用,都会有一个对应的栈帧被压入java栈,每一个函数调用结束,都会有一个栈帧被弹出java栈。当前正在执行的函数所对应的帧就是当前帧(位于栈顶),它保存着当前函数的局部变量、中间计算结果等数据。
当函数返回时,栈帧从java栈中被弹出,java方法区有两种返回函数的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧
栈帧由三部分组成:局部变量区、操作数栈、帧数据区。
栈帧组成之局部变量表
Slot 变量表的存储单元
它用于保存函数的参数以及局部变量
局部变量表中的变量只在当前函数调用中有效,当函数调用结束,随着函数栈帧的弹出销毁,局部变量表也会随之销毁。
由于局部变量表在栈帧之中,因此,如果函数的参数和局部变量很多,会使得局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。
栈帧组成之操作数栈
Java没有寄存器,故所有参数传递使用Java栈帧里的操作数栈,操作数栈被组织成一个以字长为单位的数组,它是通过标准的栈操作-入栈和出栈来进行访问,而不是通过索引访问。它主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
帧数据区
存放一些用于支持常量池解析(常量池指针)、正常方法返回以及异常派发机制的信息。即将常量池的符号引用转化为直接地址引用、恢复发起调用的方法的帧进行正常返回,发生异常时转交异常表进行处理。
此外,当函数返回或者出现异常时,虚拟机必须恢复调用者函数的栈帧,并让调用者函数继续执行下去。对于异常处理,虚拟机必须有一个异常处理表,方便在发生异常时找到处理异常的代码,因此异常处理表也是帧数据区中重要的一部分
栈上分配
栈上分配是java虚拟机提供的一项优化技术,它的基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将他们打散分配到栈上,而不是分配到堆上。分配到栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。
栈上分配的一个技术基础是进行逃逸分析,逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。
栈顶缓存技术
将栈顶元素存入pc寄存器,以提升速率
动态连接
堆与栈
堆与栈里存什么
- 1)堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用。
- 2)为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java中参数传递时的问题。
- 3)Java中的参数传递时传值呢?还是传引用?程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。
堆内存与栈内存的区别
- 申请和回收方式不同:栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
- 碎片问题:对于栈,不会产生不连续的内存块;但是对于堆来说,不断的new、delete势必会产生上面所述的内部碎片和外部碎片。
- 申请大小的限制:栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间,就会产生栈溢出;对于堆,是向高地址扩展的数据结构,是不连续的内存区域。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
- 申请效率的比较:栈由系统自动分配,速度较快。但程序员是无法控制的;堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
方法区
主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(比如spring 使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息等)
常量池
常量池的好处
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
- (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
- (2)节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。
基本类型的包装类和常量池
java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean。
这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 两种浮点数类型的包装类Float,Double并没有实现常量池技术。
String与常量池
在同包同类下,引用自同一String对象.
在同包不同类下,引用自同一String对象.
在不同包不同类下,依然引用自同一String对象.
在编译成.class时能够识别为同一字符串的,自动优化成常量,引用自同一String对象.
在运行时创建的字符串具有独立的内存地址,所以不引用自同一String对象.
本地方法栈
Native Method Stack
访问本地方式时使用到的栈,为本地方法服务, 也就是调用虚拟机使用到的Native方法服务。也会抛出StackOverflowError和OutOfMemoryError异常。
PC寄存器
每个线程都拥有一个PC寄存器,线程私有的。
PC寄存器的内容总是下一条将被执行指令的"地址",这里的"地址"可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,则程序计数器内容为undefined,区域在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
垃圾回收
按代实现垃圾回收
新生代(Young generation):
绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可到达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为”minor GC“。
新生代中存在一个Eden区和两个Survivor区。新对象会首先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden 中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代(具体细节将在下边垃圾收集算法中讨论)。
可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与新生代的比例。例如 -XX:NewRatio=8 指定老年代/新生代为8/1. 老年代占堆大小的 7/8 ,新生代占 1/8 .(默认即使1/8)
例如:-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
老年代(Old generation):
对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代少得多。对象从老年代中消失的过程,可以称之为”major GC“(或者”full GC“)
永久代(permanent generation):
像一些类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池(jdk7之后移出永久代),已确定的符号引用和虚方法表等等,它们几乎都是静态的并且很少被卸载和回收,在JDK8之前的HotSpot虚拟机中,类的这些“永久的”数据存放在一个叫做永久代的区域。永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小。但是jdk8之后取消了永久代,这些元数据被移到了一个与堆不相连的本地内存区域 。
怎样判断对象是否已经死亡
引用计数收集算法
用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象(不是引用)都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象+1),但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
- 优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
- 缺点: 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
可达性分析算法
通过一系列称为”GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(从GC Roots到此对象不可达),则证明此对象是不可用的。
可作为GC Roots的对象包括:
- 虚拟机栈中所引用的对象(本地变量表)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象(Native对象)
java中的引用
强引用(Strong Reference):
在代码中普遍存在的,类似”Object obj = new Object”这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
软引用(Sofe Reference):
有用但并非必须的对象,可用SoftReference类来实现软引用,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存异常异常。
弱引用(Weak Reference):
被弱引用关联的对象只能生存到下一次垃圾收集发生之前,JDK提供了WeakReference类来实现弱引用。
虚引用(Phantom Reference):
也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK提供了PhantomReference类来实现虚引用。
finalize方法什么作用
对于一个对象来说,在被判断没有 GCroots 与其相关联时,被第一次标记,然后判断该对象是否应该执行finalize方法(判断依据:如果对象的finalize方法被复写,并且没有执行过,则可以被执行)。如果允许执行那么这个对象将会被放到一个叫F-Query的队列中,等待被执行。(注意:由于finalize的优先级比较低,所以该对象的的finalize方法不一定被执行,即使被执行了,也不保证finalize方法一定会执行完)
垃圾收集算法
标记-清除算法:
标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
复制算法:
这种收集算法将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,JVM生成的新对象则放在另一半空间中。GC运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。并且对于指定大小堆来说,需要两倍大小的内存,因为任何时候都只使用其中的一半。
标记整理算法:
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往一端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
分代收集算法:
在上边三种收集思想中加入了分代的思想。
Hotspot实现垃圾回收细节
一致性:
在可达性分析期间整个系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况。
一致性要求导致GC进行时必须停顿所有Java执行线程。(Stop The World)即使在号称不会发生停顿的CMS收集器中,枚举根节点时也是必须停顿的。
HotSpot使用的是准确式GC,当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,这是通过一组称为OopMap的数据结构来达到的。
安全点(Safe Point):
程序只有在到达安全点时才能暂停。安全点的选定标准是“是否具有让程序长时间执行的特征”。“长时间执行”的最明显特征就是指令序列的复用,如方法调用、循环跳转等,具有这些功能的指令才会产生安全点。
垃圾收集器
HotSpot中几种常见的垃圾收集器:
Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
特性:
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。Stop The World
应用场景:
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。
优势:
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
ParNew收集器
特性:
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
应用场景:
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。有一个很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Serial收集器 VS ParNew收集器:
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
Parallel Scavenge收集器
特性:
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。
应用场景:
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
对比分析:
Parallel Scavenge收集器 VS CMS等收集器:
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关 注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
Parallel Scavenge收集器 VS ParNew收集器:
Parallel Scavenge收集器与ParNew收集器的一个重要区别是它具有自适应调节策略。
GC自适应的调节策略:
Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
Serial Old收集器
image.png
特性:
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
应用场景:
- Client模式:Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
- Server模式:如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old收集器
image.png
特性:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
应用场景:
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。
CMS收集器
image.png
特性:
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:
- 初始标记(CMS initial mark):初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记(CMS concurrent mark):并发标记阶段就是进行GC Roots Tracing的过程。
- 重新标记(CMS remark):重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
- 并发清除(CMS concurrent sweep):并发清除阶段会清除对象。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
缺点:
-
1)CMS收集器对CPU资源非常敏感
其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
-
2)CMS收集器无法处理浮动垃圾
CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
-
3)CMS收集器会产生大量空间碎片
CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1收集器
image.png
特性:
G1(Garbage-First)是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。
-
1)并行与并发
G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
-
2)分代收集
与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
-
3)空间整合
与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
-
4)可预测的停顿
这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
执行过程:
G1收集器的运作大致可划分为以下几个步骤:
-
1)初始标记(Initial Marking):初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
-
2)并发标记(Concurrent Marking):并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
-
3)最终标记(Final Marking):最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
-
4)筛选回收(Live Data Counting and Evacuation):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
-
何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出
- JVM98%的时间都花费在内存回收
- 每次回收的内存小于2%
- JVM98%的时间都花费在内存回收