JVM GC

JVM 垃圾回收

0. JVM内存模型(jmm)

https://www.jianshu.com/p/76959115d486 这篇非常详细

  1. 程序计数器 : 如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域

  2. 虚拟机栈: java方法的执行和结束对应着栈帧的入栈和出栈,

    • 栈帧 : 用于存储局部变量表,操作栈,动态链接,方法出口等信息

    • 局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

    • Java虚拟机栈可能出现两种类型的异常:

      1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
      2. 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。
  3. 本地方法栈 : 本地方法栈是与虚拟机栈发挥的作用十分相似, 区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++, 我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

  4. 堆 : 对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。因此需要重点了解下。

    实例在堆中( 也就是new的对象 )

  5. 方法区:

img

0. 内存溢出和内存泄漏是什么

溢出: 所需要用的内存大于系统给的内存

泄漏: 某对象不用了但是没被回收

1. GC机制

首先,需明白JVM 内置的通用垃圾回收原则,堆内存划分为 Eden、Survivor 和 Tenured/Old 空间,推荐去看《深入理解Java虚拟机》。GC一共分三种:MinorGC,
Major GC v和Full GC
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。
执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。
质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。
所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。
Major GC vs Full GC
大家应该注意到,目前,这些术语无论是在 JVM 规范还是在垃圾收集研究论文中都没有正式的定义。但是我们一看就知道这些在我们已经知道的基础之上做出的定义是正确的,Minor GC 清理年轻带内存应该被设计得简单:
Major GC 是清理永久代。
Full GC 是清理整个堆空间—包括年轻代和永久代。
很不幸,实际上它还有点复杂且令人困惑。首先,许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。另一方面,许多现代垃圾收集机制会清理部分永久代空间,所以使用“cleaning”一词只是部分正确。

1.1 gc发生在何处: 堆 和 方法区

img

堆分为: 1. 老年代 : 存放大对象(很多连续的内存)和长期存活的对象,

​ 2. 新生代( 又分为三个区 ) : 1. Eden区 ( 内存较大 ) 每次使用都是一个Eden 和 其中一个 Survivor

​ 2. From Survivor区

​ 3. To Survivor区

方法区: 永久代: 对象一般不会被回收 ,永久代的垃圾收集主要回收废弃常量和无用类


Eden区内存不够时: 虚拟机将发起一次Minor GC

老年代内存不够时: Major GC/Full GC


String常量abc为例,当我们声明了此常量,那么它就会被放到运行时常量池中,如果在常量池中没有任何对象对abc进行引用,那么abc这个常量就算是废弃常量而被回收;

Java虚拟机在进行垃圾回收时,将Eden和Survivor中还存活着的对象进行一次性地复制到另一块Survivor空间上,直到其两个区域中对象被回收完成, 当Survivor空间不够用时,需要依赖其他老年代的内存进行分配担保。 如果老年代内存也满了, 会抛OutOfMemory异常

1.2 判断一个类是否“无用”

​ (1)、该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;

​ (2)、加载该类的ClassLoader已经被回收

​ (3)、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

1.3 年龄计数器

  1. 虚拟机通过一个对象年龄计数器来判定哪些对象放在新生代,哪些对象应该放在老生代。

  2. 对象每在Survivor中熬过一次Minor GC,年龄就增加1岁,当他的年龄增加到最大值15时,就将会被晋升到老年代中。

  3. 如果在Survivor空间中所有相同年龄的对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。


1.4 GC实现机制-如何实现垃圾回收

1 引用计数器算法

2 可达性分析算法

  1. 引用计数算法(Reference Counting): 给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,这就是引用计数算法的核心。

    优点: 引用计数算法实现简单,判定效率也很高

    缺点: 它很难解决对象之间相互循环引用的问题

  2. 可达性分析算法(Reachability Analysis): **这是Java虚拟机采用的判定对象是否存活的算法。**通过一系列的称为“GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。本地方法栈JNI引用的对象。

    可达性分析更详细的步骤 :

    1. 在分析时,需保证这个对象引用关系不再变化,否则结果将不准确。因此GC进行时需停掉其它所有java执行线程(Sun把这种行为称为‘Stop the World’),即使是号称几乎不会停顿的CMS收集器,枚举根节点时也需停掉线程

    2. 系统停下来后JVM不需要一个个检查引用,而是通过OopMap数据结构【HotSpot的叫法】来标记对象引用。

    3. 虚拟机先得知哪些地方存放对象的引用,在类加载完时。HotSpot把对象内什么偏移量什么类型的数据算出来,在jit编译过程中,也会在特定位置记录下栈和寄存器哪些位置是引用,这样GC在扫描时就可以知道这些信息。【目前主流JVM使用准确式GC】

    4. OopMap可以帮助HotSpot快速且准确完成GC Roots枚举以及确定相关信息。但是也存在一个问题,可能导致引用关系变化。

    5. 这个时候有个 safepoint(安全点)的概念:

      1. HotSpot中GC不是在任意位置都可以进入,而只能在safepoint处进入。 GC时对一个Java线程来说,它要么处在safepoint,要么不在safepoint。

      2. safepoint不能太少,否则GC等待的时间会很久

      3. safepoint不能太多,否则将增加运行GC的负担

        安全点主要存放的位置:

        1. 1:循环的末尾 
          2:方法临返回前/调用方法的call指令后 
          3:可能抛异常的位置
          

img在上图可以看到GC Roots左边的对象都有引用链相关联,所以他们不是死亡对象,而在GCRoots右边有几个零散的对象没有引用链相关联,所以他们就会别Java虚拟机判定为死亡对象而被回收。

扩展 : 哪些对象可以为GC Roots ?

①虚拟机栈(栈桢中的本地变量表)中的引用的对象。
②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
③方法区中的常量引用的对象
④本地方法栈中JNI(native方法)引用的对象

即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下。网上例子很多,基本上和深入理解JVM一书讲的一样对象的生存还是死亡:

要真正宣告对象死亡需经过两个过程。
1.可达性分析后没有发现引用链
2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]

1.5 何为死亡对象?

java虚拟机在进行死亡对象判定时,会经历两个过程

  1. 一次标记 将对象放入F-Queue队列( 低优先级的队列 ) 如果在 finalize 方法中该对象重新与引用链上的任何一个对象建立了关联 , 即该对象连上了任何一个对象的引用链,例如this关键字,那么该对象就会逃脱垃圾回收系统
  2. 两次标记 : 如果该对象在finalize方法中没有与任何一个对象进行关联操作,那么该对象会被虚拟机进行第二次标记,该对象就会被垃圾回收系统回收。值得注意的是finaliza方法JVM系统只会自动调用一次,如果对象面临下一次回收,它的finalize方法不会被再次执行。

1.5 清除垃圾算法

  1. 经典的垃圾回收算法以下几种

    一、标记–清除算法(Mark-Sweep)

在这里插入图片描述

优缺点:

算法执行分为两个阶段标记与清除,所有的回收算法,基本都基于标记回收算法做了深度优化

缺点效率问题,内存空间碎片(不连续的空间),碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

二、复制算法(Copying)

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

img

优缺点:

比较标记清除算法,避免了回收造成的内存碎片问题,

缺点: 这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。以局部的内存空间牺牲为代价,不过空间的浪费比较小,默认8:1的比例1是浪费的。

复制也有一定的效率与空间成本,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。


三、标记整理算法(Mark-Compact)

在这里插入图片描述

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

优缺点:

避免了,空间的浪费,与内存碎片问题。

**缺点:**整理时复制有效率成本。

4.Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

  而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

  注意,在堆区之外还有一个代就是永久代(Permanet Generation),它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

1.6 GC过程中的内存担保机制

内存不足 老生代提供内存 像银行贷款似的

1.7 垃圾收集器

年轻代收集器
Serial、ParNew、Parallel Scavenge
老年代收集器
Serial Old、Parallel Old、CMS收集器
特殊收集器
G1收集器[新型,不在年轻、老年代范畴内] --- JDK1.7发布的商用垃圾收集器

img收集器,连线代表可结合使用

典型的垃圾收集器

新生代收集器

  1. Serial/Serial Old

Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。

优点: 实现简单高效

缺点: 会给用户带来停顿

  1. ParNew

    ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

  2. Parallel Scavenge

    Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量

    吞吐量 :

    该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】

    老年代收集器:

  3. Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

  1. CMS

    CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】

    启用CMS:-XX:+UseConcMarkSweepGC

    正如其名,CMS采用的是"标记-清除"(Mark Sweep)算法,而且是支持并发(Concurrent)的

    它的运作分为4个阶段:

    1.初始标记:标记一下GC Roots能直接关联到的对象,速度很快
    2.并发标记:GC Roots Tarcing过程,即可达性分析
    3.重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记
    4.并发清除
    

    以上初始标记和重新标记需要stw(停掉其它运行java线程)

    之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。

    总体上CMS是款优秀的收集器,但是它也有些缺点。

1.cms堆cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。

2.cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC

3.由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)

总结 : 1. CPU敏感 2. 无法处理浮动垃圾 3.可能有垃圾碎片

浮动垃圾:由于cms支持运行的时候用户线程也在运行,程序运行的时候会产生新的垃圾,这里产生的垃圾就是浮动垃圾,cms无法当次处理,得等下次才可以。

g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。

  1. G1

    G1( garbage first:尽可能多收垃圾,避免full gc )收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

    同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。

    摘自甲骨文:适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。G1 GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先)

    g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。

    用到的算法为标记-清理、复制算法

g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。

g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。

g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。

17.1 Minor GC、Major GC、FULL GC、mixed gc
17.1.1 Minor GC

在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代.

17.1.2 Major GC

Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC。

17.1.3 Full GC

full gc是对新生代、老年代、永久代【jdk1.8后没有这个概念了】统一的回收。

【知乎R大的回答:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)、元空间(1.8及以上)等所有部分的模式】

17.1.4 mixed GC【g1特有】

混合GC

收集整个young gen以及部分old gen的GC。只有G1有这个模式

17.2 扩展点
1. GC是怎么判断对象是被标记的

通过枚举根节点的方式,通过jvm提供的一种oopMap的数据结构,简单来说就是不要再通过去遍历内存里的东西,而是通过OOPMap的数据结构去记录该记录的信息,比如说它可以不用去遍历整个栈,而是扫描栈上面引用的信息并记录下来。

总结 : 通过OOPMap把栈上代表引用的位置全部记录下来,避免全栈扫描,加快枚举根节点的速度,除此之外还有一个极为重要的作用,可以帮HotSpot实现准确式GC【这边的准确关键就是类型,可以根据给定位置的某块数据知道它的准确类型,HotSpot是通过oopMap外部记录下这些信息,存成映射表一样的东西】。

2. 什么时候触发GC

简单来说,触发的条件就是GC算法区域满了或将满了。

minor GC(young GC):当年轻代中eden区分配满的时候触发[值得一提的是因为young GC后部分存活的对象会已到老年代(比如对象熬过15),所以过后old gen的占用量通常会变高]

full GC:
①手动调用System.gc()方法 [增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc]
②发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
③老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。
④CMS GC时出现Promotion Faield[pf]
⑤统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。
这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FUll GC。
比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC(这点挺智能的)
3. cms收集器是否会扫描年轻代

会,在初始标记的时候会扫描新生代。

虽然cms是老年代收集器,但是我们知道年轻代的对象是可以晋升为老年代的,为了空间分配担保,还是有必要去扫描年轻代。

4. 什么是空间分配担保

在minor gc前,jvm会先检查老年代最大可用空间是否大于新生代所有对象总空间,如果是的话,则minor gc可以确保是安全的

如果担保失败,会检查一个配置(HandlePromotionFailire),即是否允许担保失败。

如果允许 : 继续检查老年代最大可用可用的连续空间是否大于之前晋升的平均大小,比如说剩10m,之前每次都有9m左右的新生代到老年代,那么将尝试一次minor gc(大于的情况),这会比较冒险。

如果不允许,而且还小于的情况,则会触发full gc。【为了避免经常full GC 该参数建议打开】

这边为什么说是冒险是因为minor gc过后如果出现大对象,由于新生代采用复制算法,survivor无法容纳将跑到老年代,所以才会去计算之前的平均值作为一种担保的条件与老年代剩余空间比较,这就是分配担保。

这种担保是动态概率的手段,但是也有可能出现之前平均都比较低,突然有一次minor gc对象变得很多远高于以往的平均值,这个时候就会导致担保失败【Handle Promotion Failure】,这就只好再失败后再触发一次FULL GC,

5.为什么复制算法要分两个Survivor,而不直接移到老年代

这样做的话效率可能会更高,但是old区一般都是熬过多次可达性分析算法过后的存活的对象,要求比较苛刻且空间有限,而不能直接移过去,这将导致一系列问题(比如老年代容易被撑爆)

分两个Survivor(from/to),自然是为了保证复制算法运行以提高效率

6. 常用回收器参数设置:

img

7. stop the world具体是什么,有没有办法避免

stop the world简单来说就是gc的时候,停掉除gc外的java线程

无论什么gc都难以避免停顿,即使是g1也会在初始标记阶段发生,stw并不可怕,可以尽可能的减少停顿时间。

8. 新生代什么样的情况会晋升为老年代

对象优先分配在eden区,eden区满时会触发一次minor GC

对象晋升规则
1 长期存活的对象进入老年代,对象每熬过一次GC年龄+1(默认年龄阈值15,可配置)。
2 对象太大新生代无法容纳则会分配到老年代
3 eden区满了,进行minor gc后,eden和一个survivor区仍然存活的对象无法放到(to survivor区)则会通过分配担保机制放到老年代,这种情况一般是minor gc后新生代存活的对象太多。
4 动态年龄判定,为了使内存分配更灵活,jvm不一定要求对象年龄达到MaxTenuringThreshold(15)才晋升为老年代,若survior区相同年龄对象总大小大于survior区空间的一半,则大于等于这个年龄的对象将会在minor gc时移到老年代

9. 怎么理解g1,适用于什么场景

G1 GC 是区域化、并行-并发、增量式垃圾回收器,相比其他 HotSpot 垃圾回收器,可提供更多可预测的暂停。增量的特性使 G1 GC 适用于更大的堆,在最坏的情况下仍能提供不错的响应。G1 GC 的自适应特性使 JVM 命令行只需要软实时暂停时间目标的最大值以及 Java 堆大小的最大值和最小值,即可开始工作。

g1不再区分老年代、年轻代这样的内存空间,这是较以往收集器很大的差异,所有的内存空间就是一块划分为不同子区域,每个区域大小为1m-32m,最多支持的内存为64g左右,且由于它为了的特性适用于大内存机器。

img

g1回收时堆内存情况

适用场景:

  1. 像cms能与应用程序并发执行,GC停顿短【短而且可控】,用户体验好的场景。
  2. 面向服务端,大内存,高cpu的应用机器。【网上说差不多是6g或更大】
  3. 应用在运行过程中经常会产生大量内存碎片,需要压缩空间【比cms好的地方之一,g1具备压缩功能】。

下面补充一下关于内存分配方面的东西:

img

对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的Eden Space和From Space,少数情况下会直接分配在老年代。如果新生代的Eden Space和From Space的空间不足,则会发起一次GC,如果进行了GC之后,Eden Space和From Space能够容纳该对象就放在Eden Space和From Space。在GC的过程中,会将Eden Space和From Space中的存活对象移动到To Space,然后将Eden Space和From Space进行清理。如果在清理的过程中,To Space无法足够来存储某个对象,就会将该对象移动到老年代中。在进行了GC之后,使用的便是Eden space和To Space了,下次GC时会将存活对象复制到From Space,如此反复循环。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。

一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:

byte[] data = new byte[4*1024*1024]

这种一般会直接在老年代分配存储空间。

当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。

参考资料:

《深入理解Java虚拟机》

1.8 Eden/From/To区的步骤

Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。

GC开始时,对象只会存于Eden和From Survivor区域,To Survivor【保留空间】为空。

GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄战4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor。

链接:https://www.jianshu.com/p/76959115d486
来源:简书


2. 类加载的过程

3. 内核态和用户态

内核态: CPU可以访问内存所有数据, 包括外围设备, 例如硬盘, 网卡. CPU也可以将自己从一个程序切换到另一个程序

用户态: 只能受限的访问内存, 且不允许访问外围设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获取

由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 :用户态内核态

用户态切换成内核态的三个方式:

  1. 系统调用: 用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使 用操作系统提供的服务程序完成工作,比如print()实际上就是执行了一个输出的系统调用
  2. 异常: 发生异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序
  3. 外围设备的中断: 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会 暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到 内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

4. 永久代 和 元空间

jdk8以后,使用原空间替代了永久代

  1. 内存 : 元空间用的是本地内存, 永久代用的是jvm内存

元空间优势:

解决了"java.lang.OutOfMemoryError: PermGen space "异常的出现

  1. 解决了空间不足的问题( 老年代中的字符串或者对象所占内存无法确定, 元空间内存取决于本地内存 )

5.逃逸分析

逃逸?

当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。

例子:

public StringBuilder escapeDemo1(String a, String b) {
	StringBuilder stringBuilder = new StringBuilder();
      stringBuilder.append(a);
      stringBuilder.append(b);
      return stringBuilder;
}

如果改成 :

public String escapeDemo2(String a, String b) {
     StringBuilder stringBuilder = new StringBuilder();
     stringBuilder.append(a);
     stringBuilder.append(b);
     return stringBuilder.toString();
}

就不会发生逃逸


什么是逃逸分析

逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 逃逸分析(Escape Analysis)算是目前Java虚拟机中比较前沿的优化技术了。

逃逸分析的原理

Java本身的限制(对象只能分配到堆中),为了减少临时对象在堆内分配的数量,我会在一个方法体内定义一个局部变量,并且该变量在方法执行过程中未发生逃逸,按照JVM调优机制,

  1. 优化前 : 首先会在堆内存创建类的实例,然后将此对象的引用压入调用栈,继续执行
  2. 优化后 : 针对栈的重新分配方式,首先找出未逃逸的变量,将该变量直接存到栈里,无需进入堆,分配完成后,继续调用栈内执行,最后线程执行结束,栈空间被回收,局部变量也被回收了。如此操作,是优化前在堆中,优化后在栈中,从而减少了堆中对象的分配和销毁,从而优化性能。

逃逸的方式

  1. **方法逃逸:**在一个方法体内,定义一个局部变量,而它可能被外部方法引用,比如作为调用参数传递给方法,或作为对象直接返回。或者,可以理解成对象跳出了方法。
  2. **线程逃逸:**这个对象被其他线程访问到,比如赋值给了实例变量,并被其他线程访问到了。对象逃出了当前线程。

逃逸分析的好处

如果一个对象不会在方法体内,或线程内发生逃逸(或者说是通过逃逸分析后,使其未能发生逃逸)

  1. 栈上分配 : 一般情况下,不会逃逸的对象所占空间比较大,如果能使用栈上的空间,那么大量的对象将随方法的结束而销毁,减轻了GC压力

  2. 同步消除 : 如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

  3. 标量替换 :

    Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。

    如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。


参数开启 :

在JDK 6u23以上是默认开启,这里将设置重新明确一下:

​ 强制开启:-server -XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m

​ 关闭逃逸分析:  -server -XX:-DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m

6. JVM对并发分配内存的处理方式 ( CAS+失败重试 & TLAB )

TLAB使用前所会出现的一些问题 :

假设JVM虚拟机上,堆内存都是规整的。堆内存被一个指针一分为二。指针的左边都被塞满了对象,指针的右变是未使用的区域。每一次有新的对象创建,指针就会向右移动一个对象size的距离。这就被称为指针碰撞

img

问题来了。如果我们用多线程执行刚才的demo方法 : 一**个线程正在给A对象分配内存,指针还没有来的及修改,同时为B对象分配内存的线程,仍引用这之前的指针指向。这样就出现毛病了。


TLAB ( Thread local Allocation Buffer ) 本地线程分配缓存 , 这是一个线程专用的内存分配区域。

虚拟机参数 -XX:UseTLAB

在线程初始化时,同时也会申请一块指定大小的内存,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提升分配效率

TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小

TLAB的本质 : 其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。

**TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。**从这一点看,它被翻译为 线程私有分配区更为合理一点
当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。

TLAB的缺点 :

  1. TLAB通常很小,所以放不下大对象。

    1. TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)
    2. TLAB空间还剩一点点没有用到,有点舍不得。(比如100kb的TLAB,装了80KB,又来了个30KB的对象)
      所以JVM开发人员做了以下处理,设置了最大浪费空间。
      • 当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间。进行对象创建,还是空间不够,那你这个对象太大了,去Eden区直接创建吧!
      • 当剩余的空间大于最大浪费空间,那这个大对象请你直接去Eden区创建,我TLAB放不下没有使用完的空间。
  2. Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC,

  3. TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理。

7. 创建对象的内存分配

img

图中满满的干货:

  1. 何时会触发YGC以及FGC?
  2. YGC的流程?
  3. 遇到大对象,内存分配过程是什么?
  4. ….
  1. 编译器通过逃逸分析判断对象是在栈上分配还是堆上分配,如果是堆上分配则进入下一步。(开启逃逸分析需要设置jvm参数)
  2. 如果tlab可以放下该对象则在tlab上分配,否则进入下一步。
  3. 重新申请一个tlab,再尝试存放该对象,如果放不下则进入下一步。
  4. 在eden区加锁,尝试在eden区存放,若存放不下则进入下一步。
  5. 执行一次Young GC。
  6. Young GC后若eden区仍放不下该对象,则直接在老年代分配。

是否应该完全避免 Full GC?

如果用辩证的观点来看问题,这句话可以是对的,也可以是不对的。

1)为什么这句话是对的?

从1960年第一Full GC算法被提出到现在,full gc已经发展了接近60年的时间,但是full gc的算法还是没有得到数量级似的性能提升。Full GC还是至少需要扫描整个堆两次才能完成full gc的回收,然而在如今堆区越来越来的情况下,这种多遍扫描确实非常耗时,而且Full GC是一个STW的过程,如果暂停时间太长,对于一些交互式的应用的影响不言而喻。所以我们应该尽可能的避免Full GC的发生。

2)那为什么这句话是错的?

首先,Full GC是堆满之后,虚拟机的正常反应,难道堆区满了,我们还不发生gc,等着OOM么?其次,偶尔发生一次Full GC,还是能提升内存的利用率的,毕竟程序在长时间运行后,即使是老年代也有一些死对象存在,而这些死对象如果不回收的话,它们占用的内存就无法被重新利用。

如今已经出现了CMS-GC,G1等GC算法可以用来替代传统的full gc,在STW时间尽可能低的情况下,提升内存的利用率。

干货:

  1. 何时会触发YGC以及FGC?
  2. YGC的流程?
  3. 遇到大对象,内存分配过程是什么?
  4. ….
  1. 编译器通过逃逸分析判断对象是在栈上分配还是堆上分配,如果是堆上分配则进入下一步。(开启逃逸分析需要设置jvm参数)
  2. 如果tlab可以放下该对象则在tlab上分配,否则进入下一步。
  3. 重新申请一个tlab,再尝试存放该对象,如果放不下则进入下一步。
  4. 在eden区加锁,尝试在eden区存放,若存放不下则进入下一步。
  5. 执行一次Young GC。
  6. Young GC后若eden区仍放不下该对象,则直接在老年代分配。

是否应该完全避免 Full GC?

如果用辩证的观点来看问题,这句话可以是对的,也可以是不对的。

1)为什么这句话是对的?

从1960年第一Full GC算法被提出到现在,full gc已经发展了接近60年的时间,但是full gc的算法还是没有得到数量级似的性能提升。Full GC还是至少需要扫描整个堆两次才能完成full gc的回收,然而在如今堆区越来越来的情况下,这种多遍扫描确实非常耗时,而且Full GC是一个STW的过程,如果暂停时间太长,对于一些交互式的应用的影响不言而喻。所以我们应该尽可能的避免Full GC的发生。

2)那为什么这句话是错的?

首先,Full GC是堆满之后,虚拟机的正常反应,难道堆区满了,我们还不发生gc,等着OOM么?其次,偶尔发生一次Full GC,还是能提升内存的利用率的,毕竟程序在长时间运行后,即使是老年代也有一些死对象存在,而这些死对象如果不回收的话,它们占用的内存就无法被重新利用。

如今已经出现了CMS-GC,G1等GC算法可以用来替代传统的full gc,在STW时间尽可能低的情况下,提升内存的利用率。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值