JVM全文
概述
- Lisp,第一门开始使用内存动态分配和垃圾收集技术的语言
- 垃圾,在运行程序中没有任何指针指向的对象,这个对象就是要被回收的垃圾
如果不及时清理内存中的垃圾,这些垃圾对象所占的内存空间一直保存在应用程序结束,无法被其他对象使用,导致内存溢出
- 为什么需要GC
- 如果不进行垃圾回收,内存迟早都会消耗完;
- 除了释放没用的对象,垃圾回收也可以清楚内存里的记录碎片,碎片整理将占用的内存移到堆的一端,以便JVM将整理出的内存分配给新的对象
- 应付越来越庞大、复杂的业务需求,没有GC就不能保证应用程序的正常进行,但是经常造成
STW
的GC跟不上实际需求,需要不断堆GC优化
- 内存泄漏,逻辑上并不需要这个对象了,但是这个对象根据可达性分析,仍在引用链中,被其他对象引用着
早期垃圾回收
- 手工进行
new
内存申请delete
内存释放- 灵活控制内存释放的时间,但存在频繁申请和释放内存的管理负担,容易由于个人原因,导致
内存泄漏
(对象没有被引用,但是没有被回收),垃圾对象就永远无法被清除,随着系统运行时间,垃圾对象所耗内存持续上升,直到内存溢出并造成程序崩溃
Java回收机制
- 自动内存管理,降低内存泄漏和内存溢出的风险
没有垃圾回收,会出现悬垂指针、野指针、泄漏等问题
- 针对方法区和堆空间
- 频繁收集年轻代
- 较少收集老年代
- 基本不动方法区
标记阶段算法
- 对象存活判断
- 在GC执行垃圾之前,首先需要区分出内存中哪些是存活对象,哪些事已经死亡的对象
- 只有被标记为已经死亡的对象,GC才会执行垃圾回收,释放掉其所占空间
- 一般有两种方式,
引用计数算法和可达性分析算法
引用计数算法
- Java没有选择这个算法
- 对每一个对象都保存一个
整型的引用计数器属性
,用于记录对象被引用的情况 - 具体过程
- 对于一个对象A,只要有任何一个对象引用了A,A的引用计数器+1
- 当引用失效,引用计数器-1
- 只要对象A的引用计数器值==0,表示A不可能再被使用,可进行回收,提高吞吐量
- 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟
- 缺点
- 需要单独的字段存储计数器,增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随加法和减法,增加时间开销
- 无法处理循环引用的情况
- python/OC 使用了引用计数器的标记算法
- 手动解除
- 使用弱引用,
weakref/weakSelf
可达性分析算法
- java/C#
- 根搜索算法、追踪性垃圾收集
- 解决引用计数器中循环引用的问题,防止内存泄漏
- 以根对象集合
(一组必须活跃的引用)
,按照从上至下的方式搜索根对象集合所连接的目标对象是否可达
- 内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为
引用链
- 如果目标对象没有任何引用链相连,则不可达,意味着该对象已经死亡,标记为垃圾对象
- 只有能够被根对象集合直接或者间接连接的对象才是存活对象
- GC Roots,采用栈方式存放变量和指针,一个指针保存对内存里面的对象,但又不存放在堆内存中,就是一个root
- 虚拟机栈中引用对象,局部变量表中的引用,各个线程被调用的方法中使用到的参数、局部变量
- 本地方法栈内
JNI Java Native Interface
(本地方法)引用对象- 堆空间的类静态属性引用的对象,引用类型静态变量
- 字符串常量池中的引用
- 被同步锁持有的对象
- 虚拟机内部引用,基本数据类型对应的包装类,一些常驻的异常对象(
NullPointerException、OutOfMemoryError
),系统类加载器- 反映java虚拟机内部情况的JMXBean(java程序管理)、JVMTI(虚拟机管理)中的注册回调、本地代码缓存
- 针对回收的区域不同,有其他对象临时性的加入,当对新生代进行
局部回收Partial GC
,老年代的一些对象,也会指向新生代的对象,将这个关联的区域计入GC Roots集合中
- 分析工作必须在能保障一致性的快照中进行,也就导致java进行GC必须发生
Stop The World 停下用户线程
;CMS收集器(第一个低延迟垃圾收集器,并发执行),枚举根节点是也必须要停顿
对象finalization机制
- 允许开发人员提供对象被销毁之前的自定义处理逻辑
- 当垃圾回收器发现没有引用指向一个对象时,在回收这个对象时,会调用
finalize()
,jdk9之后被弃用,不推荐手动实现,出现java.lang.ref.Cleaner和java.lang.ref.PhantomReference
,但是底层还是会出现finalizer
finalize()
运行被子类重写,用于对象被回收时进行资源释放,如关闭文件、套接字和数据库连接等
- 永远不要主动调用某个对象的
finalize()
,完全交给垃圾回收机制调用- 可能会导致对象复活
- 执行的时间没有保障,完全由GC线程决定,极端条件下,不发生GC,
finalize()
没有执行机会,可能出现的情况是在我们耗尽资源之前,gc却仍未触发- 实现的糟糕,严重影响GC性能
- 与析构函数比较相似,但是java中采用的基于垃圾回收器的自动内存管理机制,本质上不同
- 如果
finalize()
存在,对象可能有三种状态
- 可触及的,从根节点,可以到达这个对象
- 可复活的,对象的所有引用都被释放,有可能在
finalize()
中复活- 不可触及的,对象的
finalize()
被调用,且没有被复活;不可能被复活,只能被调用一次
- 存在
finalize()
的对象判定至少要经历两次标记过程
- 对象obj到GCRoots没有引用链,进行第一次标记
- 进行筛选,判断obj是否有必要执行
finalize()
- 如果没有重写
finalize()
,或者已经被虚拟机调用过,则为不可触及的- 如果重写
finalize()
,且还未执行,obj就会被插入到F-Queue(java.lang.ref.Finalizer.ReferenceQueue)
队列中,由一个虚拟机自动创建的、低优先级的Finalize
线程触发发其finalize()
执行- GC会对
F-Queue
中的对象进行第二次标记,如果obj在finalize()
中与引用链上任何一个对象建立了联系
,那么第二次标记时,obj就会被移出即将回收
集合。当对象会再次出现没有引用存在的情况,而这个情况下,finalize()
不会被调用,对象直接变成不可触及的状态- 一个对象的
finalize()
只会被调用一次
finalize()
的缺点
- finalize机制本身就是存在问题的。
- finalize机制可能会导致性能问题,死锁和线程挂起。
- finalize中的错误可能导致内存泄漏;如果不在需要时,也没有办法取消垃圾回收;并且没有指定不同执行finalize对象的执行顺序。此外,没有办法保证finlize的执行时间。
- cleaner,有自己的线程,在所有清理操作完成后,自己会被GC
- 需要实现AutoCloseable接口,这样在重写的close()方法中释放资源会被自动调用回收。
- cleaner机制需要创建单独的线程去执行逻辑,这与finalize机制不同。
- 执行finalize机制的线程不可控,所以cleaner机制不存在类似于先执行finalize逻辑在回收对象的问题,即只要执行了cleaner机制就不会降低垃圾回收效率。
- 但是前提是执行了cleaner机制,因为它的clean()方法还是写在重写的close()方法中等待被自动调用,所以无法保证保证被及时执行。
- cleaner实现
- 被监听的类需要实现Runnable接口
- 创建一个Cleaning,将类注册到里面去
- 主线程中需要初始化 Cleaning类
//类中
public class Person implements Runnable{
public Person() {
System.out.println("Person was Born");
}
@Override
public void run() {
//释放资源...
System.out.println("Person is Dead");
}
}
class PersonCleaning implements AutoCloseable {
//创建清洁处理
private static final Cleaner cleaner = Cleaner.create();
privete Person person;
private Cleaner.Cleanable cleanable;
public PersonCleaning() {
this.person = new Person();
//注册使用的对象
this.cleanable = this.cleaner.register(this, this.person);
}
@Override
public void close() throws Exception {
//启动多线程
this.cleanable.clean();
}
}
//调用
public class CleanerTest {
public static void main(String[] args) {
try (PersonCleaning p = new PersonCleaning()){
System.out.println("start");
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
MAT和JProfiler查看GCRoots
- dump文件,进程的内存镜像,可以把程序的执行状态通过调试器保存到dump文件中
- visual vm生成dump文件
- mat查看
- 底层还是有 finalizer
GCRoot溯源
,找到导致内存泄漏的对象的GCRoot- JProfiler进行GC溯源
live memory
->选中要查的类型右键show selection in heap walker
Reference
->Incoming references
被引用的引用链->Show Pahts To GC Root
- JProfile分析OOM
-XX:+HeapDumpOnOutOfMemoryError
当出现内存错误的时候,输出堆空间的dump文件- 出现的超大对象
- 出现问题的线程和代码
清除阶段算法
- 实践上,前沿GC都是复合算法,并行和并发兼备
- 当成功区分出存活对象和死亡对象后,接下来GC要执行垃圾回收,释放掉无用对象所占用的空间
标记-清除算法 Mark-Sweep
复制算法 Copying
标记-压缩算法 Mark-Compact
标记-清除算法
- 最开始被应用于
lisp语言
,比较基础和常见 - 执行过程
- 当堆中有效空间被耗尽,会停止整个程序
STW Stop The World
- 标记,
Collector
从引用根节点开始便利,标记所有被引用的对象,在对象的Header中记录为可达对象- 清除,
Collector
对堆内存从头到尾进行线性便利,如果发现某个对象的Header没有标记为可达对象,将其回收
- 缺点
- 用递归的方式遍历,标记节点,遍历在引用链中的对象;清除阶段,需要将全部的对象都遍历一边,效率不算高
- 在GC时,停止整个应用程序
STW
,用户体验差- 清除出来的空闲区域是不连续的,产生内存碎片,需要维护一个空闲列表
- 清除,并不是真的置空,而是把
需要清除的对象地址保存在空闲的地址列表
里,下次有新的对象需要加载时,判断垃圾的位置是否够
,如果够,那么就存放,覆盖掉垃圾对象的数据
复制算法
- 可以使用
指针碰撞
来为对象分配内存 - 使用双存储区的lisp语言垃圾收集器
- 将内存空间分成两块,
每一次只使用其中一块
,在垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存块
中,之后清除正在使用的内存块中的所有对象
,交换两个内存的角色
,from区和to区
,解决碎片化 - 优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间连续性,不会出现碎片化的问题
- 缺点
- 需要两倍的内存空间
- 对于G1这种分拆成大量region(对象的区域)的GC,复制而不是移动,需要
调整对象引用的引用地址
,意味着GC需要维护region之间的对象引用关系,内存占用和时间开销不小- 系统中
存活对象很多
,复制算法会提高内存占用和时间开销,复制算法需要复制的存活对象并不会太多;对应的,新生代的对象大部分都是朝生夕死的,回收性价比很高
,避免了存活对象很多的极端情况,因此在新生代中采用复制算法,s0/s1
中的对象不断在GC的过程中交换,并增加对象的年龄计数
标记-压缩算法
- 标记-整理,内存碎片化的整理
- 针对大部分对象是存活对象的情况,符合老年代垃圾回收的特性
- 执行过程
- 与标记-清除算法一致,从根节点开始标记所有被引用对象
- 将所有的存货对象压缩到内存的一段,按顺序排放,清理边界外的所有空间
- 标记的存货对象被整理之后,当需要给新对象分配内存时,JVM只需要
持有一个内存的起始地址(分割已存和未存的空间)
- 与标记-清除算法的差别
- 清除算法是一种非移动的回收算法
- 压缩算法是移动式的,是否移动回收后的存货对象是一项优缺点并存的风险策略
- 指针碰撞,如果内存空间以规整和有序的方式分布,即
已用和未用的内存都各自一边
,彼此之间维系着一个记录下一次分配其实点的标记指针
,当为新对象分配内存时,只需要通过修改指针的偏移量,将新对象分配在第一个空闲内存位置上即可 - 优点
- 消除了标记-清除算法中,内存区域碎片化
- 消除了复制算法中,内存减半的高额代价
- 缺点
- 效率低于前两个
- 移动对象的同时,如果对象被其他对象引用,还需要调整引用地址
- 移动过程中,需要全程暂停用户应用程序
STW
三个算法比较
- 执行效率来说,复制算法>清除>压缩
- 压缩算法,比复制算法多了标记的阶段,比清除算法多了整理内存的阶段
分代收集算法
- 不同的对象的生命周期是不一样的,采取不同的手机方式,以便提高回收效率
- 根据各个年代的特点使用不同的回收算法
- Http请求中的Session对象、线程、Socket连接,生命周期比较长
- String对象,生命周期比较短
- 年轻代,区域相对老年代比较小,生命周期短、存活率低、回收频繁
用复制算法的回收整理,速度最快,复制算法的效率
只和当前存活对象大小有关
- 老年代,区域较大,对象生命周期长、存活率高;一般是由标记-清除或者
标记-清除与标记-整理混合实现
- Mark阶段的开销
与存活对象数量成正比
- Sweep阶段的开销
与所管理区域的大小成正相关
- Compact阶段的开销
与存活对象的数量成正比
- CMS针对老年代的垃圾回收器
- 基于标记清除算法实现的,对对象的回收效率很高
- 对碎片化问题,CMS采用基于压缩整理算法的
Serial Old 串行垃圾回收器
回收器最为补偿- 当回收内存不佳,碎片化导致
Concurrent Mode Failure
(在年老代被用完之前不能完成对无引用对象的回收;当新空间分配请求在年老代的剩余空间中得到满足),将采用Serial Old
执行Full GC
达到对老年代内存的整理
增量收集算法
- 实时垃圾收集算法
- 让垃圾收集线程和应用程序线程交替执行,垃圾收集线程只收集一小片的内存空间,接着切换到应用程序线程,反复切换
- 基础仍是传统的标记清除和复制算法,通过
对线程间冲突的妥善处理
,允许垃圾收集线程以分阶段的方式
完成标记、清理或复制工作 - 由于在垃圾回收过程中,间断性执行了应用程序代码,减少系统的停顿时间
- 由于线程切换和上下文转换的消耗,会导致垃圾回收的总体成本上升,造成系统吞吐量的下降
分区算法
- 针对
G1
垃圾收集器 - 为了控制GC产生的停顿时间,
将一块大的区域分割成多个小块
,根据目标停顿时间,每次合理地回收若干个小区间,减少一次GC所产生的停顿
- 将整个堆空间划分成
连续的不同小区间
,有的放eden
,有的放s0/s1
,有的放old
,有的放humongous 超大对象
- 每一个小区间都独立使用,独立回收,可以控制一次回收多少个小区间