理解JVM之GC&内存分配
垃圾回收机制
- 对象存活判定算法
- 垃圾收集算法
- 说是算法实现和垃圾收集器
- 内存分配和回收策略
- 对象存活判定算法(JVM回收那些对象)
概念:四种引用类型
- 强引用:及时内存不足也不会被GC,不会随意回收强引用对象;即使抛出OOM(OutOfMemoryError),也不会随意回收强引用对象
- 软引用:只有内存不足时,会被GC;回收后内存不足会抛出OOM异常
- 弱引用:无论当前内存是否充足都会被GC
- 虚引用:任何时候都会别GC
a.引用计数算法:给对象添加一个引用计数器,每当有一个地方引用他的时候就加1,当有引用失效的时候就减1;任何时刻计数器为0的对象是不能被使用的
(但是因为他不能解决相互循环引用问题,所以没有被主流JVM所使用)
b.可达性分析法:以【GC ROOT】对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC ROOT没人任何引用链时,则 这个对象是不可用的。
可以作为GC ROOT的对象
- 虚拟机栈中引用的对象,主要是指栈帧中的本地变量
- 本地方法栈中Native方法引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用对象
需要注意的是:在可达性分析法中被判定不可达对象未必真的就判死刑了,至少要经历两次标记过程,判断对象是否有必要执行finalize(),若判定有必要的话,还会在进行一次筛选,在finalize()中如果该对象与引用链中的任何一个对象建立关系,则它将被移除"即将回收"的集合
2.垃圾收集算法(介绍JVM怎么回收掉这些对象)
a.分代收集算法(是当前商业虚拟机都采用的一种算法)
- 新生代:大批对象死去,只有少量存活。使用『复制算法』,只需复制少量存活对象即可
- 老年代:对象存活率高。使用『标记—清理算法』或者『标记—整理算法』,只需标记较少的回收对象即可。
b.复制算法
- 把可用内存按容量划分为相等的两块,每次只使用其中一块。当其中一块内存用尽后,把还存活着的对象复制到另外一块内存,在将这一块内存清理掉。
- 优点:每次都是对整个半区进行内存回收,无需考虑内存碎片等情况。只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
- 缺点:每次可使用的内存缩小为原来的一半,内存使用率低
有研究表明新生代中的对象98%是朝生夕死的,因此没必要按照1:1来划分内存空间,而是分为一块较大的Eden空间和两块较小的Survivor空间,
在HotSpot虚拟机中默认比例为8:1:1。每次使用Eden和一块Survivor,回收时将这两块中存活着的对象一次性地复制到另外一块Survivor上,再做清理。可见只有10%的内存会被“浪费”,倘若Survivor空间不足还需要依赖其他内存(老年代)进行分配担保。
c.标记清除法:首先标记需要被回收的对象,然后统一清除这些对象
缺点:标记、清除效率不高;空间碎片太多,会产生大量不连续的空间碎片,可能会导致在后面需要分配较大对象时,因为无法找到连续较大的空间而提前 触发另一次GC,影响性能。
d.标记-整理算法:首先『标记』出所有需要回收的对象,然后进行『整理』,使得存活的对象都向一端移动,最后直接清理掉端边界以外的内存。
优点:即没有浪费50%的空间,又不存在空间碎片问题,性价比较高。
一般情况下,老年代会选择标记-整理算法。
3.HotSpot算法实现&垃圾回收器
a、枚举根节点
主流Java虚拟机是使用的准确式GC,在系统执行停顿之后无需检查所有执行上下文和全局引用位置,而是通过一些方法直接获取存放对象引用的地方;在HostSpot中是通过一组成为OopMap的数据结构实现的,完成类加载后会计算出对象某偏移量上某类型数据,JIT编译时会在特定的位置记录栈和寄存器中引用的位置。这样GC扫描时就可以直接获取这些信息,并快速准确完成GC Roots枚举
b、安全点
上面提到的特定的位置就是安全点,即程序执行时并非在所有地方都停顿执行GC,只在到达安全点时才暂停,降低GC的空间成本。
- 安全点的选定标准:可让程序长时间执行的地方,如方法调用、循环跳转、异常跳转等具有指令序列复用的特征。
- 使所有线程在最近的安全点上再停顿的方案:
- 抢占式中断:在GC发生时几乎把所有线程都中断,若线程中断不再安全点上就恢复线程,让‘它’跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
- 主动式中断:在GC中断线程时不直接对它进行操作,而是设置一个中断标志,让各个线程在执行时主动轮询它,当中断标志为真时就中断挂起
- 抢占式中断:在GC发生时几乎把所有线程都中断,若线程中断不再安全点上就恢复线程,让‘它’跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件。
c、安全区域
安全点机制只能保证程序执行时,在不太长的时间内遇到可进入GC的安全点,但在程序不执行时(如线程处于Sleep或Blocked状态)线程无法响应JVM的中断请求,此时就需要安全区域来解决。
- 安全区域:在安全区域中的任意地方开始GC都是安全的,可看做是扩展的安全点。
- 执行过程:当线程执行到安全区域中的代码时就标识一下,如果这时JVM要发起GC就不用管被标识的线程;在线程离开安全区域是要检查自己是否完成了根节点枚举,若完成则继续执行;否则等待直到收到可以安全离开安全区域的信号为止。

并发(Concurrent):垃圾收集线程与用户线程一段时间内同时工作,用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
5.内存分配与回收策略
- 对象优先在Eden区分配:大多数情况下对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时虚拟机将发起一次Minor GC。
-
新生代GC(Minor GC):发生在新生代的垃圾收集动作。较频繁、回收速度也较快。
老年代GC(Major GC/Full GC):发生在老年代的垃圾收集动作。出现Major GC经常会伴随至少一次的Minor GC。速度一般比Minor GC慢10倍以上。 - 大对象直接进入老年代:对于需要大量连续内存空间的Java对象(如很长的字符串以及数组),如果大于虚拟机设定的
-XX:PretenureSizeThreshold参数值将直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。 - 长期存活的对象进入老年代:虚拟机会给每个对象定义一个年龄计数器,当对象在Eden出生并经过第一次Minor GC后仍存活且能被Survivor容纳的话,将被移动到Survivor空间中并将对象年龄设为1;当对象在Survivor区中每“熬过”一次Minor GC年龄就+1,直至增加到一定程度(默认为15岁,可通过
-XX: MaxTenuringThreshold设置)就会被晋升到老年代中。 - 动态对象年龄判定:为了能更好地适应不同程序的内存状况,虚拟机并不要求一定要达到
-XX: MaxTenuringThreshold设置值才能晋升到老年代,当Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象可以直接进入老年代。 - 空间分配担保:在发生Minor GC之前虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若是,说明可确保Minor GC是安全的,反之虚拟机 会查看
-XX:HandlePromotionFailure设置值是否允许担保失败;若允许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;若大于,将尝试进行一次Minor GC,若小于或者不允许担保失败,将改为进行一次Full GC。
解释:当大量对象在MinorGC后仍然存活的情况时,需要借助老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代,但前提是老年代本身还有容纳这些对象的剩余空间,由于在完成内存回收之前无法预知实际存活对象,只好取之前每次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,从而决定是否进行Full GC来让老年代腾出更多空间。
-XX:PretenureSizeThreshold参数值将直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。链接:https://www.jianshu.com/p/a62697f00b85