存运行时,程序计数器、本地方法栈和虚拟机栈是随着线程的产生而产生,随着线程的消亡而消亡的,这几部分的内存分配和回收是确定好了的,随方法结束或线程结束时,内存就紧跟着回收了。而Java堆和方法区不一样。一个接口中多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在运行期间才知道会创建哪些对象,故内存回收与分配重点关注的是堆内存和方法区内存。
对于方法区,永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
对于堆,其中存放的是对象实例,对于对象实例的回收,我们首先要判断哪些对象是“存活的”,对于那部分“死亡的”对象,就是我们要回收的。判断对象的存活有两种方法:
引用计数算法
可达性分析算法
引用计数算法:
给对象添加一个引用计数器, 每当有一个地方引用它时, 计数器值+1, 引用失效, -1, 为0的对象不能被使用。
优势:实现简单,效率高。
缺点:无法解决对象相互引用的问题——会导致对象的引用虽然存在,但是已经不可能再被使用,却无法被回收。
可达性分析算法:
通过一系列的称为”GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索走过的路径称为引用链(Reference Chain), 当一个对象到GC Roots不可达(也就是不存在引用链)的时候, 证明对象是不可用的。注意:不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记,第一次要调用finalize()方法,对象自救的关键,但不推荐这样使用
在Java, 可作为GC Roots的对象包括:
方法区: 类静态属性引用的对象;
方法区: 常量引用的对象;
虚拟机栈(本地变量表)中引用的对象.
本地方法栈JNI(Native方法)中引用的对象。
垃圾回收算法
标记-清除算法
从算法的名字就能看出,该算法总共有两步。第一步标记,标记需要进行垃圾回收的对象;第二步清除,将能够回收的对象进行清除。标记-清除算法都是在新生代垃圾收集器上使用,所以标记-清除算法主要是用来清除新生代的对象。
标记-清除算法主要有两方面问题:1)效率不高;2)清除过程产生的空间碎片,导致产生不连续的空间,如果有比较大的象,可能导致垃圾回收的提前执行
复制算法
复制算法是针对标记-清除算法的缺点改造的。原理是将回收的区域分成两块,每次只使用其中的一块,当垃圾回收完之后将还存活的对象复制到另外一块内存中,简单,高效,但是缺点也很明显就是浪费了一半的空间,有点类似用空间换效率的思想。实际的算法实现上,根据IBM的专门研究,新生代中的对象98%都是朝生夕死的,所以也并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,空间比例是8:1:1,这样每次只有10%的空间被浪费。
当然我们并不能保证每次存活下来的对象都不超过内存的10%,那么超出的部分怎么办?
jvm中有一个分配担保机制来保证超出部分的内存可以用其他内存来替代,主要是老年代的内存。另外如果分配的对象占用的内存非常大,也会直接分配到老年代。
为什么是两块survivor而不是一块?
假如eden内存用A表示,两块survivor用B1,B2表示。第一次复制 A+B1->B2,第二次是A+B2->B1,所以必须是两块survivor。
标记-整理算法
标记-整理算法也是针对标记-清除算法的缺点改造的,标记过程和标记-清除算法的过程一样,但是标记完成之后不是立马进行清理释放空间,而是向一方移动,即整理。在全部移动完毕后,进行清理,这样就不会产生空间碎片。另外也不需要复制算法浪费一半空间(而且复制算法会在如果存活对象比较多时,会产生大量的复制操作,降低效率),或者需要担保禁止来应对超出内存10%的情况。
分代收集算法
分代收集算法并不是什么新的思想,我理解就是分治的思想,将jvm根据对象的特点将内存分为几块,一般分为新生代和老年代,新生代朝生夕死,可以用复制算法,只需要付出少量的复制成本即可,老年代因为存活率高,也没有分配担保就使用标记-清除或者标记-整理算法来进行回收。
HotSpot虚拟机中的垃圾收集器
GC调优
1响应时间
2吞吐量
新生代收集器
Serial收集器
ParNew收集器
Parallel Scavenge收集器
老年代收集器
Serial Old收集器
Parallel Old收集器
CMS收集器(Concurrent Mark Sweep)
G1新生代老生代通吃
下面是各个收集器的算法及工作区域
要注意的问题
新生代和老生代的收集器是可用组合使用的,serial与serial old是一对,但是也可以serial与cms搭配。parallel scanvenge与parallel old是一对,这一对主要应用于需要吞吐量大的场景。但是 parallel scanvenge不能与cms搭配使用,上图没有连线的都不能搭配使用。
cms收集器的过程
cms收集器常见问题
g1收集器
目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。
与应用线程同时工作,几乎不需要stop-the-world(与CMS类似); 整理剩余空间,不产生内存碎片;(CMS只能在full-GC时,用stop-the-world整理碎片内存)
GC停顿更加可控;
不牺牲系统的吞吐量;
gc不要求额外的内存空间(CMS需要预留空间存储浮动垃圾);
G1的设计规划,是要替换掉CMS。
eap被划分为一个个相等的不连续的内存区域(regions),每个region都有一个分代的角色:eden、survivor、old(old还有一种细分 humongous,用来存放大小超过 region 50%以上的巨型对象)。
但是对每个角色的数量并没有强制的限定,也就是说对每种分代内存的大小,可以动态变化(默认年轻代占整个heap的5%)。
G1最大的特点就是高效的执行回收,优先去执行那些大量对象可回收的区域(region)。
G1垃圾回收步骤详解
G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的
stw:stop the wrold 缩写
G1 Young GC(STW)
1.当eden数据满了,则触发g1 YGC
2.并行的执行:
YGC 将 eden region 中存活的对象拷贝到survivor,或者直接晋升到Old Region中;将Survivor Regin中存活的对象拷贝到新的Survivor或者晋升old region。
3.计算下一次YGC eden、Survivor的尺寸
G1 Mix GC
在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:
初始标记(initial mark,STW)
对Survivor区域扫描,标记出可能拥有老年代对象引用的根区域(Root Region),该阶段附属于一次Young GC的最后阶段,所以是STW。在GC日志中会被记录为GC paus
根区域扫描(root region scan)
从上一阶段标记的根区域中,标记所有拥有老年代对象引用的存活对象,这是一个并发的过程,而且必须在进行下一次Young GC之前完成
并发标记(Concurrent Marking)
从上一阶段标记的存活对象开始,并发地跟踪所有可达的老年代对象,期间可以被Young GC打断
最终标记(Remark,STW)
G1并行地跟踪在上面阶段经过更新的存活对象,找到未被标记的存活的对象,这是一个STW的阶段,会用到一个初始快照(SATB)算法。在此期间完全空闲的区域会被直接重置回收。
清除垃圾(Cleanup,STW)
G1统计存活对象和完全空闲的区域,完全空闲区域将被重置回收
执行清理RSet的操作
将存活对象复制到新的未被占用的区域。执行这一步时可以只对新生代操作,这时G1将其记录为[GC pause (young)];也可对新生代和一部分老年代都进行处理,这时被记录为[GC Pause (mixed)],这些老年代要根据其存活度去选择。