JVM总结-垃圾收集器与内存分配策略
2019独角兽企业重金招聘Python工程师标准>>>
垃圾收集器
需要回收的对象实例
垃圾收集器在对堆进行回收时,首先要判断对象是否还存活。
判断对象是否存货的算法:
1、引用计数器(一般JVM都不用这个算法)
给对象添加一个引用计数器,每当一个地方引用他,程序计数器就加一,但引用失效时程序计数器减一,计数器为0的对象就是不再使用的对象。
优点:实现简单,判定效率高
缺点:很难解决对象之间循环引用的问题(例如两个对象互相引用,这样程序技术器永远不会为0)
2、可达性分析算法
算法的基本思想思路是通过一系列成为“GC ROOTS”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC ROOTS没有任何引用链相连说明这个对象是不可用的。
JAVA中可以作为GC ROOTS的对象
- 虚拟机栈中的引用对象
- 方法区中的静态属性引用对象
- 方法区中的常量引用对象
- 本地方法中的JNI引用的对象
判定为不可达对象被标记一次并进行筛选,刷选条件为此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者虚拟机已经调用过这个方法,都视为没有必要执行。
视为有必要执行,那么这个对象会被放到一个F_Queue的队列中,由虚拟机建立、优先级低的finalizer线程去执行。(并不承诺等待它结束,避免发生死循环或者很慢造成其他对象等待,导致内存回收系统崩溃)。GC对F_QUEUE队列中的对象进行第二次标记,如果没有重新与引用链上的对象关联则被真正回收,否则将被移除“即将回收的集合”。
方法区回收:
在方法去中进行垃圾回收性价比比较低。
永久代主要回收两部分的内容:
- 废弃的常量:没有任何对象引用常量池中的常量,也没有其他地方引用这个字面量,常量就会被系统清出常量池。
- 无用的类:需要满足
- 该类的所有实例都被回收
- 加载该类的ClassLoader被回收
- 该类对应的Class对象没有在任何地方被访问,无法在任何地方通过反射访问该类的方法
满足这三个条件仅仅是可以回收,而不是必然要回收,是否必要回收,通过JVM参数进行设置。
在大量使用反射,动态代理,CGLib等字节码框架,动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要JVM具备卸载的功能,以保证永久代不会溢出。
垃圾收集算法
标记清除算法
思想:首先标记所有需要回收的对象,在标记完成后统一进行回收。
缺点:
效率低,标记删除两个过程的效率都不高
- 标记清除后产生大量不连续的内存碎片,空间碎片太多会导致后面需要分配较大对象的时候,无法找到足够的连续内存而不得提前触发另一次垃圾收集动作。
复制算法
思想:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块用完,就将还存活着的对象复制到另一块,然后再把已使用过得内存空间一次清理掉。
优点:这样只需要对半个区进行内存回收,特不用考虑内存碎片问题,只要移动堆顶指针,按顺序分配内存,实现简单运行高效
缺点:将内存缩小了原来的一半
商业JVM都用这个算法来回收新生代,并不需要按照1:1来划分内存。而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。Eden和Survivor的比例为8:1,当回收时一次性将Eden和Survivor中存活的对象复制到另一块Survivor。也就是每次新生代可用内存为整个新生代内存的90%,只有10%的会被浪费。我们没有办法保证每次回收有不超过10%的对象还存活着。当Survivor内存不够时需要其他内存(老年代)做担保进行分配担保。在对象存活率较高时就进行了比较多的复制操作,所以老年代不会选择这个算法。
标记整理算法
过程和标记清理一样,只不过后续不是直接对可回收对象进行清理,而是让所有存活对象向一端移动,直接清除掉端边界以外的内存。
分代收集算法
根据对象存活周期的不同分为几块,一般把Java堆分为新生代和老年代。这样可以根据各个年代的特点采用适合的算法。新生代中,每次都有大量的对象死去,只有少量存活就采用复制算法,只需要付出少量存活对象复制成本。老年代对象存活率高,没有额外的空间进行分配担保使用标记清除或标记整理算法来尽心回收。
垃圾收集器
手机算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。
JVM对垃圾收集器没有明确的规范,所以各个JVM的垃圾收集器可能有较大的差别。
HotSpot中所包含的垃圾收集器
新生代
优点:简单高效,对于限定单个cpu的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得更高的单线程收集效率。所以Serial对于运行在Client模式下的虚拟机是一个很好的选择。
2、ParNew收集器
是Serial收集器的多线程版本,包括收集算法、回收策略、STOP THE WORD等。
除了Serial收集器外只有ParNew能够与CMS收集器配合工作。
3、Paraller Scavenge收集器
也是使用了复制算法,又是并行的多线程收集器,但是Paraller Scavenge收集器的目标是达到一个可控的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),而其他的关注点则是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短越适合与用户交互的成率,良好的响应速度能够提高用户体验。而高吞吐可以高效的利用CPU的时间,尽快完成程序的运算任务,主要适合在后台运算不需要太多交互的任务。
Paraller Scavenge无法与CMS配合工作
PS收集器参数
用于精确控制吞吐量
-XX:MaxGCPauseMillis:分别是控制最大垃圾收集停顿时间,GC停顿时间是以牺牲吞吐量以及新生代空间换取的因此不是越小越好。
-XX:GCTimeRadio:直接设置吞吐量大小,是垃圾收集时间占总时间的比例。默认值为99,就是允许最大1%的垃圾收集时间。
-XX:+UseAdaptiveSizePolicy:这是一个开关参数,打开这个参数,就不需要手工指定新生代的大小,Eden和Survivor的比例,晋升老年代对象的年龄。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最适合的停顿时间或者最大吞吐量。
老年代
4、Serial Old收集器
Serial Old是老年代版本,也是一个单线程收集器,使用标记整理算法。这个主要是给Client模式下的虚拟机使用。
5、Parallel Old收集器
是Parallel Scanvage的老年代版本,使用多线程和标记整理算法。
在注意吞吐量以及CPU资源敏感的场合,可以优先考虑使用Parallel Scavenge和Parallel Old。
6、CMS收集器
基于标记清除算法,以获取最短回收停顿时间为目标。目前很大一部分JAVA集中在互联网站或B/S系统的服务端上,这尤其重视服务响应速度,希望系统停顿时间短。给用户带来较好的体验。
运作过程分为
初始标记:标记GC ROOTS能够关联到的对象
并发标记:进行GC ROOTS Tracing 的过程
重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的哪一部分的对象标记记录,这阶段的停顿时间比初始标记稍长,但远低于并发标记。
并发清除:
初始标记和重新标记仍需要STOP THE WORLD,整个过程并发标记以及并发处理过程收集器程序和用户程序可以一起工作。总体上讲内存回收和用户线程可以并发执行。
优点:并发收集,低停顿
缺点:
1、CMS收集器对CPU资源十分敏感,
2、无法处理浮动垃圾
3、收集结束会产生大量的空间碎片
GI收集器
是一款面向服务端的垃圾收集器
优点:
1、并行与并发
2、分代收集
3、空间整合
4、可预测的停顿
使用G1收集器,JAVA堆的内存布局就与其他的不同。它将JAVA堆划分为多个大小相等的独立区域(Region),虽然还是分了新生代和老年代,但是新生代和老年代不再物理隔离,都是一部分不需要连续的Region的集合。
运作大致划分为
- 初始标记:仅仅标记GC Roots能关联到的对象,并且修改TAMS(Next Top At Mark Start)的值。让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象,这阶段需要停顿线程,但是耗时很短。
- 并发标记:GC Roots对堆中的对象进行可达性分析,找出存活对象,这部分耗时比较长,但是可与用户进行并发执行。
- 最终标记:修正因并发标记期间用户进程并发执行而导致标记产生变动的那一部分记录,JVM将这期间对象变化记录在线程Remembered Set Log中,最终标记需要把RSL中的数据合并到Remembered Set,这阶段需要停顿现在,但是可并行执行。
- 筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所需要的GC停顿时间来定制回收计划。这阶段可以做到与用户程序并发执行,因为只回收一小部分Region,时间是用户可控的,而且停顿用户线程可大幅度提高手机效率。
内存分配与回收策略:
自动内存管理最终归结为自动化解决两个问题:
- 给对象分配内存
- 回收分配给对象的内存
1、对象优先在Eden上分配
大多数情况下,对象分配到新生代的Eden中,当Eden没有足够的空间时,虚拟机会发起一次MinorGC。
2、大对象直接进入老年代
大对象是指,需要大量连续的内存空间,典型的大对象就是很长的字符串数组或者Char数组。
经常出现大对象就会导致内存还有不少空间时就要提前触发垃圾收集来获取足够的连续内存空间。
3、长期存活的对象直接进入老年代
虚拟机给每一个对象定义了一个对象年龄计数器,对象在Eden出生经过第一次的MinorGC,并且能被Survivor容纳,将被移动到Survivor空间,对象年龄设为1,对象在Survivor区中每熬过一次,年龄就增加1,年龄增加到一定程度(默认15),就会被晋升到老年区中。对象晋升到老年区的阈值,可以通过参数设置-XX:MaxTenuringThreshold。
4、动态对象年龄判断
为了能更好的适应不同程序的内存情况,虚拟机并不是要等到年龄必须达到了MaxTeuringThreshlod才会进入老年代。如果在Survivor空间中相同年龄的对象大小总和大于Surivivor空间的一半,年龄大于或等于该对象的直接可以进入老年代
5、空间分配担保
在发生Minor GC之前,JVM会检查老年代最大的连续空间内存是否大于新生代所有对象总空间。如果条件成立,那么Minor GC可以确保是安全的。如果不成立,则JVM会查看HandlePromotiomFailure设置的值是否允许担保失败。允许则会检查老年代连续可用的内存空间是否大于历次晋升到老年代的对象大小的平均值,如果大于则尝试进行一次Minor GC,尽管还是可能有风险。如果小于或者不允许担保,则改为进行一次Full GC。
当大量对象在Minor GC后还存活着,就需要老年代进行分配担保,前提是老年代本身有容纳这些对象的空间。一共多少对象存活在内存回收之间是不清楚地,只要取以前每一次晋升到老年代对象容量的平均值作为经验值,与老年代剩余的空间进行比较,决定是否执行Full GC让老年代腾出更多的空间。如果某次Minor GC存活后的对象突然增加,远远高于平均值,只好再进行一次Full GC,避免Full GC执行过于频繁。
(在JDK6后就不再使用HandlePromotiomFailure,只要老年代的连续空间大于新生代总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full GC)
转载于//my.oschina.net/u/4108677/blog/3034467
还没有评论,来说两句吧...