一、简介
在C/C++中,都可以直接操作指针,分配内存都需要显式释放。
C++有构造函数和析构函数,创建对象用new,销毁对象用delete。C分配内存用malloc和free。
而java中,只需要显式的new出对象,具体什么时候销毁,就由JVM决定了,不需要人工干预。但是我么还是最好明白JVM垃圾回收的原理,出现问题也容易排查。
二、虚拟机基本结构
以Sun Hotspot Jvm 1.7 为例,虚拟机结构如下图:
hotspot JVM使用了分代的结构。对象分配时,会首先进入新生代,之后一部分留在新生代,一部分晋级老年代。
Jdk1.8,取消了永久代。
三、垃圾回收算法
垃圾回收,都是从根对象开始,遍历所有可以到达的对象,未到达的就是垃圾对象。回收算法共有种。如下:
1、引用计数法
引用计数法的原理,是只要有一个对象引用了A对象,则A的计数器加1。引用失效,计数器减1.
HotSpot没有采用该种办法,有两个原因:
①无法处理相互引用的情况。当两个对象都无用,但是相互引用,该算法认为对象还有用
②随着引用的添加与失效,伴随着加减的操作,浪费性能
2、标记清除法
首先通过根节点,遍历出所有可到达的对象并标记。之后清除所有未标记的对象。
该算法最大问题,是造成内存碎片
3、复制算法
将内存分成两块,每次只使用一块。垃圾回收时,将有效的对象,复制到另一块区域,然后直接清空之前那一块区域。
缺点:内存折半,真正使用的内存区域,只有一半。
单纯的复制算法无法让人接受,但是可以部分的采用。比如上图的HotSpot,新生代中的from和to区域,就是采用的复制算法,后文会说明
4、标记压缩法
在标记完对象后,将有效对象都压缩到内存的一侧。相当于标记清楚 + 内存整理
其他综合算法
分代算法
将内存分成多个代,比如上图的新生代,老年代,每次根据内存的特点,使用不同的回收算法。
新生代: 复制算法
老年代:标记清理,或标记压缩法
分区算法
将内存分成多个区域,每次回收一部分区域,能有效的控制GC停顿的时间
三、垃圾回收器
1、新生代串行回收器
缺点:
① 仅使用单线程回收
② 独占式回收。也就是说,回收时所有线程都需要暂停,也就是 stop the world.
优点:使用的是复制算法,对单CPU处理器等,性能表现超过并行和并发回收器。Client模式,是默认的回收器
2、老年代串行回收器
使用标记压缩算法
缺点:
①串行
②老年代比新生代一般需要更久,停顿时间也会更久,一般是和多种新生代回收器配合使用,也可以作为CMS的备用回收器
3、新生代ParNew回收器
简单的将串行回收器多线程化,其他一模一样
缺点:
并发能力强的CPU上,停顿时间短于串行回收器,在单CPU或并发能力弱的系统中,因为多线程的压力,效果要差于串行回收器
4、新生代ParellelGC回收器
使用复制算法,也是一个多线程,独占式的回收器,关注系统吞吐量
5、老年代ParellelOldGC回收器
需要和ParellelGC配合才能使用,使用标记压缩法
6、并发标记清除CMS回收器
全称为Concurrent Mark Sweep。关注系统停顿时间。
所谓并发,是回收器和应用程序交替执行,也就是多线程编程中常说的并发。而并行,多个线程一起GC,但应用程序是停止的
CMS比较复杂点,分为初始标记,并发标记,预清理,重新标记,并发清除和并发重
缺点:内存碎片
-XX:+UseCMSCpmpactAtFullCollect,可以使CMS回收完成后,进行一次内存整理,但内存整理不是并发的
-XX:CMSFullGCsBeforeCompaction ,设定多少次CMS回收后,进行一次内存压缩
7、G1回收器
Jdk1.7中应用的最新的垃圾回收,期望解决CMS中内存碎片的问题,使用分区算法
特点:
① 并行
② 并发,不会 在整个回收期间完全阻塞应用程序
③ 分代GC:同时兼顾新生代和老年代
④ 空间整理:回收中会进行适当的对象移动,减少空间碎片
⑤ 每次只选择部分区域进行回收
四、hotspot的回收策略
新生代的from,to都叫survivor区,默认Eden,和from,to的比例是8:1:1
1、new的对象,基本都是分配在Eden区的,少数情况例外,比如分配在TLAB中
2、GC触发后,eden区的有效对象会被复制到from或to中,假设是from。大对象可能直接进入了老年代,如果from已满,也会直接进入老年代
3、此时eden区和to区的对象,都是垃圾对象,可以直接清空。
4、下一次GC时,from是有对象的,eden区和from区域的有效对象,会进入to区或老年代,规则和第2条相同。也就是说,from和to是使用复制算法的一个典型应用,保证其中一个为空,gc时将对象都复制到该区域,之后直接清空另一块区域。
5、eden区的对象都有年龄。每熬过经历一次gc,年龄加1. 熬过一定次数的gc,就可以进入老年代,也叫晋级(这个次数默认是15次)。
虚拟机提供了一个参数 MaxTenuringThreshold,来控制晋级的年龄。但是这个参数只是充分非必要条件。也就是说,达到这个年龄,会一定晋升。不到这个年龄,也可能晋升,比如survivor区域满了。这个是JVM根据运行时的survivor区的使用情况动态算出来的。
计算晋升年龄的基本逻辑代码如下:
//TargetSurvivorRatio是from或to的使用率
size_t desired_survivor_size = (size_t) ((((double) survivor_capacity) * TargetSurvivorRatio) / 100);
size_t total = 0;
int age = 1;
//sizes数组保存了每个年龄的对象之和,比如sizes[1]是所有年龄为1的对象大小之和
assert(sizes[0] == 0, "no objects with age zero should be recorded");
while(age < table_size) {
total += sizes[age];
if (total > desired_survivor_size) break;
age++;
}
int result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
五、TLAB简介
全称叫Thread Local Allocation Buffer,即线程本地分配缓存。
是因为对象一般分配在堆上,堆是全局共享的。所以在同一时间,可能有很多线程在堆上申请空间。每次分配都必须同步,竞争激烈的情况下,分配对象的效率会进一步下降。
所以为了提高分配的效率,使用TLAB这种线程专属的区域避免多线程冲突。而TLAB本身是在Eden上。