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:设置每个线程的堆栈大小

(0)

相关推荐

  • java基础学习JVM中GC的算法

    在java学习到JVM时候,总会很多朋友问到关于GC算法的问题,小编在此给大家整理关于JVM中GC算法的原理以及图文详细分析,希望能够帮助你对这个GC算法的理解. JVM内存组成结构: (1)堆 所有通过new创建的对象都是在堆中分配内存,其大小可以通过-Xmx和-Xms来控制,堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区.Survivor被划分为from space 和 to space组成,结构图如下: (2)栈 每个线程 执行每个方法的时候都会在栈中申请一个

  • Java基础学习之字符串知识总结

    一.前言 字符串是多个字符连接起来组合成的字符序列.字符串分为可变的字符串和不可变的字符串两种. (1)不可变的字符串:当字符串对象创建完毕之后,该对象的内容(上述的字符序列)是不能改变的,一旦内容改变就会创建一个新的字符串对象.Java中的String类的对象就是不可变的. (2)可变的字符串:StringBuilder类和StringBuffer类的对象就是可变的:当对象创建完毕之后,该对象的内容发生改变时不会创建新的对象,也就是说对象的内容可以发生改变,当对象的内容发生改变时,对象保持不变

  • 基于JVM 中常见垃圾收集算法介绍

    JVM 中常见的垃圾收集算法有四种: 标记-清除算法(Mark-Sweep): 复制算法(Copying): 标记-整理(Mark-Compact): 分代收集: 下面我们来一一介绍: 一.标记-清除算法(Mark-Sweep) 这是最基础的垃圾收集算法,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象.它的主要缺点有两个:一个是效率问题,标记和清除效率都不高:另一个是空间问题,标记清除后会产生大量不连续的内存

  • Java基础学习笔记之数组详解

    本文实例讲述了Java基础学习笔记之数组.分享给大家供大家参考,具体如下: 数组的定义于使用 1:数组的基本概念 一组相关变量的集合:在Java里面将数组定义为引用数据类型,所以数组的使用一定要牵扯到内存分配:想到了用new 关键字来处理. 2:数组的定义格式 区别: 动态初始化后数组中的每一个元素的内容都是其对应数据类型的默认值,随后可以通过下标进行数组内容的修改: 如果希望数组定义的时候就可以提供内容,则采用静态初始化的方式: a:数组的动态初始化(声明并初始化数组): 数据类型 数组名称

  • Java基础学习之集合底层原理

    一.Collection集合 Collection接口是单列集合类的父接口,这种集合可以将数据一个一个的存放到集合中.它有两个重要的子接口,分别是 java.util.List 和 java.util.Set 二.List接口 1.特点 List是一种有序的集合 List是一种带索引的集合 List是一种可以存放重复数据的集合 2.List接口三个主要实现类 3.[面试题]ArrayList.LinkedList.Vector的区别 ①ArrayList:线程不安全,查询效率高,插入.删除效率低

  • Java 对象在 JVM 中的内存布局超详细解说

    目录 一.new 对象的几种说法 二.Java 对象在内存中的存在形式 1. 栈帧(Frame) 2. 对象在内存中的存在形式 ① 3. 对象中的方法存储在那儿? 4. Java 对象在内存中的存在形式 ② 三.类中属性详细说明 四.细小知识点 1. 如何创建对象 2. 如何访问属性 五.Exercise 六.总结 一.new 对象的几种说法 初学 Java 面向对象的时候,实例化对象的说法有很多种,我老是被这些说法给弄晕. public class Test { public static v

  • 你知道JVM中GC Root对象有哪些吗

    目录 JVM中GC Root对象有哪些 (一)虚拟机栈中引用的对象 (二)方法区中类静态属性引用的对象 (三)方法区中常量引用的对象 (四)本地方法栈中引用的对象 JVM 中的 GC Roots 和可达链 什么是GC Root 对象? 常用的GC算法 GC Root 对象有哪些? 总结 JVM中GC Root对象有哪些 众所周知,我们目前最常用的虚拟机hotspot使用可达性分析来进行垃圾回收,而可达性分析需要依赖GC Root. 下面我就来介绍下可以作为GC Root的对象. (一)虚拟机栈中

  • Java对象在JVM中的生命周期详解

    概念 在Java中,对象的生命周期包括以下几个阶段: 创建阶段(Created) 应用阶段(In Use) 不可见阶段(Invisible) 不可达阶段(Unreachable) 收集阶段(Collected) 终结阶段(Finalized) 对象空间重分配阶段(De-allocated) Java对象在JVM中的生命周期 当你通过new语句创建一个java对象时,JVM就会为这个对象分配一块内存空间,只要这个对象被引用变量引用了,那么这个对象就会一直驻留在内存中,否则,它就会结束生命周期,JV

  • Java基础学习之实参和形参

    关于变量的赋值: 如果变量是基本数据类型,此时赋值的是变量所保存的数据值. 如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值. public class ValueTransferTest { public static void main(String[] args) { System.out.println("***********基本数据类型:****************"); int m = 10; int n = m; System.out.println(&

  • Java基础学习之方法的重载知识总结

    一.什么是方法的重载? 方法的重载是指一个类中可以定义多个方法名相同,但参数不同的方法. 调用时,会根据不同的参数自动匹配对应的方法. 二.构成方法重载的条件 1).不同的含义:形参类型.形参个数.形参顺序不同 2).只有返回值不同不构成方法的重载 如:int a(String str){} 与 int void(String str){} 不构成方法重载 3).只有形参的名称不同,不构成方法的重载. 如:int a(String str){} 与 int a(String s){} 不构成方法

随机推荐