赞
踩
(1)先了解堆空间的基本结构
于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor(s0)、To Survivor (s1)空间等(新生代s0和s1大小相等,使用复制算法,复制交换)。
(2)分配与回收大体流程
对象首先在eden区分配内存,在一次新生代垃圾回收后如果对象还存活,就会进入To Survivor区,并且对象年龄还会加1(Enen区——>Survivor区后对象的初始年龄变为1),当对象的年龄增加到一定程度(默认15岁),就会被晋升到老年代中。Eden区和 From Survivor区被清空时,From Suvivor和To Suvivor角色交换,不管怎么样保证名为To 的Suvivor区域是空的,GC会重复这过程直到To 区被填满,会将所有对象移动到老年代。
(1)对象优先在eden区分配
大多数情况下对象在eden区分配,eden区空间不够时虚拟机将发起一次GC。
先了解Minor GC(新生代GC)和Full GC/Major GC(老年代GC)有什么不同?
测试:
- public class GCTest {
- public static void main(String[] args) {
- byte[] allocation1, allocation2;
- allocation1 = new byte[30900*1024];
- //allocation2 = new byte[900*1024];
- }
- }
通过以下方式运行:
添加的参数:-XX:+PrintGCDetails
运行结果:
从下图我们可以看出 eden 区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用 2000 多 k 内存),from space和to space都是使用率为0:
假如我们再为 allocation2 分配内存会出现什么情况呢?
allocation2 = new byte[900*1024];
from space和to space开始使用,并且老年代也开始使用:
为什么会出现这种情况?
因为给allocation2 分配内存时eden区内存几乎已经被分配完。Eden区空间不够时虚拟机发起一次Minor GC期间虚拟机又发现allocation1 无法存入Survivor (from space和to space)空间,所以只好通过 分配担保机制 把新生代的对象提前转移到老年代,老年代空间足够存放allocation1,所以不会出现Full GC/Major GC。
执行完 Minor GC 后,后面的分配对象如果还能够存放在eden区还是会在eden分配内存
大对象就是需要大量连续内存空间的对象(比如字符串、数组,都在老年代分配内存)。为了避免大对象分配内存时由于分配担保机制带来的复制而降低效率。
虚拟机给每个对象一个年龄计数器,如果对象在Eden出生并经过一次Minor GC后仍然能存活,而且能被Survivor容纳的话,将被移动到Survivor空间,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。所以长期存活对象会进入老年代。
动态对象年龄判定:
如果Survivor空间中相同年龄所有对象大小总和大于Survivor空间一半,年龄大于或者等于该年龄的对象可直接进入老年代
垃圾回收第一步就是要判断哪些对象已死亡
(1)引用计数器法(不常用,已淘汰)
(因循环依赖(引用)问题,该方法已经被淘汰)
就是给对象添加一个引用计数器,当有一个地方引用它计数器就加1;当引用无效计数器就减1;任何时候计数器为0的对象就是不可再用的对象。原理就是:每个对象有一个年龄,如果小于或者等于15岁,存放在新生代,大于15岁存放在老年代;GC线程不定时回收时,如果对象被引用,年龄会加1,如果没有被引用继续回收年龄会减1;如果年龄为0,垃圾回收器会认为该对象是不可达对象,会被清理掉。
这个方法简单效率也高,但主流虚拟机中并没有选择这个算法来管理内存,因为其很难解决对象之间引用问题,如下代码演示:
- /**除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是 GC 无法回收
- */
- public class ReferenceCountingGc {
- Object instance = null;
- public static void main(String[] args) {
- ReferenceCountingGc objA = new ReferenceCountingGc();
- ReferenceCountingGc objB = new ReferenceCountingGc();
- objA.instance = objB;
- objB.instance = objA;
- objA = null;
- objB = null;
- }
- }
(2) 可达性分析算法(根搜索法)
基本思想就是一系列称为“GCRoots的对象”作为起点,从这些节点开始写向下搜索,节点所走过的路称为引用链,当一个对象到GC Roots没有任何引用链相连接的话,就证明该对象是不可用的+
(注:什么是不可达对象? 答:相当于对象没被引用 / 对象没有继续使用 / 没有存活
)
可以作为GCRoots的对象包括下面几种:
(1)虚拟机栈(栈中的局部变量区)中引用的对象
(2)方法区中 静态属性 引用的对象
(3)方法区中 常量 引用的对象
(4)本地方法栈中JNI(Native方法)引用的对象
无论通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象引用链是否可达,判断对象的存活都与引用有关。
(1)强引用
如果一个对象具有强引用,类似生活必不可少的用品,垃圾回收器不会回收它。当内存不足虚拟机宁愿抛出OutOfMemoryError (内存溢出)错误,使程序异常终止也不会回收它。
(2)软引用(用的最多)
如果一个对象具有软引用,类似可有可无生活用品。如果内存空间足够垃圾回收期不会回收它,如果内存空间不足就会回收。软引用可以用来实现内存敏感的高速缓存。
(3)弱引用
也是类似于可有可无生活用品。和软引用区别在于:只有弱引用的对象具有更短暂的生命周期,垃圾回收器一发现就回收。不过垃圾回收器优先级低不一定很快发现弱引用对象。
(4)虚引用
与其他三种引用都不同,它不会决定对象的生命周期,形同虚设。随时可能被回收。主要用来跟踪对象被垃圾回收的活动。
虚引用和弱引用的区别:
虚引用必须和引用队列(ReferenceQueue)一起使用。程序可以通过判断引用队列中是否有虚引用来决定对象是否要被垃圾回收。
运行时常量池( JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆中开辟了一块区域存放运行时常量池)主要回收的是废弃常量。
以字符串常量为例:假如常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,说明该常量是废弃常量,如果发生内存回收并且有必要“abc”就会被回收。总的来说,就是常量池中没被引用的就是废弃常量就会死亡。
方法区主要回收的就是无用的类。
需要同时满足如下三个条件:
(1)该类的所有实例都已经被回收,也就是说Java堆中不存在该类的实例。
(2)加载该类的ClassLoader已经被回收。
(3)该类的对象没有被任何地方引用
收集算法是内存回收方法论,垃圾收集器是内存回收具体实现
一般用于老年代。
该算法分为“标记”和“清除”阶段:首先标记所需要回收的对象,在标记完成后统一回收所有被标记的对象。
它使最基础算法后边都是它的改进,它有俩问题:
优点:可解决循环引用问题、必要时才回收(内存不足时) ;
缺点:回收时其他线程要挂起、对象多时效率不高、会造成内存碎片化(内存明明有空间,但是不连续导致稍微申请大对象无法做到)
一般使用在新生代。
解决了标记清除算法的效率问题。它可以将内存分为大小相同两块from(s0)域和to(s1)域,每次只使用from(s0)域,to(s1)域空闲。当from域内存不够了,开始执行GC操作,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理。每次的内存回收都是对内存区间的一半进行回收,效率高。
注意: 万一存活对象数量比较多,那么to域的内存可能不够存放,这个时候会存放在老年代的空间。
优点:性能高、能解决碎片化问题、能解决引用更新问题;
缺点:会造成一部分内存浪费(不过可以调整内存块比例大小)
案例:
注意:s0区和s1区一定有一个空的,目的是为了存放下一次复制
一般使用在老年代。
和标记-清除类似,但是它解决了碎片化问题。后续步骤不是直接回收对象,而是让所有存活对象向一端移动,然后直接清除掉端边界以外的内存。
优点:解决了碎片化问题;
缺点:由于移动了可用对象,需要去更新引用。
根据对象存活周期不同将内存分为几块,一般将Java堆分为 新生代和老年代,可以根据各年代特点再选择合适的垃圾收集算法(整合其他几种算法)。
在新生代中:对象死亡率高,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
在老年代中:对象存活率高 ,而且没有额外的空间对它进行分配担保,所以必须选择“标记-清除”或者“标记-整理”算法。
注意:垃圾回收不会发生在永久代,但是当永久代满的时候会发生FULL GC完全垃圾回收(JDK1.8已经移除永久代,增加了元空间).
没有最好的收集器,应在不同场景选择不同收集器
新生代采用复制算法,老年代采用标记-整理算法。
单线程收集器,也称串行收集器。“单线程”的意义不仅是它只会使用一条垃圾收集线程去完成垃圾收集工作,而且收集时会暂停所有其他工作线程,直到它收集结束。但是其单线程效率高适合在Client模式下的虚拟机。
Serial收集器的老年代版本,同样是单线程收集器。主要俩用途:在JDK1.5及一般版本与Parallel Svavenge收集器搭配使用;另一种是作为CMS收集器的后备方案。
新生代采用复制算法,老年代采用标记-整理算法。
其实就是Serial收集器的多线程版本。适合运行在Server模式下的虚拟机,除了Serial收集器外,只有它能与CMS收集器(真正的 并发收集器)配合工作。
新生代采用复制算法,老年代采用标记-整理算法。
几乎和ParNew一样。它的关注点是吞吐量,CMS等垃圾收集器关注点更多是用户线程的停顿时间(提高用户体验)。提供了很多参数供用户找到合适的停顿时间与最大吞吐量。
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以考虑Parallel Scavenge收集器和Parallel Old收集器。
采用标记-清除算法。
HotSpot虚拟机真正意义第一款并发收集器,它第一次实现了让垃圾收集器与用户线程同时工作,是一种 以获取最短回收停顿时间为目标 的收集器,注重用户体验。
CMS运作过程有如下四步:
(1)初始标记:暂停所有其他线程,并记录下直接与root相连的对象,速度很快。
(2)并发标记:同时开启GC与用户线程(可以一起工作),用一个闭包结构去记录可达对象。但在这个阶段结束这个结构并不能保证包含当前所有的可达对象(因为用户线程可能会不断更新引用域,所以GC线程无法保证可达性分析的实时性)。
(3)重新标记:为了修复并发标记期间因为用户程序继续运行而 导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段时间长,但远远比并发标记阶段时间短。
(4)并发清除:开启用户线程,同时GC线程开始对为标记的区域做清扫。
CMS优点:并发收集、低停顿;
CMS缺点:对CPU资源敏感、无法处理浮动垃圾、使用“标记-清除”算法会导致收集结束会有大量空间碎片产生。
整体采用标记-整理算法,局部采用复制算法。
是一款面向服务器的收集器。主要针对配备多个处理器以及大容量内存机器,即满足GC停顿时间要求 又满足高吞吐量特性。
G1被视为JDK1.7中HotSpot虚拟机进化重要特征,具备以下特点:
并行与并发:能充分利用CPU多核环境下硬件优势,使用多个CPU来缩短停顿时间。部分其他的收集器(除了CMS)原本需要停顿Java线程来执行GC动作,G1仍可以通过并发方式让Java程序继续运行。
分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个堆,但还是保留了分代收集的概念。
空间整合:与CMS的“标记-整理”算法不同,G1从整体采用“标记-整理”算法,局部上来看是基于“复制算法”实现。
可预测的停顿:这是G1相对于CMS另一个大优势。降低停顿时间是G1和CMS共同关注点,但G1除了追求低停顿外还能建立可预测停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片内。
G1运作有如下四步:
G1优点:吸收了CMS优点、支持很大的堆,高吞吐量、堆被划分成 许多个连续的区域(region)
并行与并发区别:
并行:多个处理器同时处理多个不同任务(三个人同时吃三个馒头)
并发:一个处理器同时处理多个任务(一个人同时吃三个馒头)
上一篇:Java内存区域与内存溢出异常
下一篇:虚拟机性能监控与故常处理
参考资料:深入理解Java虚拟机(第2版) : JVM高级特性与最佳实
### 若对你有帮助的话,欢迎点赞!评论!转发!谢谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。