垃圾
从垃圾讲起
什么是垃圾?
定义垃圾的方法
1.引用计数法(Reachability Counting)
在对象头中分配一个空间,用来存储自己被引用的次数。当该对象被引用,进行increase +1 ,当引用删除的时候进行decrease -1 ,当这个计数等于0的时候,它就是垃圾。
问题:相互引用
2.可达性分析(Reachability Analysis)
通过一些根对象(GC Roots)作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
跟对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI 引用的对象
PS:方法区是规范 永久带和元空间都是实现
java运行时内存划分
垃圾产生的位置
1.程序计数器(Program Counter Register)
记录当前线程正在执行(下一个需要执行)的字节码的行号。
程序计数器占用的内存空间对比较小,和线程一一对应,所以称为线程私有的。
ps:如果是调用JNI执行方法(本地方法),技术器的值为undefined。
此处不会发生OutOfMemoryError,因为它维护的只是下一个需要执行的地址。
2.虚拟机栈(JVM Stack)
虚拟机栈和线程也是一一对应,同生共死的,也是线程私有的。
虚拟机栈存储的内容:
局部变量表、操作栈、动态链接、返回地址(方法出口信息)……
局部变量表:8种基本类型(byte、short、int、long 、float、double、char、boolean)、对象引用(指向对象的指针)、returnAddress(指向一条字节码指令的地址)
在方法运行期间局部变量表的大不会被改变。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。
2种异常:A: StatckOverFlowError 线程调用的栈深度大于虚拟机允许的最大深度(一般虚拟机如HotSpot允许动态扩展虚拟机栈的大小,所以一般不会发生)
B:OutOfMemoryError 在动态扩展的时候无法申请到足够的内存。
3.本地方法栈(Native Method Statck)
功能和虚拟机栈累死,为native方法提供服务,虚拟机栈为java方法提供服务。
4.堆(Heap)
堆是jvm内存占用最大,最容易发生OutOfMemoryError的区域。
垃圾回收(GC)最频繁的区域。
堆空间默认分配比例
5.元空间(Heap)
在 JDK8 之前, Hotspot 虚拟机中的实现方式为永久代(Permanent Generation),永久代位置是在堆内存里,和老年代的垃圾回收基本同步。元数据和永久代类似,但是元数据不在虚拟机中,直接使用本地内存,仅受本地内存大小的限制。
问题:压缩支持的不好,导致碎片化严重。
6.直接内存(Direct Memory)
native函数库占用。
垃圾回收算法
1.标记-清除(Mark-Sweep)
将内存区域对象中可回收的垃圾进行标记,标记完成之后统一进行清理。清理掉的部分可以再次使用。
问题:碎片化;效率与对象成反比,执行效率不稳定。
2.标记-整理(Mark-Compact)
先将内存区域对象中可回收的垃圾进行标记,然后将存活的对象向某个指定区域一侧移动,然后将存活之外的区域清理。
相对与Mark-Sweep可以降低碎片化
3.复制(Copying)
将内存按照容量划分为大小相等的A、B两部分,每次只使用其中的一块假设为A,A用完之后将存活的对象复制到B,然后将A区块直接全部擦除。
避免了碎片化,但是浪费了空间,直接将内存空间缩小了一半。
垃圾回收器
1.Serial(Serial Garbage Collector)
串行单线程
只会用一条垃圾收集线程去完成垃圾回收工作,而且会造成STW(STOP THE WOED:停掉用户所有正常工作的线程),直到垃圾回收线程执行完毕。新生代采取
缺点:STW
优点:相比其他回收器简单高效。在单个CPU的限制下,没有线程之间的相互切换,所以效率较高,所有使用于client模式。
服务器模式请远离。
2.ParNew
ParNew收集器其实就是Serial收集器的多线程版本。
3.Parallel Scavenge
作用于新生代,采用复制算法,多线程收集。
4.Serial Old
作用于老年代,采用标记-整理-压缩算法(Mark-Sweep-Compact),单线程收集
5.Parallel Old
作用于老年代,采用标记-整理算法,多线程收集。
6.CMS(Concurrent Mark Sweep)
官方介绍:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
四个步骤
初始标记(initial marking)
采用可达性分析方法只标记GC Roots 能直接关联到的对象,但是会STW,不过速度很快。
并发标记(concurrent marking)
找出GC Roots 能直接关联到的对象的引用对象(GC Roots Tracing),此时用户线程和GC线程同时进行。因为用户线程运行中可能会一直更新作用域导致GC线程无法保证可达性分析的实时性,但是会记录这些发生变化的地方,下一步处理
重新标记(remarking 有的也叫 final marking)
上一步标记发生变化的地方,这一步进行STW标记,时间一般大于初始标记
并发清理(cleanup)
对标记为垃圾的对象进行清理回收
问题:
因为采用了标记-清除-压缩算法,所以会造成一定的碎片。
关于浮动垃圾:
漏标:某个对象原来有引用,并发进行时,引用解除,此时该对象变为垃圾;
多标:某个对象原来没有引用变为垃圾,并发进行时,添加引用,此对象不可被回收。
这些都属于浮动垃圾问题。
三色标记法
黑色:根对象或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有field也被标记完了)。
灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)。
白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,既垃圾对象(对象没有被标记到)。
A 初始时,所有对象都在 【白色集合】中;
B 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
C 从灰色集合中获取对象: 将本对象 引用到的 其他对象 全部挪到 【灰色集合】中; 将本对象 挪到 【黑色集合】里面。
D 重复步骤C,直至【灰色集合】为空时结束。
E 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。
解决方案:重新标记,三色标记法。
1.如果A已经被标记了(已经是黑色的了),那么用户线程改动 A->C的时候,会把 C 变成灰色,这样,以后就可以搜索 C了。
2.重新标记 STW时,对象间的引用 是不会发生变化的,可以轻松完成标记。
7.G1(Garbadge First Collector)
官方介绍
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html
https://docs.oracle.com/en/java/javase/13/gctuning/garbage-first-garbage-collector.html#GUID-0394E76A-1A8F-425E-A0D0-B48A3DC82B42
G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。(jdk1.7u4可用)
G1将堆分成(虚拟的)年轻代和老年代。逻辑上分区,物理上不分区。
Region:
G1将内存划分成了多个(默认1024)大小相等的区块称为Region。每次分配对象空间将逐段地使用内存,这里就提现了分布式的思想,因为每个Region对可以认为是独立的,所以G1对内存的要求逻辑上是分区的,物理上是不分区的。每个Region大小范围限制在1-32MB(2的0到5次方)之间。分区以Region为单位,对象分配以Card为单位。
TLAB(Thread Local Allocation Buffer)
JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB, 其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁(堆内存因为其共享性需要加锁),每个TLAB都只属于一个线程,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配。TLAB属于Eden区。
Humongous: 大对象 (巨型对象)。
大对象的定义:对象大于Region的二分之一
当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象会占用一个或者多个Region且成本很高。所以巨型对象属于老年代。所占用的连续空间称为巨型分区(Humongous Region)。G1内部做了一个优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。
巨型对象会独占一个或多个连续Region,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。由于无法享受Lab带来的优化,并且确定一片连续的内存空间需要扫描整堆,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。
SATB(snapshot-at-the-beginning)
SATB是增量式标记清除垃圾收集器设计的一个标记算法。CMS的i设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root。G1的SATB设计在remark阶段则只需要扫描剩下的satb_mark_queue。
三种垃圾回收模式
Young gc
发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中。
Mixed gc
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region(垃圾最多的region)。
Full gc
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,尽可能的避免full gc.
组合
1.8 默认PS+PO