对象的实例化
创建对象的方式
new
最常见的方式
变形一:静态方法
变形二:XXXBuilder、XXXFactory的静态方法
Class的newInstance():反射方式,只能调用空参的构造器,权限必须public
Construct的newInstance(Xxx):反射方式,可以调用空参或有参的构造器,权限没有要求
clone():当前类实现Cloneable接口,实现clone()
反序列化:从文件,从网络中获取一个对象的二进制流
第三方库Objenesits:字节码技术
创建对象的步骤
判断对象对应的类是否加载、链接、初始化
为对象分配内存
如果内存规整
指针碰撞
如果内存不规整
虚拟机需要维护一个列表
空闲列表分配
说明
处理并发安全问题
采用CAS配上失败重试保证更新的原子性
每个线程分配一块TLAB(Thread Local Allocation Buffer)
线程本地分配缓存区
通过-XX:+/-UseTLAB参数来设定
初始化分配的空间
所有属性设置为默认值,保证对象实例字段在不赋值时可以直接使用
对象实例化过程中给对象的的属性赋值的操作
属性的默认初始化
显示初始化
代码块初始化
构造器初始化
设置对象头信息
属性的显示、代码块、构造器初始化
对象的内存布局
对象头(Heeder)
包含两部分
运行时数据(Mark Word)
哈希值
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳
类型指针
指向类元数据InstanceKlass,确定该对象所属的类型
说明:如果是数组,还要记录数组的长度
实例数据(Instance Data)
说明
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
规则
相同宽度的字段总是被分配在一起
父类中定义的变量会出现在子类之前
如果CompactFields参数为true(默认):子类的窄变量可能插入到父类的空隙
1、这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
2、分配策略参数-XX:FieldsAllocationStyle
3、HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
4、从默认的分配策略中可以看出,相同宽度的字段总被分配到一起存放。
5、在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
6、如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认也是true),那么子类中较窄的变量也允许插入父类变量的空隙之间,以节省一点点空间。
对齐填充(padding)
不是必须的,也灭特别含义,仅仅起到占位符的作用
对象的访问定位
【使用句柄】访问
使用句柄,Java堆中将划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄包含对象实例数据与类型数据各自的具体信息。
【直接指针】
使用指针,reference中存储的直接就是对象地址,如果访问对象本身,不需要多一次的间接访问的开销。
两种方式各有优势:
使用句柄最大好处是reference中存放的是稳定句柄地址,在对象被移动(垃圾搜集时会产生)时只改变句柄中实例数据指针,reference本身不用改变。
使用指针最大好处就是速度快,节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,所以积少成多也是一项可观的执行成本。
HotSpot主要是用指针,进行对象访问(例外情况,如果使用Shenandoah收集器的话,也会有一次额外的转发)。
直接内存
概述:元空间使用的是直接内存
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域
直接内存是在java堆外的,直接向系统申请的内存区间
来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存
通常,访问直接内存的速度会优于Java堆,即读写性能高
因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存
Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
IO NIO对比
IO
byte[]/char[]
面向流Stream
NIO非阻塞IO阻塞式
Buffer
Channer通道
也可能导致OOM异常
直接内存在堆外,所以大小不受限于-Xmx指定的最大堆大小
但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
缺点
分配回收成本较高
不受JVM内存回收管理
直接内存大小可以通过MaxDirectMemorySize设置
如果不指定,默认与堆的最大值-Xmx参数值一致
执行引擎
概述
执行引擎是Java虚拟机核心的组成部分之一
“虚拟机”是一个相对于“物理机的概念。这两种机器都有代码执行能力,区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,二虚拟机的执行引擎则是有软件自行实现的,因此可以不受屋里条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
执行引擎的工作过程
执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖PC寄存器
每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址
当然方法在执行过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
任务
将字节码指令解释、编译为对应平台上的本地机器指令,简单地说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
什么是解释器
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
工作机制
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码执行解释操作。
分类
古老字节码解释器
在执行时通过纯软件代码模拟字节码的执行,效率低下
普遍使用的模板解释器
将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。
Hotspot的解释器
构成
Interpreter:实现解释器的核心功能
Code:用于管理Hotspot VM在运行时生成的本地机器指令
什么是JIT
Just In Time compile:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
既然Hotspot vm中已经内置了JIT编译器了,为什么还需要在使用解释器来“拖累”程序的执行性能呢
首先明确:当程序启动后,解释器可以马上发挥作用,省去编译时间,立即执行。编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,效率高。
子主题 2
内嵌的两个JIT编译器
-client:指定Java虚拟机运行在Client模式下,并使用C1编译器;C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
优化策略
方法内联:将引用的函数代码编译到引用各处,这样可以减少栈帧的生成,减少参数传递以及跳转过程。
去虚拟化:队唯一的实现类进行内联
冗余消除:在运行期间把一些不会执行的代码折叠掉
-Server:使用C2编译器,C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
基于逃逸分析
标量替换:用标量值代替聚合对象的属性值
栈上分配:对于未分配的对象分配对象在栈而不是堆
同步消除:清楚同步操作,通常指synchronized
StringTable
基本特性
字符串,使用“”表示。
String s1 = ""
String s2=new String("")
声明为final,不可被继承
实现了serializable接口:字符串支持序列化。实现Comparable接口,表示string可以比较大小
8以前内部定义了final char[] value用于储存字符串数据,9时改为byte[].
jdk9更改动机
char数组一个char占16bits,String是堆空间的主要部分,大部分是latin-1字符,,一个字节就够了,这样会有一半空间浪费
中文等UTF-16 的用两个字节存储。
StringBuffer,StringBuilder同样做了修改
String代表不可变的字符序列 简称不可变性
当字符串重新赋值,需要重写指定内存区域赋值,不能使用原有的value进行赋值
当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能对使用原有的value进行赋值
当调用String的replace方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
字符串常量池中不会存储相同的字符串的
String的String pool是一个固定大小的HashTable,默认大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了,直接影响就是调用String.intern时性能会大幅下降
-XX:StringTableSize可设置StringTable的大小
JDK6固定1009,jdk7中StringTable默认的长度是60013,JDK8时默认是60013,1009是可设置的最小值
String的内存分配
Java语言中有8种基本数据类型和一种比较特殊的类型String,这些类型为了使他们再运行过程中速度更快,更节省内存,都提供了一种常量池的概念
String的常量池比较特殊,主要使用方法有两种
直接使用双引号,声明出来的String对象会直接存储在常量池中
如果不是双引号声明的String对象,可以使用String提供的intern()方法
jdk6及之前,字符串常量池存在永久代
jdk7中,字符串常量池调整到Java堆中
调优时仅需调整堆大小就可以
Jdk8中,元空间,字符串常量在堆
为什么要调整?
永久代默认情况下比较小,大量字符串容易导致OOM。
永久代垃圾回收频率低,
字符串拼接操作
常量与常量的拼接结果在常量池,原理是编译期优化
常量池中不会存在相同内容的常量
只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
字符串拼接操作不一定使用的是StringBuilder如果拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译期优化,即非StringBuilder的方式
建议:针对于final修饰类,方法,基本数据类型,引用类型的量的结构时,能用final尽量用
如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址。
如何保证变量指向的是字符串常量池中的数据?
字面量方式:String s="XXX"
调用intern方法
intern()方法
面试题:
new String(“ab")会创建几个对象?
两个。看字节码。new关键字在堆空间创建。字符串常量池中的对象。字节码指令ldc
new String("a")+new String("b")?
对象1,有拼接操作就newStringBuilder
对象2,new一个String
对象3,常量池a
对象4,new String
对象5,常量池b
对象6,StringBuilder,toString方法会new String返回
此时字符串常量池中没有ab
垃圾回收概述
什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的
如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用成功程序结束,被保留的空间无法被其他对象使用,可能导致内存溢出。
为什么需要GC
对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被耗完。
除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片,碎片整理将所占用的堆内存移到堆的一端以便 JVM将整理出的内存分配给新的对象
随着应用庞大,没有GC就不能保证应用程序的正常进行
垃圾回收相关算法
标记阶段:引用计数算法
对象存活判断
在堆里存放着几乎所有的对象实例,在GC执行回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象GC才会在执行垃圾回收时,释放掉其所占用的内存空间
那么JVM如何标记一个死亡对象?简单地说,当一个对象已经不再被任何的存活的对象继续引用时,就可以宣判死亡。
判断对象存活两种方式
引用计数法
对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况
引用+1,引用失效-1,计数器为0,可进行回收
可达性分析算法
优点
实现简单,垃圾对象便于辨识‘判定效率高,回收没有延迟
缺点
需要独立的字段存储计数器,这样做法增加了存储空间的开销
每次赋值都需要更新计数器,伴随着加减法的操作,增加了时间开销
有一个严重问题:无法处理循环引用的情况,致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
小结
引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾回收机制
具体哪种最优是看场景,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试
Java并没有选择引用计数,因为其致命缺陷-循环引用
Python如何解决?
手动解除:在合适时机,解除引用
使用弱引用weakref,是Python提供的标准库,旨在解决循环引用
标记阶段:可达性分析算法
相对于引用而言,它不仅同样具备实现简单和效率高效等特点,更重要的是可以有效地解决在引用计数算法中循环引用的问题,防止内存内泄漏的发生
Java、C#选择的。这种那个类型的垃圾收集也叫作追踪性垃圾收集
基本思路
GC Roots根集合就是一组必须活跃的引用
以根对象集合为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
使用它后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径被称为引用链
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
高频:在Java中,GC Roots包含以下几类元素
虚拟机栈中引用的对象
各个线程被调用的方法中使用到的参数、局部变量等
本地方法栈内JNI引用的对象
方法区中静态属性引用的对象
类的引用类型静态变量
方法区中常量引用的变量
字符串常量池里的引用
所有被同步锁sysnchronized持有的对象
虚拟机内部的引用
基本数据类型对应的Class对象,一些常驻的异常对象(NullPointerException、OutOfMemoryError)系统类加载器
反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存
小技巧
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么它就是一个Root
对象的finalization机制
除了固定的GC Roots集合之外,根据用户选择的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象临时性的加入,共同构成完整GCRoots集合,比如分代收集和局部回收
如果只针对Java堆中某一块内存区域进行垃圾回收,必须要考虑这个区域的对象可能被其他区域对象所引用,这是需要一并将关联的区域对象加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
Java语言提供了对象终止finaliztion机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
当垃圾回收器发现没有引用指向一个对象,即垃圾回收此对象之前,总会先调用这个对象的finalize()方法
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放,通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件,套接字和数据库链接等
定义虚拟机的对象可能的三种状态
可触及的
从根节点开始,可以到达这个对象
可复活的
对象的所有引用都被释放了,但是对象有可能在finalize()中复活
不可触及的
对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次
只有对象再不可触及时才可以被回收
具体过程
判断一个对象ObjA是否可以被回收,至少需要经历两次标记过程
1、如果对象到GCRoots没有引用链,则进行第一次标记
2、进行筛选,判断此对象是否有必要执行finalize()方法
如果对象A没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为没有必要执行,对象A被判定为不可触及的
如果对象A重写finalize()方法,且还未执行过,那么A会被插入到F-queue队列中,有一个虚拟机自动创建的,低优先级的Finalizer线程触发其finalize()方法执行
finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-queue队列中的对象进行第二次标记,如果A在finalize方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,A会被移除即将回收集合。之后,对象会再次出现没有引用存在的情况下,finalize方法不会再被调用,对象直接变为不可触及状态
MAT与JProfiler的GC Roots硕源
MAT是Memory Analyzer的简称,是一款功能强大的Java堆内存分析器。用于查找内存泄露以及查看内存消耗情况,基于Eclipse开发的一款免费性能分析工具
清除阶段:标记清除算法
执行过程
当堆中的有效内存空间被耗尽的时候,就会停止整个程序(STW),然后进行两项工作,标记-清除。
标记:Collector从根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
清除:Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
缺点
效率不算高
在进行GC的时候,需要停止整个应用程序,导致用户体验差
这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
何为清除?
这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。
清除阶段:复制算法
核心思想
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存角色,最后完成垃圾回收。
优点
没有标记和清除过程,运行高效
复制过去以后保证空间的连续性,不会出现“碎片问题”
缺点
两倍的内存空间
对于G1这种拆分成为大量region的GC,福祉而不是移动,意味着GC需要维护region之间对象的引用关系,不管是内存占用或者时间开销也不小
特别地
如果系统中的垃圾对象很多,复制算法不太理想。因为复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
清除阶段:标记-压缩(整理)算法
背景
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代,更常见的1情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代的垃圾回收的特性,需要使用其他的算法。标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所有JVM的设计者在此基础上进行改进。标记-压缩由此诞生。
执行过程
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一段,按顺序排放。
之后,清理边界外所有的空间
优点
清除了标记-清除算法当中,内存区域分散的的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
清除了复制算法中,内存减半的高额代价
缺点
从效率上说,标记-整理算法低于复制算法
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
移动过程中,需要全程暂停用户应用程序。SWT
小结
分代收集算法
不同生命周期的对象可以采取不同的收集方式,以便提高回收效率
spot中
年轻代
特点
区域相对老年代小
对象生命周期短
对象存活率低
回收频繁
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关。因此很适合用于年轻代的回收。而复制算法内存利用率不高的问题,通过Hotspot中的两个survivor的设计得到缓解。
老年代
特点
区域较大
对象生命周期长
存活率高
回收不及年轻代频繁
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理混合实现
Mark阶段的开销与存活对象的数量成正比
Sweep阶段的开销与所管理区域的大小成正比
compact阶段的开销与存活对象的数据成正比
增量收集算法、分区算法
增量收集算法思想
每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成
通过对线程间冲突的妥善管理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作
缺点
线程和上下文切换导致系统吞吐量的下降
垃圾回收相关概念的概述
System.gc()的理解
在默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显式出发full GC,同时对老年代he新生代进行回收,会尝试释放被丢弃对象占用的内存。
然而System.gc()调用无法保证对垃圾收集器的调用
JVM实现者可以通过System,gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无需手动触发,否则则就太过于麻烦了。
内存溢出与内存泄露
OOM
java 虚拟机的堆内存设置不够
代码创建大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
内存泄露
只有对象不再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露
实际情况有一些疏忽导致对象的生命周期变的很长甚至OOM,宽泛意义上的内存泄露
举例
单例的生命周期和程序是一样长,如果单例程序中,持有对外部对象的引用的话,那么这个外部对象是不能被回收的,导致内存泄露
一些提供close的资源未关闭导致内存泄露,如数据库链接,网络链接,和IO
STW
Stop the world,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应。
可达性分析算法枚举根节点导致stw
分析工作必须在一个能确保一致性的快照中进行
一致性指整个分析期间真个执行系统看起来像被冻结在某个时间点
如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
开发中不要用System.gc();会导致stw的发生
垃圾回收中的并行和并发
并发
在操作系统中,是指一个时间段中有几个程序都处于以启动运行到运行完毕之间,这几个程序都在同一个处理器上
并发并不是真正的“同时进行”,CPU把一个时间段划分成几个时间区间,然后再这几个时间区间来回切换,CPU处理非常快,只要时间间隔处理得当,用户就感觉是多个程序同时在进行
并行
当系统有一个以上CPU,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行。
决定并行的1因素并不是CPU的数量,而是CPU的核心数量,多核也可以并行
VS
执行时间
并发,多个事情,在同一时间段同时发生
并行,多个事情,在同一时间点同时发生
资源
并发多个任务之间互相抢占资源
并行不抢
只有在多CPU或一个CPU多核的情况下,才会发生并行
安全点和安全区域
安全点
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿开始GC。
选择
太少可能导致GC等待时间太长,如果太频繁可能导致运行时对的性能问题。选择一些执行时间较长的指令作为安全点,方法调用,玄幻跳转,异常跳转
GC发生时,检查所有县城都跑到碎金的安全点停顿下来?
抢先式中断
没有虚拟机使用。首先中断所有线程,如果有线程不在安全点,让线程跑到安全点
主动式中断
设置一个中断标志,各个线程运行到安全点的之后主动轮询这个标志,如果中断标志为真,将自己中断挂起
安全区域
安全点机制保证程序在执行时,在不太长时间内就会遇到可进入GC的安全点。但是,程序“不执行”的时候呢?例如线程处于sleep或blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。这时候就需要安全区域来解决
安全区域是在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
再谈引用
四种引用
强引用
永不回收。Object o = new Object()这种关系,无论在任何情况下,只要强引用关系存在,垃圾收集器就永远不会回收被引用的对象
强引用是造成java内存泄露的主要原因之一
强引用可以直接访问目标对象
软引用
内存不足才回收。在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常
弱引用
发现即回收。在弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象
可有可无的缓存数据
虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知
可以跟踪对象的回收时间,可以将一些资源释放操作放置在虚引用中执行和记录
面试题
你开发中用过WeakHashMap吗
内存溢出
OOM
java 虚拟机的堆内存设置不够
代码创建大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
垃圾回收器
GC分类与性能指标
垃圾回收器分类
按垃圾回收线程数
可以分为串行垃圾回收器
截图
串行回收指同一个时间段内,只允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直到垃圾收集工作结束
在单CPU处理器或者较小应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以串行回收默认被应用在客户端的client模式下的JVM中
在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器
并行垃圾回收器
和串行相反,并行收集可以运用在多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了STW机制
按照工作模式分
并发式
垃圾回收器与应用程序交替工作,以尽可能减少应用程序的停顿时间
独占式
一旦运行,就停止应用程序中所有的用户线程,直到垃圾回收过程完全结束
按照碎片处理方式
压缩式
非压缩式
按个工作内存区间分
年轻代
老年代
性能指标
吞吐量
运行用户代码的时间占总运行时间的比例
总运行时间:程序的运行时间+内存回收的时间
吞吐量优先,意味着单位时间内,STW的时间最短
垃圾收集开销
吞吐量的补数,垃圾收集所占用的时间与总运行时间的比例
暂停时间
执行垃圾收集时,程序的工作线程被暂停的时间
暂停时间优先,意味着单次STW的时间最短,但是频率可能增加
收集频率
相对于应用程序的执行,收集操作发生的频率
内存占用
Java堆区所占的内存大小
快速
一个对象从诞生到被回收经历的时间
不可能三角
简单来说抓住两点,吞吐量和暂停时间
高吞吐量与低暂停时间,是一对互相竞争的。因为如果高吞吐量优先,必然需要降低内存回收的执行频率,导致GC需要更长的暂停时间来执行内存回收。
如果选择低延迟优先为原则,也只能频繁的执行内存回收,引起程序吞吐量的下降
现在的标准,在最大吞吐量优先的情况下,降低停顿时间
不同的垃圾回收器概述
垃圾回收器的发展迭代史
Serial GC
1999年jdk1.3.1
第一款GC
ParNew
是SerialGC收集器的多线程版本
Parallel GC和Concurrent Mark SweepGC
jdk1.4.2
2002年2月26日
ParallelGC在JDK1.6之后称为HotSpot默认GC
G1
2012年
jdk1.7u4
2017年JDK9中G1变成默认的垃圾收集器,以替代CMS
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性改善最坏情况下的延迟
Epsilon 垃圾回收器、ZGC,可伸缩低延迟垃圾回收器
2018年9月JDK11
Shenandoah GC:低停顿时间的GC,实验版
2019年3月JDK12
增强ZGC
2019年9月JDK13
删除CMS垃圾回收器,扩展ZGC在macOS和Windows上的应用
2020年3月JDK14
7款经典垃圾收集器和垃圾分代之间的关系
截图
垃圾收集器的组合关系
jdk8之前,可以用虚线参考关系
CMS下面的实线,是CMS回收失败的后备方案
JDK8中取消了红线的组合,标记为废弃的。如果要用也可以用。
JDK9中将红线做了remove
jdk14中弃用了绿线组合
jdk14中删除了CMSGC
JDK9默认G1
JDK8默认Parallel Scavenge 和Parallel old Gc
新生代用了Parallel Scavenge 则老年代自动触发用Parallel old
Parallel底层与ParNew底层不同,所以不能和CMS组合
如何查看默认的垃圾收集器
-XX:+PrintCommandLineFlags
jinfo -flag 相关垃圾回收器参数 进程ID
截图
Serial回收器:串行回收
Serial收集器采用复制算法,串行回收和STW机制的方式执行内存回收
除了年轻代,还有用于执行老年代的Serial old收集器,同样采取了串行回收,但是用标记压缩算法
截图
使用一个CPU或者一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须暂停其他所有工作线程
优势
简单而高效,对于限定单个CPU的环境来说,由于没有线程交互的开销,可以获取最高的单线程收集效率
HotSpot虚拟机中,使用-XX:+UseSerialGC指定年轻代和老年代使用串行收集器
对于交互强的应用而言,不会采取串行垃圾收集器
ParNew回收器:并行回收
除了采用并行回收,其他方面和Serial之间几乎没有任何区别
截图
-XX:UseParNewGC手工指定ParNew收集器执行内存回收任务,它表示年轻代使用,不影响老年代
-XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数
Parallel回收器:吞吐量优先
也是并行回收
和ParNew不同,它的目标是达到一个可控制的吞吐量
自适应调节策略也是Parallel 与ParNew的一个重要区别
适合后台运算不需要太多交互的任务,例如执行批量处理,订单处理,工资支付,科学计算的应用程序
Parallel old采取标记压缩算法,同样基于并行回收和STW机制
参数配置
-XX:+UseParallelGC
手动指定年轻代使用此收集器执行内存回收任务
-XX:+UseParallelOldGC
手工指定老年代使用并行回收收集器,分别适用于新生代和老年代,默认jdk8是开启的
与上面这两个参数关联,开启一个,默认开启另一个。
-XX:ParallelGCThreads
设置年轻代并行收集器的线程数,一般与CPU数量相同,如果CPU数量大于8个,则值=3+(5*N/8)
-XX:MaxGCPauseMillis
设置收集器最大停顿时间,单位毫秒
改参数谨慎使用
-XX:GCTimeRatio
垃圾收集占总时间比,用于衡量吞吐量大小
默认99,取值范围0-100,也就是垃圾回收时间不超过1%
与上一个参数矛盾,暂停时间越长,Ratio参数就容易超过设定比例
-XX:+UseAdaptiveSizePolicy
开启自适应调节策略
这种模式下,年轻代大小,Eden和Survivor的比例,晋升老年底对象年龄参数都会被自动调整
为了达到堆大小,吞吐量和停顿时间之间的平衡点
在手动调优比较困难的场景下,可以直接用自适应方式,仅指定虚拟机最大堆,目标吞吐量和停顿时间,让虚拟机自己完成调优工作
CMS回收器:低延迟
jdk1.5推出Concurrent Mark Sweep 并发的标记清除,第一次实现了让垃圾收集线程与用户线程同时工作
截图
初始标记:STW,仅仅只是标记处GC Roots能直接关联的对象,一旦标记完成后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较小,所以这里速度非常快
并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程。可以与垃圾收集线程一起并发运行
重新标记:为了修正并发标记期间,因用户程序继续运作导致标记产生变动的那一部分对象的标记记录
并发清除:清理删除标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也可以与用户线程同时并发
初始标记和重新标记阶段仍然需要STW机制
由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此CMS收集器不能像其他收集器那样等到老年代几乎填满再进行回收,而是当堆内存使用率达到某一阈值时,便开始进行回收。
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次Concurrent Mode Failure失败,这时虚拟机启用备用方案,临时启用Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就长了。
CMS采取标记清除算法,会产生内存碎片,只能够选择空闲列表执行内存分配
为什么不采取标记压缩呢?
因为并发清除时,如果用压缩整理内存,原来的用户线程使用的内存就无法使用了。标记压缩更适合STW场景下使用
优点
并发收集
低延迟
缺点
会产生内存碎片
对CPU资源非常敏感
在并发阶段会占用一部分线程导致应用程序变慢
无法处理浮动垃圾
并发标记阶段是与工作线程同时运行,如果并发阶段产生垃圾对象,CMS无法进行标记,导致新产生的垃圾对象没有被及时回收,只能在下一次执行GC时释放空间
参数
-XX:+UseConcMarkSweepGC
手工指定CMS收集器执行内存回收任务
开启后,自动将-XX:UseParNewGC打开,即ParNew(Young区)+CMS(old区)+Serial GC组合
-XX:CMSlnitiatingOccupanyFraction
设置堆内存使用率的阈值
一旦达到该阈值,则开始进行回收
jdk5及之前默认68,即老年代的空间使用率达到68%时会执行一次CMS回收
JDK6及以上默认值为92%
如果内存增长缓慢,可以设置一个稍大的值,有效降低CMS的触发频率,减少老年代回收的次数
如果应用程序内存使用率增加很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。
-XX:+UseCMSCompactAtFullCollection
用于执行完Full GC后对内存空间进行压缩整理
不过内存压缩无法并发执行,会带来停顿时间更长的问题
-XX:CMSFullGCsBeforeCompaction
设置执行多少次FullGC后对内存空间进行压缩整理
-XX:ParallelCMSThreads
设置CMS的线程数量
默认启动的线程数是(ParallelGCThreads+3)/4
ParallelGCThreads是年轻代并行收集器的线程数
小结
如果想要最小化使用内存和并行开销,选择Serial GC
如果最大化应用程序的吞吐量,选择ParallelGC
如果想要最小化的GC的中断或停顿时间,选择CMS GC
jdk9标记为废弃的,jdk14已经删除了
G1回收器:区域化分代式
官方给G1设定的目标
就是在延迟可控的情况下,获得尽可能高的吞吐量,所以才担当起全功能收集器的重任和期望
Garbage First
G1是一个并行回收器,他把堆内存分割为很多不相关的区域(Region)(物理上不连续)
使用不同的region表示Eden,s0,s1,老年代等
G1跟踪各个region里面垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
JDK1.7版本正式启用,jdk9以后默认垃圾回收器
JDK8还不是默认的,需要用-XX:+UseG1GC来启用
优势
并行与并发
分代收集
同时兼顾年轻代与老年代
空间整合
region之间用复制算法,整体可以看做是标记压缩算法。
两种算法都避免内存碎片,有利于程序长时间运行,分配大对象不会因为无法找到连续空间提前触发下一次GC,尤其当Java堆非常大的时候,G1优势更加明显
可预测的停顿时间模型
能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不能超过N毫秒
缺点
相较于CMS,G1不具备全方位,压倒性优势。比如用户程序运行中,G1无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比CMS要高
经验上来说,小内存应用CMS表现大概率优于G1,在大内存上G1优势发挥更多,平衡点再6-8GB
参数设置
-XX:+UseG1GC
-XX:G1HeapRegionSize
设置每个Region大小,值是2的幂,范围是1MB到32MB之间,目标是根据最小的Java堆划分出约2048个区域,默认是堆内存的1/2000
-XX:MaxGCPauseMillis
设置期望达到的最大GC停顿时间指标,JVM尽力但不保证,默认200ms
-XX:ParallelGCThread
设置STW工作线程数的值,最多设置8
-XX:ConcGCThreads
设置并发标记的线程数,将N设置为并行垃圾回收线程数(parallelGCThreads)的1/4左右
-XX:InitiatingHeapOccupancyPercent
设置触发并发GC周期的Java堆占用率阈值,超过此值就触发GC,默认是45
常见调优
第一步开启G1垃圾收集器
第二步,设置堆的最大内存
第三步,设置最大的停顿时间
G1提供了三种垃圾回收模式在不同的条件下触发
YoungGC
MixedGC
FullGC
适用场景
面向服务器端应用,针对具有大内存,多处理器的机器
最主要应用是需要低GC延迟
如:在堆大小约6GB或更大,可预测的暂停时间可以低于0.5s,G1每次清理一部分region来保证每次GC停顿时间不会过长
用来替换1.5中的CMS
超过50%的Java堆被活动数据占用
对象分配频率或年代提升频率变化很大
GC停顿时间过长,长于0.5~1秒
region
所有region大小相同,且在JVM生命周期内不会改变
截图
region可以充当多个角色
垃圾回收过程
年轻代GC
当年轻代eden区用尽时
并行独占式收集器
老年代并发标记过程
当堆内存使用到一定值,默认45%
混合回收
标记完成马上开始混合回收
G1老年代回收器不需要整个老年底都被回收,一次只需要扫描回收一小部分老年代的region就可以了。
同时这个老年代回收是和年轻代一起被回收的。
有可能fullGC
记忆集
每个region对应一个记忆集
通过记忆集避免全局扫描
每次引用类型数据写操作时,会产生一个写屏障暂时中断操作
然后检查将要希尔的引用指向的对象是否和该引用对象类型数据在不同的region,如果不同就通过CardTable把相关的引用信息记录到引用指向对象所在的Region对应的记忆集中
当进行垃圾收集时,在GC根节点枚举范围加入记忆集,就可以保证不进行全局扫描,也不会有遗漏
G1回收过程一,年轻代GC
1、扫描根
根是指static变量指向的对象,正在执行的方法调用链上的局部变量等。根引用连同Rset记录的外部引用作为扫描存活对象的入口
2、更新Rset
处理dirty card queue中的card,更新Rset,此阶段完成后,Rset可以准确的反应老年代所在的内存分段中对象的引用
3、处理Rset
识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象
4、复制对象
对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor去中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,会加一,达到阈值会被复制到old区中空的内存分段,如果Survivor区空间不够,Eden空间的部分数据会直接晋升到老年代空间
5、处理引用
处理强软弱虚,终结器引用,本地方法接口引用等,最红eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。
G1回收过程二、并发标记过程
初始标记阶段STW
标记从根节点直接可达的对象,并且触发一次年轻代GC
根区域扫描阶段
扫描Survivor区直接可达老年代区域对象,并标记被引用的对象,这个过程在youngGC之前完成
并发标记
和应用程序并发执行,并发标记阶段若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。
并发标记过程中,会计算每个区域的对象活性,存活对象的比例
再次标记
由于应用程序持续进行,需要修正上次标记结果,STW,G1采取比CMS更快的初始快照算法
独占清理
计算各个区域存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下个阶段做铺垫,STW,
这个阶段并不会实际上去做垃圾的收集
并发清理阶段
识别并清理完全空闲的区域
G1回收过程三:混合回收
当越来越多的对象晋升到老年代old region时,为了避免内存被耗尽,虚拟机会触发一次混合的垃圾收集器,该算法除了回收整个young region,还会回收一部分的old region。也要注意Mixed gc并不是fullgc
并发标记结束后,老年代中百分百为垃圾的内存分段被回收了。部分为垃圾的内存分段被计算出来了,默认情况下,这些老年代的内存分段会分8次被回收-XX:G1MixedGCCountTarget设置
混合回收的回收集包括八分之一的老年代,Eden区内存分段,Survivor区内存分段。
由于老年代中内存分段默认分8次回收,G1会优先回收垃圾多的内存分段,并且有一个阈值会决定内存分段是否被回收。-XX:G1MixedGCLiveThresholdPercent,默认为65%。意思是垃圾占比达到65%才会被回收。如果垃圾占比比较低,意味存活对象较高,复制的时候花更多时间。
混合回收不一定要进行8次,有一个阈值:-XX:G1HeapWastePercent
默认值是10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存比例低于10%,则不再进行混合回收,因为GC花费更多的时间,但是回收到的内存却很少。
G1可选过程四:fullGC
G1初衷就是要避免FULLGC,如果上述方式不能正常工作,G1会停止应用程序的执行。使用单线程的内存回收算法进行垃圾回收,性能非常差。应用程序停顿时间长
比如堆太小,当G1复制存活对象的时候没有空的内存分段可用,则会回退到FullGC
导致FullGC原因可能有两个
回收阶段的时候没有足够的to-space存放晋升的对象
并发处理过程完成之前空间耗尽了。
优化建议
避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小
固定的年轻代大小会覆盖暂停时间目标
暂停时间目标不要太苛刻,太苛刻会影响吞吐量
垃圾回收器总结
截图
GC日志分析
截图
截图
截图
截图
截图
截图
GC EASY
垃圾回收器的新发展
截图
Shenandoah GC
强项
低延迟时间
弱项
高运行负担下的吞吐量下降
ZGC
在尽可能堆吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾回收的停顿时间限制在10毫秒以内的低延迟
并发标记,并发预备重分配,并发重分配,并发重映射
除了初始标记是STW,其他地方几乎都是并发执行的
垃圾回收器