为每个对象添加一个引用计数器,来统计指向该对象的引用个数,一旦某个对象引用器为0时,则说明这个对象已经死了,需要回收
问题:无法解决 循环引用 问题
如果a和b互相引用,当a和b实际已经死了的情况下,他们的引用计数都不为0,会导致无法回收,最终导致内存泄露
GC Roots(起始存活对象集合 live set)-> 从这个集合开始探索所有能够被该集合引用的对象,并将其加入该集合中(标记Mark);最终未被探索到的对象便是死亡的,可回收
多线程环境下存在误报(将引用设置为null)和漏报(将引用设置为未被访问过的对象-新对象)
解决多线程环境下漏报问题
传统的解决方法:Stop-the-world 即停止其他非垃圾回收线程的工作,直到完成垃圾回收,造成了垃圾回收的暂停时间(GC pause)
安全点(safepoint)机制,Java虚拟机收到Stop-the-world请求,便会等待所有的线程到达安全点,才允许请求Stop-the-world的线程进行独占的工作;
安全点的定义:虚拟机定义了一下情况为安全点,并对应一写安全点的检测机制(当有安全点请求时,执行一条字节码便进行一次安全点的检测)
- 清除(sweep):将死亡对象的内存标记为空闲内存,并记录到空闲列表(free list)中
- 缺点:
- 造成内存碎片:JVM的堆中对象必须是连续分布的,所有可能出现总空闲内存很多,但是无法分配的极端情况
- 分配效率低:需要逐个访问空闲列表中的项,来查找能够分配新建对象的空闲内存(如果是一块连续的内存,直接进行指针加法(pointer bumping)来做分配)
- 缺点:
- 压缩(compact):将存活的对象聚集到内存区域的起始位置
- 优点:减少内存碎片化问题
- 缺点:压缩算法的性能开销
- 复制(copy):将内存分成两等分,分别用from和to两个指针来维护两块内存,只有from区域的内存可以用来分配,当发生垃圾回收时,便将存活的对象复制到to指针指向的内存中,并交换from和to的指针内容
- 缺点:内存使用率低
现代垃圾回收器会综合上述几种回收方式,取优点规避缺点。
大部分Java对象只存活一小段时间,而存活下来的小部分对象则会存活很长一段时间。
- 新生代
- 猜测大部分对象只存在一小段时间,便可以频繁的采用耗时短的垃圾回收算法,让大部分垃圾能够在新生代被回收掉
- 老年代
- 猜测大部分垃圾已经在新生代被回收了;当触发老年代回收时,表明签名的猜测错误或者堆内存已经耗尽,这时JVM会进行一次全堆扫描,耗时将不计成本(现代垃圾回收器都是并发收集,来避免全堆扫描)
-
堆划分
- Java虚拟机堆划分:新生代 + 老年代
- 新生代划分:Eden区 + 两个大小相同的Survivor区
- 默认情况下,JVM采用动态分配策略(-XX:+UsePSAdaptiveSurvivorSizePolicy),根据对象的生成速率,以及Survivor区的使用情况动态调整Eden区和Survivor区的比例
-
内存分配:
- 当我们new一个对象时,会在Eden区划出一块作为存储对象的内存,由于堆空间是内存共享的,所有需要进行同步,否则就会出现两个对象公用一块内存的事故;
- 解决:每个线程预先申请内存区域(连续的,加锁申请),当用完时,继续预申请内存(这项技术:TLAB,Thread Local Allcoation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)
- 当Eden空间耗尽,这时,JVM会触发一次 Minor GC,回收垃圾,存活下来的对象会被送到Survivor区
- 两块Survivor区
- 分别标识为 from 和 to
- 当发生Minor GC时,Eden区和from指向的Survivor区中的存活对象会被复制到to指向的Survivor区中,然后交换from和to的指针,以保证下次Minor GC时,to指向的Survivor区还是空的
- JVM会记录Survivor区中的对象一共被来回复制了几次,如果一个对象被复制的次数为15(-XX:+MaxTenuringThreshold),则该对象将被晋升到老年代,其次如果单Survivor区被占用50%(-XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升到老年代
- 当我们new一个对象时,会在Eden区划出一块作为存储对象的内存,由于堆空间是内存共享的,所有需要进行同步,否则就会出现两个对象公用一块内存的事故;
-
卡表(新生代垃圾回收时,老年代有可能引用了新生代,也就标记存活对象时,我们需要扫描老年代的对象,这样会导致扫描整个老年代)
- 大致(推测)标志出可能存在的老年代新生代引用的内存区域
- 新生代垃圾回收器:(标记-复制算法)
- Serial:单线程 -XX:UseSerialGC
- Parallel Scavenge:多线程,吞吐量大于Parallel New,但不能与CMS一起使用
- Parallel New:多线程 -XX:UseParNewGC
- 老年代垃圾回收器:
- Serial Old:标记-压缩算法,单线程
- Paralle Old:标记-压缩算法,多线程 UseParallelGC
- CMS:标记-清除算法,并发的,可在应用程序运行过程中进行垃圾回收;并发收集失败时,虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收
- G1(Garbage First):
- 标记-压缩算法
- 横跨新生代和老年代的垃圾回收器
- 打乱前面的堆结构,直接将堆分成极其多个区域(通常是几百个),这些区域是固定大小的(默认情况下大约为2MB),每个区域都可以充当Eden区、Survivor区或者老年代中的一个;G1能够对每个细分的区域进行垃圾回收,且优先回收死亡对象较多的区域
- 在程序运行中并行进行垃圾回收
- G1还有一个较关键的参数是
-XX:MaxGCPauseMillis = n
,这个参数是用来限制最大的GC暂停时间,目的是尽量不影响请求处理的响应时间。G1将根据先前收集的信息以及检测到的垃圾量,估计它可以立即收集的最大区域数量,从而尽量保证GC时间不会超出这个限制。因此G1相对来说更加“智能”,使用起来更加简单。 - 由于G1的出现,CMS在Java9被废弃了
- ZGC:
- Java11采用,暂停时间不超过10ms
JNI(Java Native Interface)中传入的引用类型参数,或者JNI函数创建的Java对象,JNI采用局部引用与全局引用来告知垃圾回收器,不要回收这些C代码中可能引用到的Java对象,垃圾回收算法将标记这两种引用指向的对象为不可回收。