java基础学习JVM中GC的算法
在java学习到jvm时候,总会很多朋友问到关于gc算法的问题,小编在此给大家整理关于jvm中gc算法的原理以及图文详细分析,希望能够帮助你对这个gc算法的理解。
jvm内存组成结构:
(1)堆
所有通过new创建的对象都是在堆中分配内存,其大小可以通过-xmx和-xms来控制,堆被划分为新生代和旧生代,新生代又被进一步划分为eden和survivor区。survivor被划分为from space 和 to space组成,结构图如下:
(2)栈
每个线程 执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包含局部变量区和操作数栈。用于存放此次方法调用过程中的临时变量,参数和中间结果
(3)本地方法栈
用于支持native方法的执行。存储了每个native方法调用的状态
(4)方法区
存放了要加载的类信息,静态变量,final类型的常量,属性和方法信息。jvm用持久代(permanet generation)来存放方法区,可通过-xx:permsize和 -xx:maxpermsize来指定最小值和最大值。
(5)程序计数器
每个线程私有,当前线程执行的字节码的行数。
java堆内存分配机制
java内存分配和回收概括地说:就是分代分配,分代回收。对象将根据存货的时间被分为:young generation, old generation,permanent generation。
yong generation:对象被创建时,内存的分配首先发生在年轻代(大对象可以直接创建在old generation),大部分的对象在创建后很快不再使用,因此很快变得不可达,被young generation 的gc机制清理掉(ibm的研究表示,98%的对象都是很快消亡的),这个gc机制被称为minor gc或者 young gc;minor gc并不代表内存不足。
young generation分为 3个区域, eden区,两个 survivor区(from survivor, to survivor),内存分配过程如下所示:
1.绝大多数对象刚创建被分配在 eden区,其中的大多数对象很快就会消亡,eden区域是连续的内存空间,在其上分配内存极快。
2.最初一次,当eden区满的时候,执行 minor gc,将消亡的对象清理掉,并将eden,survivor 1剩余的对象复制到到一个存活区 survivor 0(此时survivor 1是空白的,两个survivor总有一个是空白的)
3.下次eden满了,在执行一次 minor gc,将消亡的对象清理掉,存活的对象复制到survivor1中,清空eden区。将survivor 0 中消亡的对象清理掉,将其中可以晋级的对象晋级到old区,将存活的对象也复制到survivor 1中,清空survivor 0
4.当被两个存活期 来回复制了几次之后,(用-xx:maxtenuringthreshold 控制,大于该值进入old generation,但是这只是个最大值,并不代表一定是这个值,因为:为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了maxtenuringthreshold才能晋升老年代,如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到maxtenuringthreshold中要求的年龄。)仍然存活的对象,将被复制到old generation。
old generation:对象如果在young generation 存活了足够长的时间而没有被清理掉,则会被复制到old generation,old generation 的空间一般比young generation大得多,发生的gc次数也比年轻代少,当年老代内存不足时,将执行 major gc,也叫 full gc;
可以使用-xx:+useadaptivesizepolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整java堆中各个区域的大小以及进入老年代的年龄。
如果对象比较大,young generation空间不足,则大对象会直接分配到old generation(大对象可能提前触发gc,应尽少使用大对象,更少用短命的大对象)。用-xx:pretenuresizethreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
可能存在年老代对象引用新生代对象的情况,如果需要执行young gc,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。young gc时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。
java 不同代gc 机制
young generation:发生的gc是minor gc,使用停止-复制算法进行清理,将新生代内存分为:较大的eden,和两个相等survivor,每次进行清理时,把eden 和一个survivor中存活的对象 复制到另一个survivor中,如果存活的对象超过了survivor内存,则需要通过空间分配担保机制将一部分对象复制到old generation,然后清理掉eden,和刚才的survivor。可以通过 -xx:survivorration参数来调整eden和survivor区的内存容量比值。默认是8,eden:survivor:survivor = 8:1:1。
old generation:发生major gc。存储的对象比young generation多得多,且存在很多大对象,对old generation进行内存清理,如果使用 停止-复制算法,相当低效,一般使用 标记-整理算法,标记出仍然存活的对象(存在引用),将所有存活的对象向一端移动,以保证内存的连续。
在发生minor gc时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次full gc,否则,就查看是否设置了-xx:+handlepromotionfailure(允许担保失败),如果允许,则只会进行minorgc,此时可以容忍内存分配失败;如果不允许,则仍然进行full gc(这代表着如果设置-xx:+handle promotionfailure,则触发minorgc就会同时触发full gc,哪怕老年代还有很多内存,所以,最好不要这样做)。
old generation gc之 标记-清除法:标记出所有需要回收的对象(可达性分析),标记完成后统一清理掉所有被标记的对象。
该算法有两个问题:
(1)下频率问题:标记和清除过程效率都不高
(2)空间问题:标记清除后会产生大量的不连续的内存碎片,空间碎片太多会导致在运行过程需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
old generation gc之 标记整理算法:标记过程和标记清除算法一样,但是后续步骤不再对可回收对象直接清理,而是让所有存活对象都向一端移动,然后清理掉边界以外的内存。
permanent generation:永久代的垃圾收集分为两类:废弃的常量和不再被使用的类。
废弃常量:比如说我们在常量池中用intern()添个字符串常量“a”,但是现在的系统没有任何一个string对象叫“a”,所以这个常量就是废弃了。
不再被使用的类:
①该类的所有的实例都已经被回收,java堆中不存在该类的任何实例。
②加载类的classloader已经被回收
③没有该类的java.lang.class对象被引用,即不能通过反射访问该类信息。
满足了上述三个条件只是满足了类回收的基本条件,是否回收不用的类需要看设置的-xnoclassgc参数进行控制,还可以使用-verbose:class及-xx:+traceclassloading、 -xx:+traceclassunloading查看类的加载和卸载信息。
在大量使用反射、动态代理、cglib等bytecode框架的场景,以及动态生成jsp和osgi这类频繁自定义classloader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。
算法分析:
空间分配担保:
在执行minor gc前, vm会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个survivor作为轮换备份, 因此当出现大量对象在minor gc后仍然存活的情况时, 就需要老年代进行分配担保, 让survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成gc前是无法明确知道的, 因此minor gc前, vm会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行minor gc, 否则进行full gc(让老年代腾出更多空间).然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次minor gc存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(handle promotion failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次full gc(让老年代腾出更多空间).
gc回收对象确立:
引用计数:如果有引用这个对象的,对象计数器+1,引用失效,计数器-1,绝大多数情况下,这个算法高效简单,但是不能解决对象之间的循环引用的关系,所以没有被主流语言采用。
可达性算法:通过一系列的称为 gc roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/reference chain, 当一个对象到 gc roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图: object5、6、7 虽然互有关联, 但它们到gc roots是不可达的, 因此也会被判定为可回收的对象:
在java, 可作为gc roots的对象包括:
方法区: 类静态属性引用的对象; 方法区: 常量引用的对象; 虚拟机栈(本地变量表)中引用的对象. 本地方法栈jni(native方法)中引用的对象。
可达性算法如果对象到gc roots不可达也不是说这个对象会立即被回收,是需要经过一个两次标记过程的,第一次:是在可达性分析后发现没有与gc roots相连接的引用链。第二次是判断是否需要执行finalize(),这个方法也是对象唯一能够进行自我救赎的机会了,但是不推荐使用,因为这个方法运行代价高,不确定性大,不能保证不同对象的执行顺序。如果不需要执行该方法,直接就进行回收,如果需要执行该方法,那么该对象会被放在一个f-queue里。虽然会被执行,但是不一定保证能够执行成功,因为有可能会在这个方法执行过程中出现死循环等意外情况,所以虚拟机并不一定会等待这个方法执行结束才进行回收。如果在第二次标记的时候,该对象没有成功的自我拯救,那么就真的被回收了。
常用jvm配置参数
-xx:cmsinitiatingpermoccupancyfraction:当永久区占用率达到这一百分比时,启动cms回收
-xx:cmsinitiatingoccupancyfraction:设置cms收集器在老年代空间被使用多少后触发
-xx:+cmsclassunloadingenabled:允许对类元数据进行回收
-xx:cmsfullgcsbeforecompaction:设定进行多少次cms垃圾回收后,进行一次内存压缩
-xx:newratio:新生代和老年代的比
-xx:parallelcmsthreads:设定cms的线程数量
-xx:parallelgcthreads:设置用于垃圾回收的线程数
-xx:survivorratio:设置eden区大小和survivior区大小的比例
-xx:+useparnewgc:在新生代使用并行收集器
-xx:+useparallelgc :新生代使用并行回收收集器
-xx:+useparalleloldgc:老年代使用并行回收收集器
-xx:+useserialgc:在新生代和老年代使用串行收集器
-xx:+useconcmarksweepgc:新生代使用并行收集器,老年代使用cms+串行收集器
-xx:+usecmscompactatfullcollection:设置cms收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-xx:+usecmsinitiatingoccupancyonly:表示只在到达阀值的时候,才进行cms回收
(+不能省)
-xms:设置堆的最小空间大小。
-xmx:设置堆的最大空间大小。
-xx:newsize设置新生代最小空间大小。
-xx:maxnewsize设置新生代最大空间大小。
-xx:permsize设置永久代最小空间大小。
-xx:maxpermsize设置永久代最大空间大小。
-xss:设置每个线程的堆栈大小
推荐阅读
-
java基础学习JVM中GC的算法
-
Java基础语法(8)-数组中的常见排序算法
-
Java中volatile关键字的效果 博客分类: Java基础 Java多线程thread工作JVM
-
Java中volatile关键字的效果 博客分类: Java基础 Java多线程thread工作JVM
-
DK、JRE、JVM的区别及JavaSE、JavaEE和JavaME的区别 博客分类: java基础javaWeb学习
-
DK、JRE、JVM的区别及JavaSE、JavaEE和JavaME的区别 博客分类: java基础javaWeb学习
-
Java中的数组基础知识学习教程
-
Java中的数组基础知识学习教程
-
Java线程编程中Thread类的基础学习教程
-
Java线程编程中Thread类的基础学习教程