JVM中的垃圾收集算法
介绍什么是垃圾,怎么回收垃圾。本文的理论依据是《深入理解java虚拟机》第二部分 自动内存管理。如有错误,感谢指正。
垃圾收集
垃圾
什么是垃圾对象呢?那些创建出来用了一下 以后再也不会再用的对象,他还在内存里,但在业务和伦理层面我们认为他已经死了,变成了垃圾。既然死了那就别再占用宝贵的内存空间了,于是有了垃圾收集算法。这一节看一下怎么判断一个对象是垃圾。
引用计数算法
给对象添加一个引用计数器,每次引用就加1,引用失效就减1。
那循环引用呢?这种算法看似简单但实际上需要配合更复杂的逻辑处理特殊情况。
java并不用这个算法来标记垃圾对象。
可达性分析
从一系列GC Roots往下连接这每一个对象,当对象没有连接上根的时候,说明这个对象已经死了。
那么GCRoots都是哪些呢:
- 虚拟机栈引用的对象
- 方法区静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈引用的对象
- 系统类
- 锁持有的对象
- 等等
根节点枚举的过程会
Stop The World
.
引用
- 强引用 不会被回收
- 软引用 内存不足时,回收
- 弱引用 在下一次GC将会被回收
- 虚引用 被虚引用指向的对象在被回收时得到一个通知
回收垃圾
被可达性分析认定为垃圾的对象不会立马被回收,而是经历两轮标记。
- 第一轮标记看对象有没有实现Object中的finalize()方法,如果没有,或者finalize()已经被虚拟机调用过,则标记为不需要调用finalize()。
- 第二轮虚拟机将触发finalize()方法,如果这次方法中没有将对象和GC Roots上的节点关联,则下一次将会被回收。从上面的第一轮可以看出,对象只能在这个finalize()中自救一次,延迟一次GC的死亡时间。
分代收集
根据大多数对象都会频繁创建和消亡的频率进行分代。根据对象的“分代年龄”将对象存储在不同的内存分区。
一般情况下,分为
- 新生代
- 老年代
在新生代中不停有对象死去(判断对象已死也是有算法的,比如可达性分析算法),当分代年龄达到一定数值的对象晋升到老年代。
垃圾收集
根据深入理解java虚拟机给出的定义,垃圾收集的说法如下:
- Partial GC 部分收集
- Minor GC/Young GC 新生代收集
- Major GC/Old GC 老年代收集(只有CMS会单独收集老年代)
- Mixed GC 混合收集
- Full GC 整堆收集
默认情况下 新生代中的Eden和两个Survivor区的占比是8:1:1, 当新生代中无法再给对象分配堆空间将会触发一次Minor GC。
标记-清除算法
顾名思义,首先将已经判定为垃圾的对象标记为可清除,然后一次性去除。这样会在内存中留下许多内存碎片。
标记-复制算法
标记复制算法是为了解决标记清除算法的不足的。例如内存碎片和效率低下。
首先将内存等分为两块,每次GC将垃圾标记为可回收,然后将不可回收的对象复制到另一半内存中,将这边的内存一次性清除。很明显这种算法是拿空间换时间,太过浪费。且在存活率高的时候,复制的次数太多。
标记-整理算法
在老年代中对象的存活率很高,就不能使用标记复制算法,于是出来了个标记整理算法。
先标记,然后对存活的对象进行移动归整内存。
内存分配
一般情况下,对象都会分配在堆空间里,这里讨论的内存分配也指的是堆空间的内存分配。
整体看一下jvm:
堆内存情况
以
HotSpot
虚拟机为例对堆内存站台讨论。
堆内存的细分
在HotSpot
虚拟机中,堆内存被划分为三个部分,新生代、老年代、永久代(次概念jdk1.8被移除)。
-
新生代(
Young Generation
):占用整个堆空间的一部分,用于存放新创建的对象,分为Eden
区和两个Survivor
区。 -
老年代(
Old Generation
):经过多次垃圾回收仍然存活的对象会被移到老年代,如果该对象所在的Eden
区或Survivor
区已满,则直接在老年代上分配空间。 -
永久代(
Permanent Generation
):用于存放JVM
自身的类信息、方法信息、运行时常量池等。
其中新生代的划分在上面提到过,再做描述。
新生代详解
新生代划分成了三个部分,他们分别是Eden
区、From Survivor
区、To Survivor
区。
在java中大部分对象都是短命鬼,来到java世界也许只用了一次就死了。能长生不老的对象并不多。所以需要给堆空间分区,其中在这些短命鬼中也不乏有人能成功一步步晋升长生不老,所以给新生代也划分了不同区域,用来选拔对象。
在HotSpot
中,Eden区和两个Survivor
的占比是8:1:1
.
当对象被首次创建的时候会进入到Eden区。
那么From Survivor
区、To Survivor
区是干嘛的呢?
From Survivor
用来保存在 Minor GC
中存活下来的对象(对象每存活一次分代年龄就会+1 )。
那么为什么要有两个 Survivor
呢?
为了归整,每次新生代GC后,会将From Survivor
中的对象一次放入To Survivor
中,然后互换From Survivor
和To Survivor
.这样不会产生太多的内存碎片。
思考:如果
survivor
区的大小不足了,那么存活下来的对象需要晋升怎么办呢?
默认情况下会开启-XX:HandlePromotionFailure
让老年代通过一定的策略为此次GC做担保,老年代认为可以容纳晋升的对象的时候会根据情况选择是否Full GC
腾出更多空间存放晋升的对象。如果将-XX:HandlePromotionFailure=false
将会不允许担保,那么每次发生这种情况都会Full GC
。