Java虚拟机(2)-----内存的回收与分配

        Java虚拟机的运行时数据区共有5快区域,其中本地方法栈,虚拟机栈,程序计数器随线程生而生,随线程灭而灭。用虚拟机栈举例,一个线程的虚拟机栈所占用的内存在编译期已经大致确定(由类的结构决定),所以内存的回收并不关注这三个区域。

        而堆和方法区由于只能在运行时才能确定有多少对象会被创建和回收,所以,内存的回收和分配主要集中在这两个区域。


一.内存的回收

    1.1方法区如何回收内存

            首先,Java虚拟机规定中,方法区的内存回收机制可以选择实现,也可以选择不实现。满足回收条件也不一定会立即回收,会在有需要的时候才进行回收

            在这个前提下,如果实现了内存回收机制的话,主要时针对两个方面:1废弃常量    2无用的类

                1.废弃常量:指没有任何对象引用指向这个常量或者他的字面量。举例,运行时常量池中有“abc",如果没有任何String对象引用”abc"的话,那么,"abc“就是可以被回收的。

                2.无用的类:无用类的判定比较严苛,主要有三个要求:1.堆中没有此类的实例对象    2.此类的类加载器被回收    3.此类的类字面常量没有被引用,无法通过反射访问该类的方法。

    1.2堆如何回收内存

            当进行内存回收时,会有如下两个个判定步骤也成为2次标记:

            1.是否在GC roots引用链上(可达性分析算法):所谓的GC roots是一系列的对象,包括1.虚拟机栈的栈帧的本地变量表所引用的对象    2.方法区中静态引用的对象以及常量引用的对象    3.本地方法栈(Native方法)引用的对象。引用一般来说分为4种,包括:1.强引用(代码中的引用,比如Object o = new Object();),只要强引用还存在,就不会别回收。    2.软引用(有用但非必须的对象)在内存溢出异常发生之前会回收点此类对象的内存,如果还不够,才会发出内存溢出异常。    3.弱引用(非必须对象,强度比软引用要弱),在下一次垃圾手机时就会被回收。    4.虚引用(唯一目的时在被回收时发出系统通知)完全不影响其生存时间。GC root引用链就是由GC roots对象及其引用不停扩展所形成的引用链条。

                这部分标记有两种情况,上文所说的是可达性分析算法,还有一种是引用计数算法,引用计数算法的基本原理是每个对象都有一个引用计数器,此对象被引用就+1,引用失效就-1,值位0是就是可被回收的,就会被标记。引用计数算法的优点是速度快,缺点是存在死循环是会失效。

            2.finalize()方法的调用情况:若回收对象的finalize()方法被重写且未被调用,那么,此finalize()方法会被放入F-Queue中,此队列有单独的Finalizer线程处理,执行其中的finalize()方法(不一定会执行完毕,避免方法中的死循环造成队列后面的finalize()不能执行),如果finalize()中,此对象重新被加入到了GC roots引用链中,那么,这个对象这次可以不进行回收。否则,回收。

                如果finalize()方法未被重写或者已被调用过一次(任何finalize()方法只会被调用一次),则会被回收。

                最好不要使用finalize()方法!!!运行代价大,不确定性也大,无法保证对象的调用顺序,指是对C/C++的析构函数的妥协。最好使用try/finally语句块。

       1.2.1垃圾回收算法

                内存到底是如何回收的呢,有如下具体的算法:
                    1.标记-----清除算法:分为两个阶段,标记阶段上文已经说过,标记完成后统一进行回收。他又一些缺点,效率不高,形成大量的空间碎片。不太常用
                    2.标记-----整理算法:在标记后,不直接清除对象,而是让存活的对象向前移动,然后清理掉边界外的内存

                   3.复制算法:将内存分为大小相等的两块,每次只用其中一块内存。用完一块内存是,将存活的对象复制到另一块内存上,然后清理点现在这个内存。优点是简单高效,缺点是只能用一般的内存。

                                        目前常见的JVM都是使用这种算法进行回收新生代内存区域;但是有一点不同的是,它的内存比例划分是8:1:1;分别是Eden(新生代):Survivor(幸存代):Survivor(幸存代);可以使用的为新生代和一个幸存代;其中10%的幸存代作为Copy算法的未使用内存,当然一旦10%幸存代无法全部复制所有存活的对象,可以通过借用老年代内存暂时使用


                                

            4.分代收集算法:当前商用虚拟机都采用这种算法,他将内存分为几块,一般分为新生代和老年代。然后根据不同的年代采用不同的收集算法。新生代就采用复制算法,因为有大量的对象会死去。而老年代则采用标记-----清理/整理算法,因为老年代对象的存活率较高。

    

1.2.2HotSpot的算法实现

枚举根节点(寻找GC ROOT)

由于目前大多数JVM垃圾回收判断机制都是采用的可达性分析,那么在JVM需要发生GC的时候,就需要寻找执行方法中的根节点(GC ROOT);如果说就目前程序一直在不停的执行,那么对于根节点(GC ROOT)是有可能在不停的变化的;我们这里需要JVM停止当前所有程序的执行(stop the world),才能进行根节点的可达性分析;但是就目前常见的Java方法可能极大,所以HotSpot提出一个OopMap的数据结构来存储目前方法内的对象地址和偏移量(可以直接理解为GC ROOT的位置)

安全点

对于一段执行的方法来说,如果需要在每一行记录OopMap那么JVM的空间成本就会提高,事实上HotSpot也并没有给每一条指令生成OopMap;只是在“特定的位置”上生成OopMap,这些特定位置我们称之为安全点

  1. 安全点生成标准

    对于普通的指令来说,执行速度是非常快的,一般来说安全点都在“长时间执行”的指令处生成,例如方法调用,循环跳转和异常跳转等

  2. 执行到安全点的方式

    因为安全点不是在每一条指令上生成,那么一旦垃圾收集器需要回收内存的时候,那么需要让每一个线程执行到安全点的位置(因为有可能部分线程现执行位置不在安全点),执行方式如下

    1. 抢占式中断

      在GC发生时,JVM会首先停掉所有线程,当发现某一线程不在安全点,恢复线程执行状态,直到所有线程都在安全点上,这样称之为抢占式中断;但是绝大多数JVM都没有采用

    2. 主动式中断

      在GC发生时,JVM不会直接对所有线程直接操作,而是设置一个标识,所有线程在执行到安全点的时候都会主动去轮询这个标识,一旦标识为真,那么线程自动挂起直到GC完成,设置标识为假的时候继续执行;这样称之为主动式中断

安全域

安全域是对安全点的一个补充或者是一种拓展;安全域指的是在一段特定的区域所有垃圾收集器在进行根节点的可达性分析都是可行的;之所以要加入安全域是因为线程可能出现等待或者休眠的状态,那么GC不可能去等待休眠的线程执行到安全点,所有拓展了安全域的概念

1.2.3垃圾收集器

Java虚拟机垃圾收集器

上图主要显示两个区域(Young generation:新生代;Tenured generation: 老年代)使用不同的收集器;中间的连线说明它们直接可以搭配使用

Serial收集器

  1. 概念

    从名称我们可以看出来,Serial是一个单线程收集器;所谓单线程收集器意思就是Java虚拟机发生GC的时候,需要停止所有线程的执行,然后Serial单线程进行GC直到完成

  2. 使用算法

    使用复制算法收集新生代内存

  3. 搭配老年代收集器

    1. CMS(Concurrent Mark Sweep):同步标记-清除收集器
    2. Serial Old:老年代单线程收集器
  4. 优缺点

    1. 优点:避免线程之间通信消耗成本;简单高效适合客户端(client)模式运行的JVM
    2. 缺点:需要收集的新生代内存大时,暂停(Stop the world)时间长,无法忍受
  5. Serial和Serial Old运行示意图

    Serial/Serial Old垃圾收集器运行示意图

ParNew收集器

  1. 概念

    ParNew实际上是Serial的多线程实现,就是在GC发生时是多个线程==并行==收集内存

  2. 参数设置(与Serial实现一样)

    1. -XX:PretenureSizeThreshold:设置至今进入老年代的对象大小
    2. -XX:SurvivorRatio:设置新生代中Eden和Survivor比例大小;默认是8
    3. -XX:HandlePromotionFailure:是否一下分配担保失败;1.5默认关闭;1.6某人开启
    4. -XX:+UseConcMarkSweepGC:设置老年代GC使用CMS,默认新生代收集器是ParNew
    5. -XX:UseParNewGC:强制新生代使用ParNew作为垃圾收集器
    6. -XX:ParallelGCThreads:设置ParNew收集线程数;少于或者等于8个CPU时,默认值为CPU值;CPU数量大于8时,默认值比CPU值少
  3. 使用算法

    和Serial一样同样也是使用复制算法

  4. 搭配老年代收集器

    1. Serial Old:老年代单线程收集器
    2. CMS(Concurrent Mark Sweep):同步标记-清除收集器
  5. 优缺点

    1. 优点:随着CPU数量的增多,ParNew相比于Serial可以更好的利用CPU;效率更好
    2. 缺点:CPU数量为1时,ParNew在线程之间切换上下文耗费资源导致效率肯定不如Serial;当然在CPU数量为2时;ParNew也不敢百分百保证超过Serial收集器
  6. 并行和并发区别

    1. 并行(Parallel):是指多个线程同时收集内存,但是对于用户线程依然是停止的
    2. 并发(Concurrent):是指用户线程和GC线程同时进行中(当然对于GC线程也可以是多个并行的),用户线程依然进行着;而GC线程收集线程也进行着

Parallel Scavenge收集器

  1. 概念

    Parallel Scavenge是一个新生代收集器,使用复制算法,也是并行收集;相比于ParNew收集器不同的是:Parallel Scavenge可以设置吞吐量

  2. 吞吐量的概念

    吞吐量指的是CPU运行用户代码时间与CPU消耗总时间的比值(CPU消耗总时间为用户代码运行时间+垃圾回收消耗时间)

    吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
    • 1
  3. 适配老年代收集器

    1. Parallel Old收集器
    2. Serial Old收集器
  4. 参数设置

    1. -XX:MaxGCPauseMillis:设置最大垃圾收集时间,大于0的毫秒数

      这个参数并不是设置越小越好,一旦设置太小,会导致收集器更加频繁的触发GC,导致收集器的吞吐量下降

    2. -XX:GCTimeRatio:设置最大垃圾收集时间,占程序运行总集时间的比率,设置值m为0

Serial Old收集器

  1. 概念

    Serial Old收集器是Serial老年代收集器;当然它也是作为一个单线程收集器存在的,一般来说只在虚拟机Client模式下选择

  2. 适配收集器

    1. 可以适配新生代Serial收集器使用
    2. 可以适配新生代Parallel Scavenge收集器使用
    3. 可以组合老年代CMS一起使用
  3. 使用算法

    使用标记-整理算法

Parallel Old收集器

  1. 概念

    作为Parallel Scanvenge收集器的老年代收集器,也是作为一个注重吞吐量的收集器

  2. 搭配收集器

    1. 只能搭配Parallel Scavenge新生代收集器;但是在大多数单核CPU下Parallel Scavenge收集器发挥的性能不一定比Serial收集器强

==CMS(Concurrent Mark Sweep)收集器==

  1. 概念

    CMS收集器是一个老年代收集器;目的是一种以获取最短回收停顿时间为目标的收集器

  2. 适配收集器

    1. 搭配Serial收集器
    2. 搭配ParNew收集器
    3. 组合Serial Old一起共同组成老年代收集器
  3. 使用算法

    CMS使用“标记-清除”算法,这个算法可能导致产生大量的内存碎片

  4. 工作过程

    1. 第一步:初始标记:这一步任然需要“stop the world”,==耗时短(这里的耗时相比较与并发标记而言)==
    2. 第二步:并发标记:这一步GC线程是和用户线程同步进行的,==耗时长==
    3. 第三步:重新标记:这一步GC线程独占,需要“stop the world”,==耗时短==
    4. 第四步:并发清除:这一步GC线程和用户线程同时工作,最后回收内存

    CMS收集器示意图

  5. 优点

    “stop the world”时间短,大部分时间都是并发执行的

  6. 缺点

    1. ==由于采用了“标记-清除”算法可能导致产生内存碎片==;在产生大量的内存碎片的时候,那么导致无法为大对象找到合适的内存空间,那么就不得不进行Full GC,然后压缩内存空间腾出足够大的空间给大对象分配内存,这个过程是无法并发的
    2. ==CMS收集器堆CPU资源非常依赖==,因为CMS默认开启线程数量为:(CPU数量+3)/ 4,也就是说CPU数量在4个以上,至少占用了一个CPU作为GC线程使用;4个以下时,JVM提供了一种“增量式并发收集器”,大致思想就是用户线程和GC线程抢占CPU使用权限,但是这样导致GC时间增加,吞吐量下降
    3. ==CMS无法处理浮动垃圾(指的在CMS在并发清理的时候出现对象内存)==;因为CMS在并发清除的时候,必须保证剩余内存足够,所以CMS不能像其他老年代收集器一样,等到老年代内存几乎满了之后才进行GC,设置-XX:CMSInitiatingOccuancyFraction的值触发GC动作,1.5默认值为68;1.6默认值为92;这个值不能设置太高导致出现大量“Concurrent Model Failure”(因为GC触发频率太低,导致所剩下内存无法支持用户线程执行),设置太高导致GC触发频率上升
  7. 参数设置

    1. -XX:CMSInitiatingOccupannyFraction:这个参数用来控制触发CMS收集器进行收集动作的比例;一般来说老年代GC触发都是在老年代内存几乎满了之后,而因为CMS需要并发清除动作,所以需要提前清除;1.6默认值92,1.5默认值68
    2. -XX:UseCMSCompactAtFullCollection:用于内存碎片太多导致无法分配大对象Full GC开关;默认值是开启
    3. -XX:CMSFullGCsBeforeCompaction:用于执行设置执行多少次不进行压缩的操作;默认值是0,也就是每次进行Full GC的时候都进行压缩
  8. Full GC和Minor GC

    1. Full GC(又称Major GC):指的是发生在老年代的GC动作,通常伴随着一次Minor GC;速度通常非常慢
    2. Minor GC:指的是发生在新生代GC动作,速度通常非常快

G1收集器

  1. 概念

    G1作为一个可以使用新生代和老年代两块不同内存区域的收集器(但是G1所管理的内存区域与传统的分代内存模型有一定的区别,它划分了很多块Region,新生代由一些Region构成,老年代由其他Region构成,同时作为新生代或者老年代内存可以是不连续的),保证了类似CMS的并发处理的特性,在JDK1.7后正式提供商用版本

  2. 使用算法

    从整体(所有的Region)来看采用的“标记-整理”算法;从局部(两个Region)来说基于“复制”算法实现的;这样两种算法可以==避免CMS收集器所产生的内存碎片==

  3. 执行流程

    1. 初始标记:这个过程不能并发执行,需要“stop the world”,标记出需要回收的对象
    2. 并发标记:GC线程和用户线程同时执行,GC线程同步标记出需要回收的内存
    3. 最终标记:这个过程也是不能并发的,但是这一步需要记录对象的变化信息从Remembered Set Logs记录到==Remembered Set==中
    4. 筛选回收:根据用户期望的回收时间,最终进行回收,这个过程是并发处理的
  4. 如何在不同的Region中进行对象可达性分析

    试想一下,作为一个GC无论采用什么方式去回收内存,那么无法避免的就是对象的可达性分析,那么假如我们现在采用的是使用物理上不连续的多个Region来存储对象,那么我们假设其中一个Region中的对象等于null,那么能不能直接断定这个对象不具有可达性呢?答案肯定是不行的,因为为了保证虚拟机的完整性和正确性,我们需要确定这个Region对象是否还在其他Region中还存在实例(或者说具有可达性),那么常规的做法是遍历所有的Region然后最终判断该对象是否还存在实例,但是这样做法效率太低,虚拟机在处理这个问题上加入一个==Remembered Set==,这个东西我们可以理解为一个对象引用信息的记录表,在每一个Region上维护一个Remembered Set避免GC去全堆扫描,判断对象是否具有可达性

    其实作为其他GC收集器也是一个道理,只是因为只有一个整体的内存堆,那么只需要维护一个Remembered Set则可以避免全堆扫描

  5. G1特点

    1. 并发和并行:G1可以充分利用多CPU的优势做“初始标记”和“最终标记”;同时并行在“并发标记”和“整理回收”两个过程可以用户线程同时进行
    2. 分代收集:作为G1也是分代收集的模式依然保留下来了,处理不同类型对象,还不同的应对方式
    3. 避免内存碎片:因为采用的是“标记-整理”和“复制”算法,那么G1可以避免内存出现碎片的情况
    4. 可以预测的停顿:G1和CMS都是最为追求低停顿的收集器,但是G1提供了一个提顿时间设置的功能,这样我们可以在一定程度上控制垃圾收集停顿时间

1.3内存分配策略

  1. 对象优先在Eden分配
  2. 大对象直接进入老年代,可以通过参数-XX:PretenureSizeThreshold设置直接进入老年代的对象,大数组,长字符串等。
  3. 长期存活的对象将进入老年代,JVM通过对每一个对象提供一个年龄字段存放在对象头中,每当对象在通过一次Minor GC没有被回收就在年龄上+1,每一个对象初始化年龄是0,虚拟机默认进入老年代年龄是15,也可以通过-XX:MaxTenuringThreshold设置进入老年代的年龄
  4. 动态对象年龄判断,指的是当新生代中Survivor空间中相同年龄的所有对象大小的总和达到或者超过Survivor空间的一半,那么这个年龄或者大于这个年龄的所有对象都会直接进入老年代
  5. 空间分配担保模式,在进行Minor GC(也就是新生代GC),虚拟机会在去查看老年代最大连续空间是否大于当前所有新生代对象所占空间,如果大于,那么就直接开始Minor GC,但是如果不大于,==那么虚拟机就会查看-XX:HandlePromotionFailure是否是开启的,如果开启就分析之前每次进行Minor GC所存活对象平均占用空间数值,如果目前目前老年代空间大于之前平均大小,那么就冒险进行Minor GC(冒险是因为这一次GC有一定概念是无法成功的),如果小于那么直接停止Minor GC,先进行Full GC直到老年代腾出足够的空间==;如果-XX:HandlePromotionFailure是关闭的,那么就直接进行Full GC;但是虚拟机这个-XX:HandlePromotionFailure默认是开启的,避免每次进行Full GC

声明:全文多处是COPY的,原文在------点击打开链接,一共有4篇文章。


猜你喜欢

转载自blog.csdn.net/zh328271057/article/details/80554018
今日推荐