判断对象是否存活
在进行
GC
之前,首先要确定的就是在
java
堆中那些对象已经“死去”那些对象还“活着”
引用记数法
(Referencecounting)
了解即可,虚拟机并不是通过该算法来判断对象是否存活。
引用计数器的实现很简单,对于一个对象
A
,只要有任何一个对象引用了
A
,则
A
的引用计数器就加
1
,当引用失效时,引用计数器就减
1
。只要对象
A
的引用计数器的值为
0
,则对象
A
就不可能再被使用。
存在的问题:很难处理对象的循环引用。根对象已经不可达,垃圾对象的相互引用造成垃圾对象的引用计数都不为
1
,所以就不会被回收。
可达性分析算法
(ReachabilityAnalysis)
主流虚拟机通过该算法来判断对象是否存活。
以
“
GCRoots
”
(
一组必须活跃的引用对象
)
的对象为起点,如果从
GCroots
到这个对象不可达,就说明该对象是不可用的,被判定为可回收的对象
。
可作为
GCRoots
的对象:
虚拟机栈中应用的对象,方法区中类静态属性应用的对象,方法区中常量引用的对象,
Native
方法应用的对象。
对象的自救
在可达性分析算法中
“不可达”的对象,并不是“非死不可的”。暂时处于“缓刑”。
一个对象如果被标记为
“不可达”,接下来将会进行筛选,即判断该对象是否有必要执行
fianlize
方法,如果对象中没有覆盖
finalize
方法,或者
finalize
方法已经被
JVM
调用过了
(
任何一个对象的
finalize
方法只会被系统自动调用一次
)
。则判定没有必要执行
finalize
方法。判定对象死亡。
finalize
方法是对象逃脱死亡的最后一次机会。
如果对象被判定为有必要执行
finalize
方法,只要在
finalize
方法中重新与引用链上任意一个对象建立关联
(
比如把自己(
this
关键字)赋值给一个变量
)
。对象就会自救成功。
垃圾收集算法
标记
—清除算法
(Mark-Sweep)
标记
-
清除算法是现代垃圾回收算法的思想基础。标记
-
清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
该算法有两个不足:
一是效率,标记和清除两个阶段效率都不高。
二是空间问题,标记清除后会产生大量不连续的内存碎片,这样以后在位大对象分配空间时,由于找不到足够的连续内存会提前出发下一次
GC
。
标记
—整理算法
(Mark
—
Compact)
标记
-
整理算法适合用于存活对象较多的场合,如老年代。它在标记
-
清除算法的基础上做了一些优化。
和标记
-
清除算法一样,标记
-
压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象移动到内存的一端。之后,清理边界外所有的空间。
复制算法
(Copying)
与标记
-
清除算法相比,复制算法是一种相对高效的回收方法。
不适用于存活对象较多的场合,如老年代。
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
Java
中的堆是
JVM
所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在
Java
中,堆被划分成两个不同的区域:新生代
(Young)
、老年代
(Old)
。新生代
(Young)
又被划分为
三个区域:
Eden
、
From Survivor
、
To Survivor
。
这样划分的目的是为了使
JVM
能够更好的管理堆内存中的对象,包括内存的分配以及回收。
从图中可以看出:堆大小
=
新生代
+
老年代。其中,堆的大小可以通过参数–
Xms
、
-Xmx
来指定。
默认的,新生代
(Young)
与老年代
(Old)
的比例的值为
1:2(
该值可以通过参数–
XX:NewRatio
来指定,即:新生代
(Young)=1/3
的堆空间大小。老年代
(Old)=2/3
的堆空间大小。其中,新生代
(Young)
被细分为
Eden
和两个
Survivor
区域,这两个
Survivor
区域分别被命名为
from
和
to
,以示区分。
默认的,
Eden:from:to=8:1:1(
可以通过参数–
XX:SurvivorRatio
来设定
)
,即:
Eden=8/10
的新生代空间大小,
from=to=1/10
的新生代空间大小。
JVM
每次只会使用
Eden
和其中的一块
Survivor
区域来为对象服务,所以无论什么时候,总是有一块
Survivor
区域是空闲着的。因此,新生代实际可用的内存空间为
9/10(
即
90%)
的新生代空间。
分代收集算法
(Generational Colletion)
依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。
根据不同代的特点,选取合适的收集算法。
少量对象存活,适合复制算法
大量对象存活,适合标记清理或者标记整理算法。
内存分配及回收策略
对象优先在
Eden(
伊甸园
)
分配:大多数情况下对戏在新生代
Eden
中分配,当
Eden
区中没有足够的空间时,虚拟机将发起一次
Minor GC
。
大对象直接进入老年代:所谓大对象就是指那些需要大量连续内存空间的
Java
对象,最典型的大对象就是那些很长的字符串以及数组。
长期存活的进入老年代:虚拟机给每个对象都定义了一个年龄计数器。对象在
Eden
出生,经过一次
Minor GC
还存活。对象的年龄就加一,当它的年龄增加到一定程度时
(
默认是
15
岁
)
,就会被晋升到老年代。
新生代
GC(Minor GC):
指发生在新生代的
GC
动作,因为大多数
Java
对象都具有朝生夕灭的特点,所以
Minor
GC
发生较为频繁,回收速度页比较快。
老年代
GC(Major GC
/
Full
GC)
:指发生在老年代的
GC
,出现了
Major
GC
,至少会伴随一次
Minor
GC
。
Major
GC
的速度比
Minor
GC
慢10倍以上。
内存溢出和内存泄漏
内存溢出
out of memory(memory overflow)
,是指程序在申请内存时,没有足够的内存空间供其使用,出现
out of memory.
泄漏,什么是泄漏?我举个简单的例子,不知道是不是这个意思,就比如说有人跟你关系不错,找你借了点钱,但是后来他搬家了,新地址你不知道,你想找他要钱回来,但是就是找不到他在什么地方。专业点的话就是说你向系统申请到了你想要的内存空间,但是使用完了之后却不归还,结果你申请到的内存空间你自己也访问不到(也许你把地址搞丢了),系统也无法分配该空间给其他的程序。这就是一次泄漏
.
内存泄露
是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露的堆积会造成内存溢出。
Java
内存泄露根本原因是什么呢
?
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,
尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是
java
中内存泄露的发生场景
造成的原因有:
1
、静态集合类引起内存泄漏:
2
、当集合里面的对象属性被修改后,再调用
remove()
方法时不起作用。
3
、监听器。
4
、各种连接。
5
、内部类和外部模块的引用。
垃圾收集器
CMS
收集器
CMS
收集器是一种以
获取最短回收停顿时间为目标
的收集器。基于
“
标记
-
清除
”算法实现,它的运作过程如下:
1
)初始标记
(CMS initial mark)
2
)并发标记
(CMS concurrent mark)
3
)重新标记
(CMS remark)
4
)并发清除
(CMS concurrent sweep)
初始标记、重新标记这两个步骤仍然需要
“
stop the world
”,初始标记仅仅只是标记一下
GC Roots
能直接关联到的对象,速度很快,
并发标记阶段就是进行
GC Roots Tracing
的过程
而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短。
在整个过程中耗时最长的的并发标记和并发清除过程收集器线程都可以和用户线程一起工作,所以从总体上说,
CMS
收集器的内存回收过程是与用户线程一起并发执行的。
CMS
是一款优秀的收集器,主要
优点
:并发收集、低停顿
缺点:
1
)
CMS
收集器对
CPU
资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
2
)
CMS
收集器无法处理浮动垃圾,可能会出现“
Concurrent Mode Failure
(并发模式故障)”失败而导致另一次
Full GC
产生。
浮动垃圾(
Floating Garbage
)
:由于
CMS
并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,
CMS
无法在当次收集中处理掉它们,只好留待下一次
GC
中再清理。这些垃圾就是“浮动垃圾”。
3
)
CMS
是一款“标记
--
清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次
Full GC
G1
收集器
G1(Garbage-First)
收集器是当今集线器技术发展的最前沿成果之一。
G1
是一款面向服务端应用的垃圾收集器。
G1
具备如下特点:
1
、并行与并发:
G1
能充分利用
CPU
、多核环境下的硬件优势,使用多个
CPU
(
CPU
或者
CPU
核心)来缩短
stop-The-World
停顿时间。部分其他收集器原本需要停顿
Java
线程执行的
GC
动作,
G1
收集器仍然可以通过并发的方式让
java
程序继续执行。
2
、分代收集:虽然
G1
可以不需要其他收集器配合就能独立管理整个
GC
堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次
GC
的旧对象以获取更好的收集效果。
3
、空间整合:与
CMS
的“标记
--
清理”算法不同,
G1
从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4
、可预测的停顿:这是
G1
相对于
CMS
的另一个大优势,降低停顿时间是
G1
和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为
M
毫秒的时间片段内,
G1
收集器的运行步骤
1
)初始标记
(CMS initial mark)
2
)并发标记
(CMS concurrent mark)
3
)最终标记
(Final Marking)
4
)筛选回收
(Live Data Counting and Evacuation)
上面几个步骤的运作过程和
CMS
有很多相似之处。初始标记阶段仅仅只是标记一下
GC Roots
能直接关联到的对象,并且修改
TAMS
的值,让下一个阶段用户程序并发运行时,能在正确可用的
Region
中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从
GC Root
开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程
Remenbered Set Logs
里面,最终标记阶段需要把
Remembered Set Logs
的数据合并到
Remembered Set Logs
里面,最终标记阶段需要把
Remembered Set Logs
的数据合并到
Remembered Set
中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个
Region
的回收价值和成本进行排序,根据用户所期望的
GC
停顿时间来制定回收计划。