Java内存模型及GC算法

Java内存模型及GC算法

前言

学习记录使用,原文:https://www.juejin.im/post/6874867748120297480

java内存模型

在这里插入图片描述

提升响应速度和吞吐量为目标的性能优化的关键就在java堆和垃圾回收器。

堆和栈的内存分配

  • Stack(栈)是JVM的内存指令区,顺序分配,内存大小定长,速度很快;
  • Heap(堆)是JVM的内存数据区,分配不定长的内存空间;

静态和非静态方法的内存分配
非静态方法在调用前,必须先new一个对象实例,获得Stack中的地址指针,否则JVM将无法将隐含参数传给非静态方法。
静态方法,只要class文件被ClassLoader load进入JVM的Stack,该静态方法即可被调用。当然此时静态方法是获取不到Heap中的对象属性的。
前面提到对象实例及动态属性都是保存在Heap中,而Heap必须通过Stack中的地址指针才能够被指令(类的方法)访问到。因此可以推断出:静态属性是保存在Stack中的,而不同于动态属性保存在Heap中。正因为都是在Stack中,而Stack中指令和数据都是定长的,因此很容易算出偏移量,也因此不管什么指令(类的方法),都可以访问到类的静态属性。也正因为静态属性被保存在Stack中,所以具有了全局属性。
在JVM中,静态属性保存在Stack指令区内存区,动态属性保存在Heap数据内存区。
当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap区没有数据,然后程序计数器开始执行指令。如果,是静态方法,直接依次执行指令代码,当然此时指令代码是不能访问Heap数据区的;
如果是非静态方法,由于隐含参数没有值,会报错。因此在非静态方法执行前,要先new对象,在Heap中分配数据,并把Stack中的地址指针交给非静态方法,这样程序计数器依次执行指令,而指令代码此时能够访问到Heap数据区了。

  • 非静态方法有一个隐含的传入参数,该参数是JVM给它的;
  • 静态方法无此隐含参数,因此也不需要new对象;
  • 静态属性和动态属性;
  • 方法加载过程;

JVM内存模型

在这里插入图片描述

  1. 程序计数器
    程序计数器是用于存储每个线程下一步将执行的jvm指令,如该方法为native的,则程序计数器中不存储任何信息。
  2. JVM栈(JVM Stack)
    JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
  3. 堆(Heap)
    它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
    (1)堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
    (2)Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则依然是直接使用堆空间分配的。
    (3)TLAB仅作用于新生代的Eden Space,因此在编写java程序时,通多多个小的对象比大的对象分配起来更加高效。
  4. 方法区(Method Area)
    (1)在Sun JDK中这块区域对象的为PermanetGeneration,又称为持久代。
    (2)方法区域存放了所加载的类信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。
  5. 本地方法栈(Native Method Stacks)
    JVM采用本地方法栈来支持native发放的执行,此区域用于存储每个native方法调用的状态。
  6. 运行时常量池(Runtiem Constant Pool)
    存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。JVM在加载类时会为每个class分配一个独立的常量池,但是运行时常量池中的字符串常量池是全局共享的。

JVM堆内存(Heap)

JVM将堆分成了二个大区:新生代(Young)和老年代(Old),新生代又被进一步划分为Eden和Survivor区,而Survivor由FromSpace(S0)和ToSpace(S1)组成。Young中的98%的对象都是朝生夕死,所以将内存分为一块较大的Eden和两块较小的Survivor0、Survivor1,JVM默认的分配比例是8:1:1,每次调用Eden和其中的Survivor0(FromSpace),当发生回收的时候,将Eden和Survivor0(FromSpace)存货的对象复制到Survivor1(ToSpace),然后直接清理掉Eden和Survivor0的空间。
堆模型图如下:
在这里插入图片描述

新生代GC(Minor GC):
新生代通常存活时间较短,基于Copying算法进行回收,所谓Copying算法就是扫描存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC出发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。
老年代的GC(Major GC/Full GC):
老年代与新生代不同,老年代对象存活的时间比较长、比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫面出存活的对象,然后再进行回收未被标记的对象,回收后,对于空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。

垃圾回收算法

1、Mark-Sweep(标记-清楚算法)
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
在这里插入图片描述

标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2、Copying(复制)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
在这里插入图片描述
这种算法虽然简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活的对象很多,那么Copying算法的效率将会大大降低。新生代GC算法采用的就是这种算法。
**3、Mark-Compact(标记-整理)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是再完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
在这里插入图片描述

JVM中老年代GC就是使用的这种算法,老年代的特点是每次回收都只回收少量对象。

新生代GC:串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)

串行GC:在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-xx:+UseSeralGC来强制指定。
并行回收GC:在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParalleleGCThreads=4来指定线程数。
并行GC:与老年代的并发GC配合使用。

老年代GC:串行GC(Serial MSC)、并行GC(Parallel MSC)和并发GC(CMS)。

串行GC(Serial MSC):client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。
并行GC(Parallel MSC):吞吐量大,但是GC的时候相应很慢:server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。
并发GC(CMS):响应比并行gc快很多,但是牺牲了一定的吞吐量。

CMS垃圾回收算法

  • CMS满足对响应时间的重要性需求大于对吞吐量的要求;
  • 应用中存在比较多的长生命周期的对象的应用;
  • CMS用于老年代的回收,目标是尽量减少应用的暂停时间,减少full gc发生的机率,利用和应用程序线程并发的垃圾回收线程来标记清除老年代。

收集阶段

  • 初始标记(Initial Mark)
    (Stop the World Event,所有应用线程暂停)
    从root对象开始标记存活的对象。
    暂停时间一般持续时间比较短。
  • 并发标记(Concurrent Marking)
    和Java应用程序线程并发运行;
    遍历老年代的对象图,标记出活着的对象。
    扫描从被标记的对象开始,直到遍历完从root可达的所有对象。
  • 再次标记
    (Stop the World Event,所有应用线程暂停)
    查找在并发标记阶段漏过的对象,这些对象是在并发收集器完成对象跟踪之后应用线程更新的。
  • 并发清理(Concurrent Sweep)
    回收在标记阶段(marking phases)确定为不可达的对象。
    垃圾对象占用的空间添加到一个空闲列表(free list),供以后的分配使用。死对象的合并可能在此时发生,请注意,存活的对象并没有被移动。
  • 重置(Restting)
    清理数据结构,为下一个并发收集做准备。

触发场景

与其他老年代的垃圾回收器相比,CMS在老年代空间占满之前就应该开始。
CMS收集会在老年代的空闲时间少于某一个阈值的时候被触发(这个阈值可以是动态统计出来的,也可以是固定设置的),而实际的回收周期可能要延迟到下一次年轻代的回收。为什么要这样,前面已经有解释了。在某些极端恶劣的情况下,对象会直接在老年代中进行分配,并且CMS回收周期开始的时候,eden区尚有非常多的对象。这个时候初始标记阶段会有多于10-100倍的时间消耗。这个通常是因为要分配非常大的对象,几兆的数组等。为了尽量避免长时间的暂停,我们需要合理的配置。
启动CMS设置参数:
> -XX:+UseConcMarkSweepGC

配置固定的CMS启动阈值:
1、-XX:+UseCMSInitiatingOccupancyOnly
2、-XX:MCSInitiatingOccupancyFraction=70

如果CMS不能够在老年代清理出足够的空间,会导致异常,使得JVM临时启动Serial Old垃圾回收方式进行回收。这个会造成长时间stop-the-world暂停。全量的GC的原因可能有两个。
- CMS垃圾回收的速度跟不上
- 老年代中有大量的内存碎片
一个导致CMS需要进行全量GC的原因是永久代中的垃圾。默认情况下,CMS是不回收永久代中的垃圾的。如果在你的应用中使用了多个类加载器,或者反射机制,那么就需要对永久代进行回收。
采用参数-XX:+CMSClassUnloadingEnabled会打开永久代的垃圾回收。

通过使用以下的选项,可以使得CMS充分利用多核:

  • -XX:+CMSConcurrentMTEnabled 在并发阶段,可以利用多核
  • -XX:+ConcGCThreads 指定线程数量
  • -XX:+ParallelGCThreads 指定在stop-the-world过程中,垃圾回收的线程数,默认是cpu的个数
  • -XX:+UseParNewGC 年轻代采用并行的垃圾回收器

CMS的缺点

  • CMS占用CPU资源,4个CPU以上才能更好发挥CMS优势
    CMS并发阶段,它不会导致用户线程停顿,但会因为占用了一部分线程(或CPU资源)而导致应用程序变慢,总吞吐量会降低。
    CMS默认启动的回收线程是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程最多占用不超过25%的CPU资源。但是当CPU不足4个时(比如2个),那么CMS对用户程序的影响就可能变得很大,如果CPU负载本来就比较大的时候,还分出一半的运算能力区执行收集器线程,就可能导致用户程序的执行速度忽然降低50%,这也很让人受不了。
    为了解决这种情况,虚拟机提供了一种称为“增量式并发器”(Increnmental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记和并发清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,速度下降也就没有那么明显,但是目前版本中,i-CMS已经被生命为"deprecated"。
  • 产生浮动垃圾
    CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure"失败而导致另一次Full GC。
    原因:
    CMS并发清理阶段,同时用户线程还在运行着,伴随程序的运行自然会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好等下一次GC时再将其清理掉。
    这一部分垃圾就成为“浮动垃圾"。也是由于在来及收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
    在默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高出发百分比,以便降低内存回收次数以获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次”Concurent Mode Failure"失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新机型老年代的垃圾收集 ,这样停顿时间就很长了。所有说参数-XX:CMSInitiatingOccupancyFraction设置的太高会很容易导致大量"Concurrent Mode Failure"失败,性能反而降低。
  • 产生大量的空间碎片
    CMS是一款基于“标记-清除”算法实现的收集器,这意味着手机结束时会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC.
    为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用户在“享受”完Full GC服务之后额外免费附送一个碎片整理过程,内存整理的过程是无法并发的。
    空间碎片问题没有了,但停顿时间不得不边长了。虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值