JVM之垃圾收集器与内存分配策略(三)

对于回收主要思考问题:
那些内存需要回收?
什么时候回收?
如何回收?
来展开
第一个问题:那些内存需要回收?
即在哪些区域回收?在这些区域中那些内存可以回收?(对象是否已死?)

回收区域

虚拟机栈、本地方法栈、程序计数器这些都属于线程私有区域,随线程而灭,栈中的栈帧在类结构确定下来时就基本已经确定,因此有条不紊的进行出栈和入栈操作,方法结束或线程结束内存就自然回收了。因此关注的回收区域为Java堆和方法区(主要是Java堆,回收效率高)
回收区域内存回收:
对于Java堆来说,主要回收已经无用的(死的)对象实例内存,所以要先解决那些对象已经死了或者可以回收了问题!
主要有两种方法:
1》引用计数算法:给对象添加一个引用计数器,当有一个地方引用它时,计数器加1,引用失效时减1,当计数器为0时表示没有地方引用它。这种方法实现简单,效率也很高,但是很难解决两个对象之间相互引用问题。
2》可达性分析算法:从“GC Roots”对象作为起点,开始搜索,搜索所走过的路称为引用链,如果一个对象到“GC Roots”没有任何引用链,则表示这个对象不可达。(可以被回收的对象)。sun HotSpot 使用这种。其中可以作为“GC Roots”的对象时:栈中本地变量表的引用对象、方法区类静态属性引用的对象,方法区中常量的引用対像,native方法引用的对象。
再谈引用
强引用:Object a=new Object()强引用a只要还存在就不会回收a引用的对象。
软引用:描述一些还有用但非必须的对象,这种是在内存即将发生溢出时,列为回收对象进行二次回收(还没有回收,回收有专门的线程)。如果回收后还没有足内存则抛出内存溢出异常。
弱引用:描述非必须对象,这种弱引用关联的对象只能活到下一次垃圾收集之前(这个收集并不一定是内存溢出前导致的)。
虚引用:最弱的一种引用关系,它的存在不会对其对象的生存时间产生影响,也无法通过虚引用来获得一个对象实例。只是回收的时候对象可以收到一个通知。
对象的自我拯救:
即使是不可达的对象,也不一定“非死不可”,对象的死亡至少要经过两次标记过程:如果与“GC Roots”之间没有引用链,则做第一标记并且筛选该对象是否要执行finalize()方法,如果已经调用过该方法或者没有覆盖该方法则没有必要执行。则会直接进入或等待第二次标记。如果需要自行,则会把它放在F-Queue的队列中,专门一个线程去执行(触发)它的方法,GC会对F-Queue中的对象第二次标记,如果这个时候建立有引用链,则就算逃脱,在第二次被标记的时候,移除“即将回收集合”,如果没有建立引用链则就基本上被回收了。
不过这个方法不建议使用拯救对象。代价大,不确定性大。
这里写图片描述
对于方法区来说:方法区回收效率低,主要回收两部分:废弃常量和无用的类。
常量回收:以常量池中的字母,字面量回收为例,字符串“abc”已经在常量池中,如果没有任何string对象引用常量池中的“abc”,那么“abc”常量需要被回收,类似方法、字段的符号、接口等。
类(class对象)回收:需要满足三个条件:
该类的所有实例对象都已经被回收,堆中不存在该类的任何实例。
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
同时满足才可以被回收,但不是一定会被回收。
什么时候回收?上面回收区域也讲到,对象等已经死掉!
如何回收?采用什么方法回收?

垃圾收集算法

几种垃圾收集算法:
1》标记-清除算法
第一阶段“标记”需要回收的对象,第二阶段“清除”回收对象区域。
缺点:效率不高,产生内存碎片
2》复制算法
原始是把内存分为两块大小相等的区域,只使用其中的一半,当这一半用完的时候,进行这块区域回收,把这块中存存活的对象移动到另一半区域,并且按顺序分配内存,这样就不会产生内存碎片。
但这样内存利用率太低,不过商用的虚拟机都用这种方法来回收新生代,不过不是按1:1划分,而是分为Eden和两个survivor空间,HotSpot 默认比例大小eden:survivor=8:1, 这种需要老年代分配担保。
3》标记-整理算法
复制算法并不适用老年代,因为老年代存活率比较高,需要复制更多的对象,也不想浪费空间。(二者折中)所以提出 标记-整理算法,标记与“标记-清除”算法一样,不过后面步骤不是直接清除对象内存,而是把所有存活的对象移动到连续的一块,然后清理外面的区域。
4》分代收集算法
利用前几种算法,根据对象存活的周期划分内存为几块:新生代、老年代
然后各区域使用合适的算法,比如新生代朝生夕死,采用复制算法,只有少量存活,则复制开销比较小。老年代存活率较高,则使用“标记-清理”或“标记-整理”算法回收。
HotSpot算法实现
判定对象存活和垃圾收集算法实现时,有严格的考量:
枚举根节点
可达性分析对执行时敏感体现在GC停顿上(stop the world),就需要时间冻结在某个点上,不能一边分析,一边还有对象引用关系变化。
安全点
HotSpot并没有为每个指令都生成OopMap,而是在“特定的位置”记录这些信息,称为安全点,即程序不是在所有的地方都停顿下来GC,只有到达安全点才停顿。这些安全点特征是长时间执行,比如循环跳转、方法调用异常跳转等。这些指令才会产生安全点。
安全区域
对于指定的线程处于阻塞或睡眠状态是,并没有分配CPU时间,JVM显然不能等这部分线程分配到CPU时间然后执行到安全点,对于这种情况,就需要安全区域来解决。
安全区域指这一段代码中引用关系不会发生改变,在这里开始GC是安全的,线程进入安全区则标识自己已经进入安全区域,这段时间发起GC(根节点枚举)则不需要管自己,当自己要离开安全区域检查系统是否已经完成根节点枚举额,完成则继续执行,否则等待可以离开的信号。
**

几种垃圾收集器

**
HotSpot中的收集器
连线表示可以配合使用
这里写图片描述

内存分配与回收策略

对象主要分配在新生代的Eden区,如果启动本地线程分配缓冲区,则优先在TLAB上分配,少数情况也会直接分配到老年代。
1)对象优先分配在Eden区
对象大多数在新生代Eden区分配,如果Eden区没有足够空间则进行一次Minor GC,把存活对象存进survivor(两部分来回复制),腾出的Eden区域用来分配新对象,如果这个时候Survivor空间还不够分配,则使用分配担保机制分配到老年代(这个机制是对于已经存活的对象区别于大对象直接进入老年代而没有进行回收空间)。
2)大对象直接进入老年代
JVM提供一个-XX:PretenureSizethreshold参数,可以设置大于这个值就可以直接进入老年代分配,主要是避免在新生代使用赋值算法需要复制大量内存数据。
3)长期存活的对象进入老年代
JVM给每一个对象定义一个对象年龄计数器,在Eden区存活并且survivor可以容纳,则年龄为1,在survivor区每经过一次Minor GC年龄就加1,虚拟机默认年龄15进入老年代。
4)动态对象年龄判定
JVM并不是必须要年龄达到阈值才可以晋升到老年代,如果survivor区中相同年龄的所有对象的和大于survivor区的一半,则大于或等于该年龄的对象直接进入老年代。
5)空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象的总空间,如果成立,那么Minor GC是安全的。如果不成立,则查看是否允许担保失败,如果允许,查看老年代的连续空闲空间是否大于历次晋升到老年代独享的平均大小。如果大于,则尝试进行一次Minor GC(有风险 失败full GC),如果小于,或者不允许担保失败,则进行full GC。

猜你喜欢

转载自blog.csdn.net/qq_26564827/article/details/80170658
今日推荐