欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Garbage First

程序员文章站 2022-05-09 16:17:10
...

1 G1 的基本概念

1 : HotSpot:现有的垃圾回收器: Serial GC, Parallel GC ,Concurrent Mark Sweep Gc
这三个GC不同:

1:如果你想要最小化的使用内存和并行开销:选Serial GC
2:如果你想要最大化应用程序的吞吐量选用Parallel GC
3:如果想要最小化GC的中断或停顿时间选CMS GC

2 G1是Garbage First, 意思: G1是一个并行回收器,它把内存分割为很多不相关的区间(Region),每一个区间可以属于老年代或年轻代,并且每个区间上是可以物理上不连续的,老年代区间这个设计理念本身是为了服务于并行后台线程,这些线程的主要工作是寻找未被引用的对象,而这样就会产生一种现象,即某些区间的垃圾多余其它区间,(垃圾回收时都是需要停下应用程序,不然没办法防止应用程序的干扰),然后G1可以集中精力在垃圾最多的区间上,并且只需要一点点时间就可以清空这些区间垃圾,腾出完全空闲的时间.总而言之:G1侧重点在于处理垃圾最多的区间,所以叫Garbege First

3 : G1内部主要有四个操作阶段:

1:年轻代回收 (A young Collection)
2: 运行在后台的并行循环(A background Concurrent Cycle)
3: 混合回收(A mixed Collection)
4: 全量回收 (A full GC)

2 进程和线程

1: 子进程和父进程有不同的代码和数据空间,而多个线程共享数据空间,每个线程都有自己的执行堆栈和程序计数器为其执行上下文

3 ava###1 G1 的基本概念

1 : HotSpot:现有的垃圾回收器: Serial GC, Parallel GC ,Concurrent Mark Sweep Gc
这三个GC不同:

1:如果你想要最小化的使用内存和并行开销:选Serial GC
2:如果你想要最大化应用程序的吞吐量选用Parallel GC
3:如果想要最小化GC的中断或停顿时间选CMS GC

*2 G1是Garbage First, 意思: G1是一个并行回收器,它把内存分割为很多不相关的区间(Region),每一个区间可以属于老年代或年轻代,并且每个区间上是可以物理上不连续的,老年代区间这个设计理念本身是为了服务于并行后台线程,这些线程的主要工作是寻找未被引用的对象,而这样就会产生一种现象,即某些区间的垃圾多余其它区间,(垃圾回收时都是需要停下应用程序,不然没办法防止应用程序的干扰),然后G1可以集中精力在垃圾最多的区间上,并且只需要一点点时间就可以清空这些区间垃圾,腾出完全空闲的时间.总而言之:G1侧重点在于处理垃圾最多的区间,所以叫Garbege First
*3 : G1内部主要有四个操作阶段:

1:年轻代回收 (A young Collection)
2: 运行在后台的并行循环(A background Concurrent Cycle)
3: 混合回收(A mixed Collection)
4: 全量回收 (A full GC)

2 进程和线程

1: 子进程和父进程有不同的代码和数据空间,而多个线程共享数据空间,每个线程都有自己的执行堆栈和程序计数器为其执行上下文

3 java 里面的4中引用类型

1: 强引用 在一个线程内,无需直接引用直接可以使用的对象,除非引用不存在了,否则不会被GC清理了, ==操作用于表示两个操作数所指向的堆地址空间地址是否相同,不表示两个操作数所指向的对象是否相等
2: 软用于,JVM抛出OOM之前,GC会清理所有的软引用对象,垃圾回收器在莫个时刻决定回收软可达的对象的时候,会清理软引用,并可选的把引用放到一个引用队列(Reference Queue),类似弱引用,只不过java虚拟机会尽量让软引用存活时间长一点,破不得一才清理.
3弱引用 (Weak Rederence) 弱引用和软引用的最大不同在于:当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收.弱引用对象更容易,更快被GC回收.GC在运行时一定回收弱引用对象,但是关系复杂的弱引用对象群常常需要好几次GC的运行才能完成
4虚引用(Phantom Reference): 主要目的是在一个对象所占用的内存被实际回收之前得到通知,从而进行一些相关清理工作,虚引用在创建时必须提供一个引用队列作为参数,其次虚引用对象的get方法总是返回null,因此无法通过虚引用来获得被引用的对象
5:finalization机制: finalize方法和c++中的析构函数类似,但是java采用的是基于垃圾回收器的自动内存管理机制,所以finalize方法在本质上不同于c++的析构函数,当垃圾回收器发现没有引用指向一个对象时,会调用这个对象的finalize方法,通常这个方法会进行一些资源释放和清理的工作,比如关闭文件,套接字或数据库链接

由于finalize方法存在,虚拟机中的对象一般处于三种可能的状态 (1)可达到状态(2)可复活状态(腹泻了finalize方法)(3)不可到达状态,垃圾回收器*释放对象所占用内存
finalize()只会被JVM自动调用一次,也就是只有一次拯救自己的机会

4并行

在Hotpot垃圾搜集器里除了G1之外,其他的垃圾收集器使用的是内置的JVM 线程执行GC的多线程操作,而G1 GC 采用的是应用线程承担后台运行的GC工作,即当VM线程线程处理速度慢时,系统会调用应用线程帮助加速垃圾回收过程.
并发在JVM里面是指垃圾收集线程和用户线程同时执行,但不一定是并行执行,可能是交叉执行,用户线程继续运行,而垃圾手机线程运行在另一块CPU上.

5 分代

1: heap上的分代: Eden survivor spaces virtual1(称Young) --> ... Virtual1(Tenured) --> Perm(存放Class and Meta) (Young代MinorGC,每执行一次,对象年龄+1, (Tenured区叫Full GC) 注意GC不会在主程序运行期对PernGen Space进行清理,如果应用程序会加载很多类的时候,就可能出现PermGen Space 错误,例如加入的第三方包太多导致
2: jdk7的更新过程中,永久区还是存在并使用的,只不多已经开始从他内部移除数据了,大致一共移除三类数据:
(1) 例如&APPLID这样的符号(Symbols)被移到了本地堆区
(2)内部字符串被移到了java堆区
(3)类的静态属性被移到了Java堆区
如果你使用jdk7而使用G1 GC ,那么永久区只有在Full GC阶段才会被回收,G1只会在永久区满了之后才会调用Full GC事件,或者在应用程序的生产速度比G1的垃圾回收速度块时调用
JDK8 JVM 开始使用本地化的内存空间来存放元数据,这个空间叫做元空间(Meta Space)这样修改就意味着OOM:PermGen的问题不存在了,并且不需要调整和监控这个空间,JDK8完成了对永久区的移除

6 Full Gc的条件(除了System.gc)

1: 老年代空间不足,当执行Full GC之后任然不足的会抛出如下 OOME:Java heap space
2:永久代空间满,永久代中存放一些类的信息,当系统中要加载的类,反射的类和调用的方法较多时,永久代可能会占满,执行Full GC之后任然回收不了会抛出OOME:PermGen space
3:CMS GC时出现Promotion Failed and Concurrent Mode Failure

尤其要注意GC日志中有这两种情况很可能会出现Full GC,PromotionFailed是在进行MinorGC时,SurvivorSpace放不下了,对象只能放入老年代,而此时老年代也放不下造成的. Concurrent Mode Failure 是在执行CMS GC的过程中,同时有对象要放入老年代,而此时老年代空间不足造成的
应对措施:增大Survivor Space,老年代空间或调低并发GC的比率

4:统计得到的MinorGC晋升到老年代的平均大小大于老年代的剩余空间

7 java中GC Roots包括以下内容:

1: 虚拟机栈中引用的对像
2:方法区中类静态属性实体引用的对象
3:方法区中常量引用对象
4:本地方法栈内JNI引用的对象

8 GC 参数

1 G1之前: -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails
2: G1 : -XX:+UseSerialGC -Xlog:gc:gc.log -Xlog:gc*

9 堆外内存(off-heap memory)

JVM内部会把内存分为Java使用的堆内存和native使用的内存,他们之间是不能共享的,就是说当native内存用完了时,如果Java内存堆有空闲,这是native回向jvm申请而不是直接使用Java堆内存
线程栈,应用程序代码,NIO缓存用的都是堆外内存,事实上,c/c++中,使用的也是未托管内存,在Java中使用托管内存或者堆内存是这门语言的一个特性.
使用堆外内存对象池都能减少GC的暂停时间.
堆外内存的好处:

1: 可以扩展更大的内存空间,比如超过1TB甚至比主内存还大的空间
2: 理论上可以减少GC暂停时间
3:可以在进程间共享,减少JVM间对象复制,使得JVM的分割部署更容易实现
4:持久化存储可以支持快速重启,同时还能够在测试环境中重现生产数据

堆外内存的缺点:

1:数据结构变得有些扭曲
2:可能需要一个简单的数据结构以便于直接映射到堆外内存
3:使用复杂的数据结构并序列化和反序列化到内存中
4:序列化比使用对象的性能差很多

与其他方案比较,比如堆缓存,消息队列,使用堆外内存更简单,高效. 堆外内存可以支持操作系统的同步写入,不需要异步去执行,但可能丢失数据,堆外内存的好处是能够缩短映射时间,映射1TB的数据只需要10ms

10 不推荐使用finalize释放资源:

1:在 finalize()时可能导致对象复活
2:finalize()函数执行是没有时间保证的,她完全由GC线程决定,极端情况下,若不发生GC,则finalize()函数不执行
3:一个糟糕的finalize()会严重影响GC的性能

函数finalize() 是由FinalizerThread线程处理的,方法的对象都会在被正式回收前加入FinalizerThread的执行队列,该队列由链表实现...

11 G1涉及术语

MetaSpace jdk8 HotSpot JVM 使用本地内存来存储类元数据信息并称为元空间(java8移除了永久区的概念,转而使用元空间)
默认情况下,大部分类元数据都在本地内存中分配,类元数据只受可用的本地内存限制,并动态调整,新参数(MaxMetaspaceSize用来限制元数据的空间大小)
-XX:MetaspaceSize 初始化元空间大小(默认12M(32位JVM) ,16M(64位JVM))
-XX:MaxMetaspaceSize 最大元空间大小(默认本地内存)
-XX:MinMetaspaceFreeRatio 扩大空间的最小比例,当GC过后,内存占用超过这一比例,就会扩大空间.
-XX:MaxMetaspaceFreeRatio: 缩小空间的最小比例,当GC后,内存占用低于这一比例,就会缩小空间.
Reclaimable G1 GC 为了能够回收,创建了一系列专门用于存放可回收对象的Region,这些Region都在一个链表队列里面.这个队列只包含存活率小于 -XX:G1MixedGCLiveThresholdPercent(默认85%)的Region, Region的值除以整个Java堆区,如果大于-XX:G1HeapWastePercent(默认5%)则启动回收机制
RSet 全称是 RememberdSet 简称是RSet: 指跟踪指向某个堆区(Region)内的对象引用,在标记存货对象时,G1使用RememberSet的概念,将每个分区外指向分区内的引用记录在该分区的RememberSet中,避免了对整个heap的扫描,使得各个分区更加独立.堆内存中每个分区都有一个RSet,Rset的作用是让堆区能并行独立的进行垃圾集合.Rset所占用的JVM内存小于总大小的5%
CSet Collection Set 简称CSet即收集集合,保存一次GC中将执行垃圾回收的区间(Region),GC时在CSet中的所有存活数据(Live Data) 都会被转移(复制/移动).集合中的堆区可以是Eden Survivor或Old Generation. CSet所占用的JVM内存小于总大小的1%.
可以看出: CSet相当于一个大圈,里面包含了很多小圈(Rset).这些圈圈都是需要被回收的信息,这样可以把CSet比作垃圾场,Rset是垃圾场里面一个个绿色可回收垃圾桶.
G1 Pause Time Target: G1停顿目标时间,由于垃圾收集阶段可能是独占试的,就会引起应用程序的停顿,这个停顿时间就是你所设置的期望时间.G1使用一个停顿预测模型去匹配用户设定的目标停顿时间,并且基于这个目标停顿时间去选择需要回收的Region的数量
Root Region Scan 这个阶段从根区间的扫描开始,标记所有可达的存活对象.由于在并行标记的执行过程中移动数据会造成应用程序的暂停.所有根区间扫描这个阶段需要在下一次评估中断开始执行直到结束.
PLAB (Promotion Local Allocation Buffers) 它被用于年轻代回收.PLAB的`作用是避免多线程竞争相同的数据.处理方式是每个线程拥有独立的PLAB,用于针对幸存者和老年空间.当应用开启的线程较多时,最好使用 -XX:-ResizePLAB来关闭PLAB()的大小调整,以避免大量的线程通信导致的性能下降.
TLAB (Thread Local Allocation Buffers) 即线程本地分配缓存,是一个线程专用的内存分配区域. 总的来说TLAB是为了加速对象分配而生的,由于对象一般会分配在堆上,而堆是全局共享的.因此在同一时间,可能会有多个线程在堆上申请空间.JVM使用TLAB这种线程专属的区间来避免多线程冲突,提高对象分配效率,TLAB本身占用了eden区的空间,即JVM会为每一个Java线程分配一块TLAB空间.对于G1来说,TLAB是Eden的一个Region,被一个单一线程用于分配资源.主要用途是让一个线程通过栈操作方式独享内存空间,用于对象分配.这样比多个线程之间共享资源要快得多.

Lock-free Manner: TLAB调用本地线程的无锁竞争分配方式.
对于GC来说,堆内存的压缩和栈检索通常是引起性能问题的瓶颈爆发点,基于锁的机制扩展能力不好,所以采用CAS/MCAS同步方式可以确保不会被其他线程阻塞.
Region G1收集器将堆进行分区,划分一个个区域,每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的停顿时间,这就是Region
从字面来说Region表示一个区域,每个区域里面的字母代表不同的分代内存空间类型,如:[E] Eden [O] Old [S] survivor ,空白的区域不属于任何一个分区,G1 可以在需要的时候任意指定这个区域属于Eden或是O区之类的
G1通过将内存空间分成区域(Region)的方式避免内存碎片问题,每个Region是大小一致的,从逻辑层面来说,他们又是连续的虚拟内存块,G1的并行全局标记阶段会决定整个堆区的存活对象,在这个标记阶段完成之后,G1就知道哪些Region是空的了.
Ergonomics Heuristic Decision 字面意思是人体工程学,可以理解为适合人类理解的行为或习惯
Evacuation Failure 可以理解为提升失败(Promotion Failre) ,to-space exhaustion to-space overflow这个异常通常发生在提升对象时发现内存空间不足.
Top-at-mark-start 每个区域都记录这两个TAMS指针(top-at-mark-start),分别为 prevTAMS,nextTAMS,在TAMS以上的对象时新分配的,因而被视为隐式标记.

12 Java虚拟机内存模型

1 jvm将内存数据分为5个部分: 程序计数器 虚拟机栈 本地方法栈 Java堆 方法区简单来说: 程序计数器用于存放下一条运行时指令,虚拟机栈和本地方法栈用于存放函数调用堆栈信息,Java堆用于存放Java运行时所需要的对象等,方法区用于存放程序的类元数据信息

线程共享区域(堆内存区,方法区,运行时常量池)
-XSS设置虚拟机栈的大小,栈里面存放栈帧(Stack Frame)栈帧里面存放:局部变量表,操作数栈,动态链接方法和返回地址等信息
栈帧由3部组成: :局部变量区(Local Variable) ,操作数栈(Operand Stack) ,帧数据区(Frame Data)

Escape Analysis(逃逸分析):指分析指针动态范围的方法,他与编译器优化原理的指针分析和外形分析相关联,通俗点:如果一个对象的指针被多个方法或线程引用时,那我们可以称这个指针发生了逃逸.一般出现在三种场景:全局变量赋值 方法返回值 实例引用传递

static class B{
        public void printClassName(G g){
            System.out.println(g.getClass().getName());
        }
    }
    static class G{
        public static B b;
        public void globalVarPointEscape(){
             b = new B();//给全局变量赋值,发生逃逸
        }
        public B methodPoint(){
            return new B();//方法返回值,发生逃逸
        }
        public void instancePassPointer(){
            methodPoint().printClassName(this);//实例引用发生逃逸
        }
    }

java里面缺少像C#里面的值对象或者C++里面的struct,栈里面只保存了对象指针,当对象不再使用后需要依靠GC来遍历引用树并收回内存,如果对象较多,会影响性能
*Java7开始支持对象的栈分配和逃逸分析机制,此外,逃逸分析还有其他2个优化应用,同步消除 矢量代替 ,同步消除:逃逸分析可以判断出某个对象是否只被一个线程访问,如果只被一个线程访问.那么该对象的同步操作会转化为没有同步操作,提高性能, 矢量代替:逃逸分析方法如果发现对象内存储结构不需要连续进行的话,就可以将对象的部分或者全部都保存在CPU寄存器内

Java7完全支持栈式分配对象,JIT支持逃逸分析优化,还默认支持OpenGL的加速功能
方法区:主要保存的信息是类的元数据,和堆空间类似,也是被JVM所有线程共享,包括

  • 类型信息(类的完整名称,父类完整名称,类型修饰符),
  • 常量池,
  • 方法信息,和
  • 类型的直接接口类表`

*在HotSpot虚拟机中*方法区也被称作是永久区,是一块独立于堆空间的内存空间 永久区GC: (1)对永久区常量池的回收(2)类元数据的回收

三种常见的垃圾回收算法: (Mark-Sweep) 标记清除 ,复制算法(Copying) 标记压缩(Mark-Compact)

mutator and colletor:collector就是指的垃圾收集器,而mutator是指垃圾收集器之外的部分,例如我们的应用程序本身,mutator指责就是NEW(分配内存) READ(从内存中读取内容) WRITE(将内容写入内存) ,而colletor则是回收不再使用的内存来供mutator进行NEW操作,mutator根对象一般指的是分配在堆内存之外,可以直接被mutator直接访问到的对象,一般指静态/全局变量以及ThreadLocal变量

  • 标记清除: 分为2个阶段: 标记和清除
Mark-Sweep Mark-Compact Copying
速度 中等 最慢 最快
空间开销 少(但会积累碎片) 少(没有碎片) 通常需要活动对象的2倍大小
移动对象

标记清除最大缺点: 存在大量的空间碎片.

  • 复制算法: 复制效率高,但是内存缩小一半,一般采用分代: java heap 分为 YounGen and OldGen, 其中Young分为Eden s0 s1 ,其比例为8:1 ,
  • 标记压缩: 一般用在老年代中,标记清除算法不仅效率底下而且执行完还有内存碎片,JVM对此进行了改进产生了标记压缩,先标记再压缩,老年代执行full gc
  • 增量算法(Incremental Collecting): 思路简单,一个线程收集另一个线程执行应用程序,这样就不会stop-the-world, 但是有缺陷,一个线程辛辛苦苦标记的垃圾会被另一个线程影响.增量算法的基本思想是:如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用线程交替执行.缺陷就是线程切换和上下文转换消耗,会使得垃圾收集成本上升造成系统吞吐量下降.
  • 分代收集算法:(Generational Collecting) : 基于对象生命周期分析后得出的垃圾回收算法.他把对象分为,年轻代,老年代和持久代,对不同生命周期的对象使用不同的算法进行回收. JVM(j2se 1.2)都是使用此算法.
  • 评估GC的性能
属性 描述
吞吐量 程序的运行时间(程序运行时间+内存回收时间)
垃圾收集开销 吞吐量的补数,垃圾收集器所占时间与总时间比例
暂停时间 执行垃圾收集,程序的工作线程被暂停时间
收集频率 相对于应用程序的执行,收集操作发生的频率
堆空间 Java堆区所占的内存大小
快速 一个对象从诞生到被回收所经历的时间
  • 垃圾收集器分类

    • Serial/Serial Old
    • ParNew
    • Parallel Old
    • CMS (Concurrent-Mark-Sweep)
    • G1(garbage first)
  • 垃圾收集器的分类

  • Serial 收集器:

0: 串行回收和 stop-the-world
1: -XX:+UseSerialGC 手动指定使用Serial收集器执行内存回收任务.
2: 该算法步骤: (1)在老年代标记存活对象(2)从头开始检查堆内存空间,并且只留下依然幸存的对象(清除)(3)从头开始,顺序的填满堆内存空间,将存活的对象连续存放一起,这样堆分成2部分,一边有存放的对象,一遍没有对象(整理) (4) : Serial收集器应用于小的存储器和少量的CPU

  • 年轻代串行收集器

    • 在HotSpot虚拟机中,使用-XX:+UseSerialGC 参数可以指定使用年轻代串行收集器和老年代串行收集器,当JVM在client模式下运行,它是默认的垃圾收集器
  • 老年代串行收集器

    • 老年代串行收集器使用的是标记压缩算法.和年轻代串行收集器一样,也是独占式串行收集器,但是老年代一旦启动垃圾收集,应用程序会停顿好几秒,也可以使用 -XX:+UseParNewGC,指定年轻代使用并行收集器老年代使用串行收集器
  • ParNew 收集器

    • ParNew 是Serial收集器的多线程版本.基本没有区别,如果使用 -XX:+UseParallelGC 表示年轻代使用并行回收器,老年代使用串行收集器 ,ParallelGC和ParNew不同,Parallel可以控制程序吞吐量大小,因此他也称为吞吐量优先的垃圾收集器.可以使用-XX:GCTimeRatio设置执行内存回收的时间所占用JVM运行总时间比例,也就是控制GC执行频率.公式为1/(1+N).默认值为99/也就是只有1%的时间用于执行垃圾回收.
  • CMS收集器(非独占式):

    • 1: 一个优秀的老年代垃圾收集器. 低延迟是他的优势,算法采用标记清除,因此也会有 'Stop-The-World'机制而出现短暂暂停.
    • 2: CMS执行的四个阶段: 初试标记(Init-Mark) 并发标记(Concurrent-Mark) 再次标记(Remark) 和并发清除(Concurrent-Sweep)
    • 3: 尽管CMS收集器采用的是并行回收,但是初始化和再标记阶段任然要STW机制暂停工作线程,不过时间不长.
    • 4: 前面的几个老年代收集器 Serial Old and Parallel Old收集器都采用算法都是标记清除,因此没有full
      gc之后的内存碎片,而CMS采用的是标记清除,意味着每次执行完会有内存碎片.那么CMS在为新对象分配内存之后,将无法使用指针碰撞(Bump the pointer)技术.而只能选择空闲列表(Free List) 执行内存分配.
  • G1 收集器

    • 1: G1 GC 切分堆内存为多个区间(region),从而避免了很多GC操作在整个Java堆或者整个年代进行.
    • 2: 在JVM启动的时候不需要立即指定哪些Region属于年轻代,哪些Region属于老年代,因为他们都不需要一大块连续的内存.
    • 3: G1的年轻代收集阶段是一个并行的独占式收集器.当一个年轻代进行收集时,整个年代都会被收回,所有的应用线程会被中断,G1 GC会启用多线程执行年轻代回收, 和年轻代不同,老年代G1回收器和其他Hotspot不同,G1的老年代回收器不需要整个老年代被收回,一次只需要扫描/收回一小部分老年代Region就行了,需要注意的是,这个老年代Region是和年轻代一起被收回的
    • 4: G1 把整个Java堆划分为若干个区间(Region),每个region大小为2的倍数.范围在1M-32M之间.所有的region有一样的大小,在JVM生命周期内不会被改变. CSet包含一系列的Region,每一个Region有一个Rset,这个Rset包含了相应Region里面所有指针的位置集合.Rset大小和Region数量有直接关系.一般来说RSet的大小占整个Java堆空间的1%-20%
    • 5: 可用Region:就是unused Region,Eden Region SurvivorRegion 组成了年轻代空间.
    • 6: G1对于大对象有特殊的分配方式,一个大对象是指该对象的大小超过一个Region大小的50%以上,这个大小包含了Java对象头.
  • 全垃圾收集(Full Garbage collection)

    • 1: G1 的FULL GC和Serial GC的Full GC 采用的是同一种算法.Full GC 会对整个Java堆进行压缩,G1的Full GC 是单线程的,会引起较长时间的停顿.因此G1的设计目标就是减少FUll GC的发生次数.
    • 2: 并行循环(Concurrent Cycle) ,一个G1的并行循环包括几个阶段的活动: 初试标记(initialmarking) 并行Root区间扫描和清除(Cleanup),除了最后的cleanup阶段以外,其余阶段都属于标记存活对象阶段.
  • 3: 初始标记阶段的目的是收集所有的GC 根(Roots).Roots是一个对象的起源指针. 为了收集根应用,从应用线程开始,应用线程必须停下,所以他是一个独占式.由于一个年轻代GC必须收集所有的Roots,所以G1的初始标记在一个年轻代GC里完成

    • 4: 并行根区间扫描阶段必须扫描和标记所有的幸存者区间的对象引用,这一阶段可以并行执行.
    • 5: 重标记阶段是一个独占式阶段,通常是一个很短的暂停.这个阶段会完成所以的标记工作.
    • 6: 清除阶段:没有包含存活对象的Region会被回收.并随即被加入Region队列.这个阶段的重要意义是最终决定了哪些Region可以进入混合GC ,在G1内部,混合GC是非常重要的释放内存机制,避免了G1出现没有可用的Region的情况发生.否则就会出现Full GC的情况.
  • 堆大小(Heap Sizing)

    • G1的Java堆通常由多个Region组成,设置堆大小的选项和其它的GC一样,都是由 -xms-xmx的配置
    • 以下情况可能会增大堆内存大小
      (1):Full Gc 阶段

    (2): Young或者Mixed GC发生时,G1 计算GC算费时间与Java线程的花费时间比例,如果-XX:GCTimeRatio 设置GC花费时间很长,则堆大小会增大.这样就会使G1发生GC的频率降低. 这个值默认是9 所有其它HotSpot GC的默认值是99
    (3): 如果一个对象分配失败,即便一个GC刚刚结束,G1采取的策略不是立即重复Full GC,而是通过增大堆内存大小,确保对象分配成功
    (4): 当GC申请加入一个新的Region时.

  • Java永久区:

    • 1: 类中包含的元数据: 类的层级信息,方法数据和方法信息(字节码,栈,和变量大小),运行时常量,已确定的符号引用和虚方法表.
    • 2: 在Java8之前这些数据都放在 永久区,永久区的垃圾回收和老年代的垃圾回收是绑定的,一旦一个区被占满就进行垃圾回收.
    • 3: Java8之后,没有永久区,这些数据被移到了一个与堆不像连的本地内存区域 ,也就是元空间.这就不会出现永久区内存溢出或者出现泄漏的数据移到交换区这样的事情
    • 4: 元空间的内存管理由元空间虚拟机来完成,之前,对于类的元数据需要不同的垃圾回收器进行处理,`现在只需要执行元空间虚拟机的C++代码即可完成.在元空间中,类和元数据的生命周期和其对应的类加载器是相同的. 换句话说: 只要类加载器存活,其加载的类的元数据也是存活的,因而不会被回收掉
    • 5: 每一个类加载器的存储区域都称作一个元空间.所有的元空间和在一起就是我们一直说的元空间,当一个类加载器被垃圾回收标记为不再存活时,其对应空间会被回收,在元空间回收过程中没有重定位和压缩等操作.但是元空间内的元素会进行扫描来确定Java应用.
      -6: 元空间虚拟机复制元空间的分配,其采用的形式为组块分配.组块的大小因类加载器的类型而异.在元空间虚拟机中存在一个全局的空闲组块列表.当一个类加载器需要组块时,他就会从这个全局的组块列表中获取并维持一个自己的组块列表,当一个类加载器不再存活时,其持有的组块将会被释放.并返回给全局列表.类加载器持有的组块又会被分成多个块,每一个块存储一个单元的元信息.组块中的块是线性分配的(指针碰撞分配形式),组块分配自内存映射区域.这些全局的虚拟内存映射区域以链表形式链接.一旦某个虚拟内存映射区域清空,这部分内存就会 操作系统.
    • 7: 元空间虚拟机控制元空间的增长, 可以用: -XX:MaxMetaspaceSize来设定,默认没有限制.因此元空间可以延伸到交换区,但是这时候进行本地内存分配会失败.
    • 8: 元空间虚拟机采用了组块分配的形式,同时区块的大小由类加载器类型决定.类信息并不是固定大小的,因此可能分配的空闲区块和类需要的区块大小不同.这种情况可能导致碎片存在.元空间虚拟机目前不支持压缩操作,所以碎片化是目前最大的问题

13 GC 的参数调试

  • 输出样式
输出 参数 使用场景 是否独占
串行收集器 输出DefNew -XX:+UseSerialGC() 年轻代老年代都使用
并行收集器 输出ParNew -XX:+UseParNewGC() 年轻代(并行)老年代(串行) 部分
PSYoungGen -XX:+UseParallelOldGC() 年轻代老年代都使用并行 部分
PSYoungGen -XX:+UseParallelGC() 年轻代(并行)老年代使用CMS 部分
GarbageFirst heap -XX:+UseG1GC() 特殊收集器 部分
一般来说, 只有在嵌入式产品下 才使用SerialGC - -
  • 使用java -X可以输入参数意思
  • -Xloggc --> -XX:+PrintGCDetails -verbose:gc -Xloggc:gclog
  • -XX:+UseSerialGC -verbose -Xlog:gc:gc.log -Xlog:gc*
  • -Xlog:gc:gc.log -Xlog:gc* -XX:+PrintFlagsFinal -XX:+UnlockDiagnosticVMOptions: 在GC里面有一些选项称之为诊断选项(Diagnostic Options), 可以通过这俩个选项组合起来运行.就可以输出并查看这些选项.

15 深入理解G1 GC

  • G1 GC采用递增,并行运算,独占式的特征方式,并采用拷贝技术实现自身的压缩目标,同时通过并行的多级标记的方式缩短各层级(标记,重标记,清除等阶段)的停顿时间.
  • 在G1中 堆被分成若干个大小相等的Region,每个Region都有一个相关联的RememberSet简称RSet,RS里面的数据结构是Hash表,里面的数据时Card Table(堆中每512个Byte映射在card table 1 byte).简单地说,RS里面存在的都是Region中存活对象的指针,当region中数据发生变化时,首先反映到Card Table中的一个或多个Card上.RS通过扫描内部Card Table得知Region中内存使用和活动对象.在使用R给哦你的过程中,如果Region被填满了.分配内存的线程会重新选择一个新的Region.空闲Region被组织到一个基于链表的数据结构(LinkedList)里面,这样可以快速找到新的Region.
  • G1 的收集过程涵盖4个阶段: 年轻代GC 并发标记周期 混合收集 Full GC
  • 堆区间大小也决定了什么对象可以认定为大对象,大对象通常是很大的对象,占据了超过G1 GC区间的50%,大对象没有按照年轻代的回收方式把对象放入老年区,而是放置在老年区以外的大对象区间里面,即单独分离出来形成一片新的区域,独立管理,
  • 大对象在堆内存里是物理连续的,所以在回收阶段尝试去频繁的回收大对象,存在2个缺点: (1)移动对象需要 拷贝数据,大对象较大,拷贝效率会成为瓶颈.(2)很难找到连续的堆内存用于大对象存储,回收越频繁,越容易出现互相独立的区间.
  • 大对象并没有包含在年轻代或老年代存储空间里,所以不能采用年轻代针对TLABPLAB的分配或者优化措施.
  • 任何一次垃圾回收都会释放CSet里面所有空间.一个CSet由一系列的等待回收区间所组成
  • RSet是一个数据结构,这个数据结构帮助维护和跟踪在他们单元内部的对象引用信息,在G1里这个单元就是Region,G1里面的每一个RSet对应的是一个区间内部的对象引用情况,有了RSet就不需要扫描整个堆内存了,当G1 执行STW独占式回收(年轻代,混合回收)时,只需要扫描每一个区间内部的RSet就可以了.因为所有RSet都保存在CSet里面,即: *Region-RSet-CSet*这样的概念.所有一旦区间内部存活对象被移除,RSet里面保存的引用信息也会被立即更新.这样我们就可以认为RSet就是一张虚拟的对象引用表了...每个区间内部都有这么一张表存在,帮助对区间的内部的对象存活情况,基本信息做有序高效的管理
  • Region并不是最小的单元,每个区间会被进一步划分为若干个块(Chunks),在G1区间里,最小单元是512字节的堆内存块(Card) ,G1为每个区间设置一个全局内存表快来帮助维护所有的堆内存块.
  • RSet帮助维护和跟踪指向G1区间的引用,而这些区间本身拥有这些RSet, 并行Refienment线程的任务是扫描更新日志缓存,并且更新区间RSet,为了更加有效的支援这些Refinement线程的工作.在并行回收阶段,所有未被处理的缓存(已经有日志写在里面了)都会被工作线程拿来处理.这些缓存也被称为日志里面的处理缓存