引用计数算法简单的说就是在给对象添加一个引用计数器,每次被引用则该计数器加1,当引用失效时,计数器减1,计数器值为0的表示该对象不再被使用。
该算法被Python等许多脚本语言采用来管理内存,但java中并没有使用该算法。因为该算法存在许多弊端,比如很难解决对象之间循环引用的问题。
2. 可达性算法
这个算法的基本思路就是通过一系列的称谓“GC
Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径为引用链,当一个对象
到GC Roots没有任何引用链项链时,则证明此对象时不可用的。
比较难理解的是*GC
Roots和引用链**这两个概念
所谓“GC
roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。例如说,这些引用可能包括:
所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。
VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
JNI handles,包括global handles和local handles
(看情况)所有当前被加载的Java类
(看情况)Java类的引用类型静态变量
(看情况)Java类的运行时常量池里的引用类型常量(String或Class类型)
(看情况)String常量池(StringTable)里的引用注意,是一组必须活跃的引用,不是对象。
Tracing
GC的根本思路就是:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,
其余对象(也就是没有被遍历到的)就自然被判定为死亡。注意再注意:tracing
GC的本质是通过找出所有活对象来把其余空间认定为“无用”
,而不是找出所有死掉的对象并回收它们占用的空间。GC roots这组引用是tracing GC的起点。要实现语义正确的tracing
GC,就必须要能完
整枚举出所有的GC roots,否则就可能会漏扫描应该存活的对象,导致GC错误回收了这些被漏扫的活对象。
可达性算法的弊端在于每次gc的过程可能很长而且很耗费资源。
生存还是死亡–finalize方法解析
在可达性算法中被标记为不可达的对象是可以通过finalize方法进行拯救的。
finalize方法是Object类的方法,在Oject中是一个空实现,子类可以通过重写该方法来进行一些清理工作。javadoc中对其的解释如下:
Called by the garbage collector on an object when garbage
collection determines that there are no more references to the
object.
当gc系统认为某个对象没有被引用到时在gc的时候调用该方法。
The finalize
method is never invoked more than once by a Java virtual machine
for any given object.
一个特定的对象的finalize方法绝对不会被Java虚拟机调用超过一次。
不可靠的finalize
学过c++的同学可能以为这是个与c++类似的对象析构函数,其实有很大区别,java的对象回收是通过gc系统自动的,我们无法知道系统会在何时进行gc
,因为大部分的gc系统都很耗费资源,java虚拟机会尽可能地减少gc的次数,除非当内存消耗殆尽而迫不得已的时候,而finalize方法只会在gc系统进行
对该对象回收时进行运行该对象重写的finalize方法。
采用Stack
Overflow上的一段话:
Note that it’s entirely possible
that an object never gets garbage collected (and thus finalize is
never called). This can happen when the object never becomes
eligible for gc (because it’s reachable through the entire lifetime
of the JVM) or when no garbage collection actually runs between the
time the object become eligible and the time the JVM stops running
(this often occurs with simple test programs).
There are ways to
tell the JVM to run finalize on objects that it wasn’t called on
yet, but using them isn’t a good idea either (the guarantees of
that method aren’t very strong either).
If you rely on
finalize for the correct operation of your application, then you’re
doing something wrong. finalize should only be used for cleanup of
(usually non- resources. And that’s exactly because the JVM
doesn’t guarantee that finalize is ever called on any object.
大致的意思就是说一个对象也许永远可能不会被gc,直到java虚拟机停止运行时。
那么此时finalize方法就永远得不到调用,我们不应该依靠finalize方法去进行一些必须的操作,finalize方法就只应该被当做对象回收后的资源清理。
因为java虚拟机无法保证finalize方法在何时被调用。
同样的,大部分虚拟机中调用System.gc()并不一定会进行gc操作,而只是给JVM一个建议,进不进行垃圾回收还是由JVM自行操作。
垃圾收集
垃圾收集算法
常用的收集算法主要有以下三种:
标记-清除算法
标记阶段:标记出所有可回收的对象。
清楚阶段:回收所有已被标记的对象,释放这部分空间。
复制算法
划分区域:将内存区域划分为1个Eden区作为分配对象的主战场和2个幸存区(即Suvivor空间,划分为2个等比例的from区和to区)
复制:收集时,将Eden区上和其中一块幸存区仍标记存活的对象复制到另一块幸存区。
清除:存活对象已经被安置,则将Eden区和另一块Suvivor区内存释放
晋升:复制阶段,如果一块幸存区容纳不下所有幸存对象,则将直接存放到老年代。
标记-压缩算法
标记阶段:标记出所有可以回收的对象。
压缩阶段:将标记阶段的对象移动到空间的一段,释放剩余的空间。
复制算法实现简单,以空间的代价来换取运行时的高效,一般适合新生代的收集,老年代的对象生命周期比较长,如果使用复制算法,则
每次复制的代价太高,所以老年代的收集算法多为标记算。
分代收集(HotSpot虚拟机)
为什么需要分代?
收集的代价太高,如果频繁地对整个内存区域进行完整回收,可以想象代价是多么的大,并且java中大部分对象都是朝生夕死的,
生命周期非常短暂,极少数对象生命周期很长,因此不如针对这些进行分开收集,分代收集的思路因此而来。
这样一来,我们可以根据不同区域的特点选择适合的收集算法进行收集。
在HotSpot虚拟机中,可将分代分为三种类型:
新生代
老年代
永久代(在
新生代收集:Minor Collection
一般情况下,Eden区是新生代分配空间的主要区域,新对象都分配在这里,当Eden区内存消耗殆尽时,无法满足对象分配请求,则触发
Minor Collection。
第一次Minor
Collection扫描Eden区将存活对象复制到其中一块空闲的Suvivor区,清除Eden区,并且存活对象的年龄加1,第二次Minor
Collection时,
则扫描Eden区和该Suvivor区,将存活对象复制到另一块Suvivor区,并且清除Eden区和Suvivor区。反复如此,当第N次Minor
Collection时,某存活对象的
年龄足够时,则将该对象晋升到老年代。
另一种晋升老年代的可能是,但是Survivor内存区域中没有足够的空间来容纳从Eden升级过来的对象时,也会有部分对象直接升级到Tenured内存区域中。
总结来说,晋升老年代有两种可能:
年龄足够大
Survivor区无法存放下某存活对象时。
我们可以通过VM选项
-XX:InitialTenuringThreshold来设置晋升老年代的对象年龄阈值
老年代收集:Full Collection
当老年代无法容纳新生代晋升过来的对象时,则触发Full Collection,将对整个堆进行回收,包括young gen、old
gen、perm gen(如果存在的话)等所有部分的模式。(CMS收集器除外,CMS老年代收集算法不会对新生代进行收集)。
既然提到了永久代,那就说说永久代和元空间Metaspace。
java8之后永久代被移除了,替换其的是元空间区域。元空间被放在本地内存中,我们可以通过-XX:MaxMetaspaceSize来指定元空间的大小,但是它会自动增长,增长的最大限额由机器的内存大小决定。
而永久代通常有一个固定的最大值,而永久代的收集又并不效率,所以我们经常会见到那令人厌恶的java.lang.OutOfMemoryError:
PermGen space。
那么元空间也需要GC吗?答案是肯定的,当元空间越来越大接近MaxMetaspaceSize时。则会进行垃圾回收。
更多Metaspace的知识请看揭秘Metaspace
各类垃圾收集器的优缺点
CMS收集器
CMS(concurrent Mark
Sweep)收集器,以获取最短回收停顿时间为目标的收集器,适用于对交互响应速度敏感,希望系统停顿时间短的应用程序。
CMS是基于“标记-清楚”的算法实现的,完整的收集过程如下:
初始标记:标记GC Roots能关联到的对象。
并发标记:进行GC Root Tracing,并标记可达对象。
并发预清理:重新扫描,减少下一个阶段的工作。
重新标记:在之前标志的基础上对并发标记阶段遭到破坏的对象引用关系进行修复,保证最终清理前建立的对象引用关系正常。扫描
从根对象向下追溯,并处理对象关联,这个过程也很短暂。
并发清理:清理垃圾对象。
并发重置:重置收集器的数据结构,做好下一次GC任务的准备工作。
只有初始标记和重新标记需要“stop
the world”。
G1收集器
G1收集器是一款面向服务端应用的垃圾收集器,主要特色是其重新定义了堆空间,在原先分代的基础上,将堆划分为一个个区域Region.
并且在解决如何在不进行全堆扫描的基础上对不同Region区域的引用关系的对象进行确定是否存活的问题上采用了Remembered
Set来避免全堆扫描。
在这里引申一下一个问题,分代收集中,新生代收集中如果老年代对象引用了新生代的对象,而Minor GC是比较频繁的,为了避免Minor
GC过慢,如何解决避免扫描老年代?
在HotSpot VM中,采用了Card Marking(卡片标记)的方法,避免了在做Minor
GC时需要对整个老年代扫描。具体的方法如下:
将老年代的内存分片,1个片默认是512byte
如果老年代的对象发生了修改,就把这个老年代对象所在的片标记为脏
dirty。或者老年代对象指向了新生代对象,那么它所在的片也会被标记为dirty
没有标记为脏的老年代片它没有指向新的新生代对象,所以可以不需要去扫描
G1收集器的优势
充分利用并行和并发。
分代收集
空间整合,即标记-整理算法。避免了CMS标记-清理算法所带来的内存空间碎片问题。
可预测的停顿:除了追求低停顿之外,还能建立可预测的停顿时间模型。能让使用者指定在某段时间内,消耗在GC上的时间不超过N。
何时使用G1:
实时数据占用了超过半数的堆空间
对象分配率或晋升的速度变化明显。
期望消除耗时较长的GC或停顿。
G1的工作过程
除了Region的工作,G1的大致收集过程如下:
初始标记
根区域扫描
并发标记
重新标记
清理
复制
建议大家阅读Oracle官网的文章,清晰明了的图解Get
Started With G1
我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。