一、概述
在《JVM — 内存模型》中,我们了解了JVM的内存模型,得到以下结论:
程序计数器、虚拟机栈、本地方法栈
都是线程私有的;程序计数器、虚拟机栈、本地方法栈
随着线程的创建而创建,随着线程的销毁而回收;程序计数器、虚拟机栈、本地方法栈
这几个区域的内存分配和回收都具备确定性,因此无需过多考虑回收的问题;Java堆、方法区
这两块内存区域在程序运行期会动态分配内存,具有不确定性;
小结: 因此,垃圾回收主要针对
Java堆、方法区
这两块内存区域;
二、如何判断对象是否被引用
判断一个对象是否被引用有两种方法:1.引用计数算法; 2.可达性分析算法;
2.1 引用计数算法
原理:
给对象中添加一个引用计数器,分如下三种情况:
- 当对象在一个地方被引用时,计数器值加1;
- 当引用时效时,计数器减1;
- 在任何时刻,计数器为0的对象就是没有被使用的对象,可以被回收;
缺点:
主流的JVM没有使用引用计数算法来管理内存,因为很难解决
对象之间相互循环引用
的问题;
2.2 可达性分析算法
原理:
- 从一系列称为
GC Roots
的对象作为起点,开始向下搜索,搜索所走过的路径称为引用链
;- 当一个对象到
GC Roots
没有任何引用链相连时,就说明该对象是不可用的(可被回收的);- 上图中的
object5、object6、object7
虽然有被引用,但没有一条到GC Roots
的引用链,所以是可回收的对象;
Java中可以作为 GC Roots
对象的有如下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中静态属性引用的对象;
- 方法区中静态常量的对象;
- 本地方法栈中Native方法引用的对象;
2.3 方法区的回收
方法区主要回收两部分内容:废弃常量、无用的类
;
废弃常量:
类似回收Java堆中的对象;
回收 无用的类
需要 同时满足
3个条件:
- 该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
三、垃圾收集算法
垃圾收集算法有4种:1.标记-清除算法;2.复制算法;3.标记-整理算法;4.分代收集算法
3.1 标记 - 清除算法
原理:
该算法分为
标记
和清除
两个阶段:首先标记处所有需要回收的对象,在标记之后统一回收所有被标记的对象;
缺点:
- 效率问题: 标记和清除两个过程效率都不高;
- 空间问题: 清除之后会产生大量不连续的内存碎片,当之后需要创建一个较大内存的对象时,会因为无法找到找到这么一块满足需求的内存区域而提前出发
GC
操作;
3.2 复制算法 (解决“标记 - 清除算法”的效率问题)
原理:
将内存分为大小相等的两块(A、B),每次只使用一块(A),当一块(A)内存用完时,将(A)中还存活的对象复制到另一块内存(B)中,然后清除内存(A),避免了内存回收之后的碎片化,
简单、高效
;
缺点:
- 内存会缩小到原先的一半;
- 当内存中对象存活率较高时,要进行多次的复制操作,效率会降低;
3.3 标记 - 整理算法
原理:
和 “标记 - 清除算法” 类似,先标记需要清理的对象,然后将所有存活的对象都向一端移动,最后直接清理掉端边界以外的内存;
3.4 分代收集算法
目前JVM垃圾收集都采用“分代收集”;
原理:
将Java堆分为新生代和老年代,根据不同代的特点采用不同的垃圾收集算法;
- 新生代:大批量对象死亡,少量存活,使用
复制算法
;- 老年代:对象存活率高,使用
标记 - 清除
或标记 - 整理
算法;
四、内存分配
说明:
上图是Java堆中的分代图:分为新生代、老年代、永久代
;
新生代又分为:Eden区、Survivor from区、Survivor to区
三部分;
问1: 新创建的对象,在哪里分配内存空间 ?
答: 多数情况下,对象的创建是在新生代Eden区中分配内存的;
问2: 为什么新生代需要两个Survivor区
?
答: 避免存活对象复制时,碎片化的发生;
参考:《为什么新生代内存需要有两个Survivor区》
问3: 为什么新生代内存需要Eden区
和Survivor区
?
答: 延缓新生代中的对象进入年老代的速度(主要还是新生代内存和年老代内存GC策略不同);从GC角度看,年老代GC对系统影响比新生代的大;
五、对象如何进入老年代
5.1 大对象直接进入老年代
由于新生代使用复制算法,为避免在Eden区和两个Survivor区之间发生大量的内存复制,因此当对象内存达到阀值后会直接进入老年代。
5.2 新生代对象年龄到一定程度后进入老年代
虚拟机给每个对象定义了一个Age的计数器,初始值为0,每经过一次GC并且存活,这个对象的Age就会加1,当增加到一定程度(默认为15),那么就会进入老年代中。
5.3 动态对象年龄判定
如果新生代Survivor区空间中相同年龄所有对象大小的总和大于Survivor区的一半,年龄大于或等于该年龄的对象就会直接进入老年代。
六、参考
- 《深入理解Java虚拟机》
- 《为什么新生代内存需要有两个Survivor区》