赞
踩
不是,只有stw(stop the world)即用户线程停下来后才会执行,那么如果千万线程如何治理呢?
就是此处的安全点
和安全区域
。
Safe Point
?程序执行时并非在所有地方都能停顿下来开始GC , 只有在特定的位置才能停顿下来开始GC , 这些位置称为“ 安全点(Safepoint)。
安全点的选择很重要, 如果太少可能导致GC 等待的时间太长, 如果太频繁可能导致运行时的性能问题。
大部分指令的执行时间都非常短暂,通常会根据“ 是否具有让程序长时间执行的特征” 为标准。
比如: 选择一些执行时间较长的指令作为safe Point
,如:
- 循环的末尾
- 方法临返回前
- 调用方法之后
- 抛异常的位置
Safe Region
?SafePoint 机制保证了程序执行时, 在不太长的时间内就会遇到可进入GC的safepoint 。
但是, 程序“ 不执行” 的时候呢? 例如线程处于Sleep 状态或Blocked 状态, 这时候线程无法响应JVM的中断请求, “ 走” 到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况, 就需要安全区域(Safe Region) 来解决。
安全区域是指在一段代码片段中, 对象的引用关系不会发生变化, 在这个区域中的任何位置开始GC 都是安全的。
我们也可以把Safe Region 看做是被扩展了的Safepoint
实际执行时:
举个生活列子:
一句话,睡觉可以,请先进入酒店再睡觉,并且进去是在屏幕上说一声我进入安全区域了,在睡觉。这样做的目的就是当要GC的时候不至于找不到你,如果看到屏幕上有你这个线程的名字,就知道你是安全的,就会忽略你;
你(线程)睡醒了要出门了。抬头看看大屏幕是不是安全在出去(true),如果不安全(false)就在酒店待着,别出门,等到gc完成后,状态变为false在出门。
如何在GC生时, 检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断: ( 目前没有虚拟机采用了)
首先中断所有线程。如果还有线程不在安全点, 就恢复线程, 让线程跑到安全点。- 主动式中断:
设置一个中断标志, 各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真, 则将自己进行中断挂起。(客栈门上安装了一个显示器,上面会显示ture,或者false。如果系统需要垃圾回收,就会更新这个状态为true,线程到了客栈后看到为true,就进店别别出来了。)
然而, CMS并不是完美的,在使用CMS的过程中会产生2个最让人头痛的问题:
promotion failed(晋升失败)
promotion failed是在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年带有足够的空闲空间,但是由于碎片较多,这时如果新生代要转移到老年带的对象比较大,所以,必须尽可能提早触发老年带的CMS回收来避免这个问题(promotion failed时老年带CMS还没有机会进行回收,又放不下转移到老年带的对象,因此会出现下一个问题concurrent mode failure,需要stop-the-wold GC- Serail Old)。
优化办法:
让CMS在进行一定次数的Full GC(标记清除)的时候进行一次标记整理算法,CMS提供了以下参数来控制:
-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
也就是CMS在进行5此Full GC(标记清除)之后进行一次标记整理算法,从而可以控制老年带的碎片在一定的数量以内,甚至可以配置CMS在每次Full GC的时候都进行内存的整理。
concurrent mode failure(并发更新失败)
concurrent mode failure是在执行CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,这时CMS还没有机会回收老年带产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年带,而老年带也放不下而产生的。
concurrent mode failure出现的频率,这可以通过-XX:+PrintGCDetails
来观察,当出现concurrent mode failure的现象时,就意味着此时JVM将继续采用Stop-The-World的方式来进行Full GC,这种情况下,CMS就没什么意义了,造成concurrent mode failure的原因是当minor GC进行时,旧生代所剩下的空间小于Eden区域+From区域的空间,或者在CMS执行老年带的回收时有业务线程试图将大的对象放入老年带,导致CMS在老年带的回收慢于业务对象对老年带内存的分配。
优化办法:
解决这个问题的通用方法是调低触发CMS GC执行的阀值,CMS GC触发主要由CMSInitiatingOccupancyFraction
值决定,默认情况是当老年代已用空间为68%时,即触发CMS GC,在出现concurrent mode failure的情况下,可考虑调小这个值,提前CMS GC的触发,以保证旧生代有足够的空间。另外,扩大老年代空间和调小CMSMaxAbortablePrecleanTime的值也有助于避免这个问题。
CMS GC需要经过较多步骤才能完成一次GC的动作,在minor GC较为频繁的情况下,很有可能造成CMS GC尚未完成,从而造成concurrent mode failure,这种情况下,减少minor GC触发的频率是一种方法,另外一种方法则是加快CMS GC执行时间,在CMS的整个步骤中,JDK 5.0+、6.0+的有些版本在CMS-concurrent-abortable-preclean-start和CMS-concurrent-abortable-preclean这两步间有可能会耗费很长的时间,导致可回收的旧生代的对象很长时间后才被回收,这是Sun JDK CMS GC的一个bug[1],如通过PrintGCDetails观察到这两步之间耗费了较长的时间,可以通过-XX: CMSMaxAbortablePrecleanTime设置较小的值,以保证CMS GC尽快完成对象的回收,避免concurrent mode failure的现象。
Minor GC后, 救助空间容纳不了剩余对象,将要放入老年带,老年带有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。
解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者 调大新生代或者救助空间
CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年带直接分配,例如大对象,但是老年带没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。
解决办法:+XX:CMSInitiatingOccupancyFraction,调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime
总结一句话:使用标记整理清除碎片和提早进行CMS操作。
Jvm内存结构(jvm运行时内存结构)
Java内存模型
Java对象模型
热点代码
”(HotSpot Code)即时编译器
。
主流的Java虚拟机都有解释器与编译器,他们各有优势:
HotSpot中内置了两个即时编译器,分别为Client Compiler(简称C1)
和 Server Compiler(简称C2)
。
默认采用一个解释器和一个编译器配合的方式工作,叫做混合模式(Mixed Mode),用户可以使用“-client”或“-server”参数去强制指定编译器。
只使用解释器的方式叫“解释模式”(Interpreted Mode),只使用编译器的方式叫“编译模式”(Compiled Mode)。可以通过虚拟机的 -version 命令查看相应信息。
在运行过程中会被即时编译器编译的“热点代码”有两类:
次数是判断为热点代码的判定条件,判断一段代码是不是热点代码的行为就叫“热点探测”(Hot Spot Detection)
目前主要的热点探测方式有两种:基于采样的热点探测,基于计数器的热点探测。
基于采样的热点探测
基于采样的热点探测(Sample Based Hot Spot Detection):采用这种方式虚拟机会周期性地检查各个线程的栈顶,如果某个方法经常出现在栈顶,即为“热点方法”。
基于计数器的热点探测
基于计数器的热点探测(Counter Based Hot Spot Detection)
采用这种方式的虚拟机会为每个方法(甚至代码块)建立计数器,统计执行次数,超过一定的次数即为“热点方法”。
HotSpt使用的是这种方法。
这种方法实现起来比较麻烦一些,需要为每个方法建立并维护计数器,且不能直接获取方法的调用关系,但是它的统计结果更加精确和严谨。
基于计数器的热点探测方式中有两种计数器:
方法调用计数器(Invocation Counter)
:用于统计方法的调用次数。回边计数器(Back Edge Counter)
:用于统计循环代码块调用次数。三色标记法
是Java 虚拟机(JVM)中垃圾回收算法的一种
,主要用来标记内存中存活和需要回收的对象。
它的好处是,可以让 JVM 不发生或仅短时间发生STW(Stop The World),从而达到清除JVM 内存垃圾的目的
JVM 中的「 CMS
、G1 垃圾回收器
」都用到了三色标记法。
在三色标记法中,Java 虚拟机将内存中的对象分为三个颜色:
在GC 开始时(如图)
按照这样一个步骤不断推导,直到灰色集合中所有的对象变黑后,本轮标记完成。最后,还处于白色标记的对象就是不可达对象,可以直接被回收。
1.CPU 是整个电脑的核心计算资源,对于一个应用进程来说,CPU 的最小执行单元是线程。
1.导致CPU 飙高的原因有几个方面
上下文切换需要做两个事情
这两个过程需要 CPU 执行内核相关指令实现状态保存,如果较多的上下文切换会占据大量CPU 资源,从而使得 cpu 无法去执行用户进程中的指令,导致响应速度下降。在Java 中,文件IO、网络IO、锁等待、线程阻塞等操作都会造成线程阻塞从而触发上下文切换
既然是这两个问题导致的CPU 利用率较高,于是我们可以通过 top 命令,找到 CPU利用率较高的进程,在通过种情况。
找到进程中CPU 消耗过高的线程,这里有两种:
最后有可能定位的结果是程序正常,只是在 CPU 飙高的那一刻,用户访问量较大,导致系统资源不够
从这张图中可以看出 JVM 所处的位置,同时也能看出它两个作用:
⼀般情况下,对于开发者⽽⾔,即使不熟悉 JVM 的运⾏机制并不影响业务代码的开发,因为在安装完JDK 或者JRE 之后,其中就已经内置了JVM,所以只需要将Class⽂件 交给JVM 运⾏即可;
但当程序运⾏的过程中出现了问题,⽽这个问题发生在 JVM 层⾯的,那我们就需要熟悉JVM 的运⾏机制,才能迅速排查并解决 JVM 的性能问题。
我们先看下目前主流的 JVM HotSpot 的架构图,通过这张架构图,我们可以看出 JVM的大致流程是把一个 class 文件通过类加载器加载进系统,然后放到不同的区域,通过编译器编译。
Class Files
在Java 中,Class⽂件是由源码⽂件⽣成的,⾄于源码⽂件的内容,是每个Java 开发者在JavaSE 阶段的必备知识,这⾥就不再赘述了,我们可以关注⼀下 Class⽂件的格式,⽐如其中的常量池、成员变量、⽅法等,这样就能知道Java 源码内容在Class⽂件中的表示⽅式
Class Loader Subsystem
即类加载机制
Class⽂件加载到内存中,需要借助Java 中的类加载机制。类加载机制分为装载、链接和初始化,其主要就是对类进⾏查找、验证以及分配相关的内存空间和赋值
Runtime Data Areas
第三个部分Runtime Data Areas 也就是通常所说的运⾏时数据区
其解决的问题就是Class⽂件进入内存之后,该如何进⾏存储不同的数据以及数据该如何进⾏扭转。比如:Method Area 通常会储存由Class⽂件常量池所对应的运⾏时常量池、字段和⽅法的元数据信息、类的模板信息等;Heap 是存储各种Java 中的对象实例;Java Threads 通过线程以栈的⽅式运⾏加载各个⽅法;Native Internal Thread可以理解为是加载运⾏native 类型的⽅法;PC Register 则是保存每个线程执⾏⽅法的实时地址。
Garbage Collector
第四个部分Garbage Collector 也就是通常所说的垃圾回收
就是对运⾏时数据区中的数据进⾏管理和回收。回收机制可以基于不同的垃圾收集器,
⽐如Serial、Parallel、CMS、G1、ZGC 等,可以针对不同的业务场景选择不同的收集器,只需要通过JVM 参数设置 即可。如果我们打开hotspot 的源码,可以发现这些收集器其实就是对于不同垃圾收集算法的实现,核⼼的算法有 3 个:标记-清除、标记-整理、复制
JIT Compiler 和Interpreter
通俗理解就是翻译器,Class 的字节码指令通过JIT Compiler 和Interpreter 翻译成对应操作系统的CPU 指令,只不过可以选择解释执⾏或者编译执⾏,在HotSpot JVM默认采用的是这两种⽅式的混合。
JNI 的技术
如果我们想要找 Java 中的某个native⽅法是如何通过C 或者C++实现的,那么可以通过Native Method Interface 来进⾏查找,也就是所谓的 JNI 技术。
OOM 是out of memory 的简称,表示程序需要的内存空间大于 JVM 分配的内存空间。 OOM 后果就是导致程序崩溃;可以通俗理解:程序申请内存过大,虚拟机无法满足。
导致OOM 错误的情况一般是:
内存泄漏和内存溢出是两个完全不一样的情况
内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出。常见的OOM 异常情况有两种
第一种是配置JVM 启动参数,当触发了OOM 异常的时候自动生成
第二种是使用jmap 工具来生成。然后使用MAT 工具来分析Dump 文件。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。掌握了泄漏对象的类信息和 GC Roots 引用链的信息,就可以比较准确地定位泄漏代码的位置。
如果是普通的内存溢出,确实有很多占用内存的对象,那就只需要提升堆内存空间即可。
首先,我简单说一下类的加载机制(如图),就是我们自己写的 java 源文件到最终运行,必须要经过编译和类加载两个阶段。
(如图)而类的加载过程,需要涉及到类加载器。
JVM 在运行的时候,会产生 3 个类加载器,这三个类加载器组成了一个层级关系每个类加载器分别去加载不同作用范围的 jar 包,比如
除了系统自己提供的类加载器以外,还可以通过 ClassLoader 类实现自定义加载器,去满足一些特殊场景的需求。
(如图)所谓的父委托模型,就是按照类加载器的层级关系,逐层进行委派。
比如当需要加载一个 class 文件的时候,首先会把这个 class 的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个 class。
这样设计的好处,我认为有几个。
在JVM 里面,要判断一个对象是否可以被回收,最重要的是判断这个对象是否还在被使用,只有没被使用的对象才能回收。
也就是为每一个对象添加一个引用计数器,用来统计指向当前对象的引用次数,如果当前对象存在应用的更新,那么就对这个引用计数器进行增加,一旦这个引用计数器变成 0,就意味着它可以被回收了。
这种方法需要额外的空间来存储引用计数器,但是它的实现很简单,而且效率也比较高;
不过主流的JVM 都没有采用这种方式,因为引用计数器在处理一些复杂的循环引用或者相互依赖的情况时,
可能会出现一些不再使用但是又无法回收的内存,造成内存泄露的问题。
它的主要思想是,首先确定一系列肯定不能回收的对象作为 GC Root,比如虚拟机栈里面的引用对象、本地方法栈引用的对象等,然后以 GC ROOT 作为起始节点,
从这些节点开始向下搜索,去寻找它的直接和间接引用的对象,当遍历完之后如果发现有一些对象不可到达,
那么就认为这些对象已经没有用了,需要被回收。
在垃圾回收的时候,JVM 会首先找到所有的GC root,这个过程会暂停所有用户线程,也就是stop the world,然后再从GC Roots 这些根节点向下搜索,可达的对象保留,不可达的就会回收掉。
可达性分析是目前主流 JVM 使用的算法。
首先,在 JVM 的heap 内存里面,分为 Eden Space、Survivor Space、Old Generation(如图)。
当我们在Java 里面使用new 关键字创建一个对象的时候,JVM 会在Eden Space 分配一块内存空间来存储这个对象。
当Eden Space 的内存空间不足的时候,会触发 Young GC 进行对象回收。
那些因为存在引用关系而无法回收的对象,JVM 会把它们转移到 Survivor Space。
Survivor Space 内部又分为From 区和To 区,刚从Eden 区转移过来的对象会分配到From 区,
每经历一次Young GC,这些没有办法被回收的对象就会在 From 区和To 区来回移动,每移动一次,这个对象的
GC 年龄就加 1。默认情况下 GC 年龄达到 15 的时候,JVM 就会把这个对象移动到 Old Generation。
其次呢,一个对象的 GC 年龄,是存储在对象头里面的(如图),一个 Java 对象在JVM内存中的布局由三个部分组成,分别是对象头、实例数据、对齐填充。而对象头里面有 4 个bit 位来存储GC 年龄。
而 4 个bit 位能够存储的最大数值是 15,所以从这个角度来说,JVM 分代年龄之所以设置成 15 次是因为它最大能够存储的数值就是 15。
虽然JVM 提供了参数来设置分代年龄的大小,但是这个大小不能超过 15。
而从设计角度来看,当一个对象触发了最大值 15 次gc,还没有办法被回收,就只能移动到old generation 了。
另外,设计者还引入了动态对象年龄判断的方式来决定把对象转移到 old generation,也就是说
不管这个对象的 gc 年龄是否达到了 15 次,只要满足动态年龄判断的依据,也同样会转移到old generation。
在HotSpot 虚拟机里面,(如图)一个对象在堆内存里面的内存布局是使用OOP 结构来表示的,
它主要分为三个部分。
Java 虚拟机是Java 语言的运行环境。
之所以需要Java 虚拟机,主要是为 Java 语言提供Write Once,Run Anywhere 能力。实际上,一次编写,到处运行这个能力本身是不可能实现的。因为不同的操作系统和硬件。
最终执行的指令会有较大的差异。
而Java 虚拟机就是解决这个问题的,它能根据不同的操作系统和硬件差异,生成符合这个平台机器指令。
简单理解,它就相当于一个翻译工具,在window 下,翻译成window 可执行的指令,在linux 下,
翻译成linux 下可执行的指令。除了这个因素以为,我认为自动回收垃圾这个功能也是原因之一,它让开发者省去了垃圾回收这个工作。减少了程序开发的复杂性。
在Hotspot 虚拟机中,方法区的实现是在永久代里面,它里面主要存储运行时常量池、 Klass 类元信息等。
永久代属于JVM 运行时内存中的一块存储空间,我们可以通过-XX:PermSize 来设置永久代的大小。
当内存不够的时候,会触发垃圾回收。
在JDK1.8 里面,JVM 运行时数据区是这样的(如图)
在Hotspot 虚拟机中,取消了永久代,由元空间来实现方法区的数据存储。 元空间不属于JVM 内存,而是直接使用本地内存,因此不需要考虑GC 问题。
默认情况下元空间是可以无限制的使用本地内存的,但是我们也可以使用JVM 参数来限制内存使用大小。
实际上,垃圾收集器(GC,Garbage Collector)是和具体 JVM 实现紧密相关的,不同厂商(IBM、Oracle),不同版本的 JVM,提供的选择也不同。接下来,我来谈谈最主流的Oracle JDK。
Serial GC
Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。
当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是Client 模式下 JVM 的默认选项。从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(MarkCompact)算法,区别于新生代的复制算法。 Serial GC 的对应 JVM 参数是-XX:+UseSerialGC
ParNew GC
很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作,下面是对应参数-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
Parrallel GC
在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC选择,也被称作是吞吐量优先的 GC
。
它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。开启选项是:-XX:+UseParallelGC
另外,Parallel GC 引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标,JVM 会自动进行适应性调整,例如下面参数:
-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC 时间和用户时间比例= 1 / (N+1)
CMS(Concurrent Mark Sweep) GC
基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间
这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。
但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。
G1 GC
这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选项。
G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。
G1 GC 仍然存在着年代的 概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。 Region
之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。
G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时 CMS 已经在 JDK9 中被标记为废弃(deprecated),所以 G1 GC 值得你深入掌握。
我们自己写的java 源文件到最终运行,必须要经过编译和类加载两个阶段(如图)。
JVM 在运行的时候,会产生 3 个类加载器,这三个类加载器组成了一个层级关系每个类加载器分别去加载不同作用范围的 jar 包,比如
除了系统自己提供的类加载器以外,还可以通过 ClassLoader 类实现自定义加载器,去满足一些特殊场景的需求。
(如图)而双亲模型,就是按照类加载器的层级关系,逐层进行委派。比如当需要加载一个 class 文件的时候,首先会把这个 class 的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个 class。
不过,双亲委派并不是一个强制性的约束模型,我们可以通过一些方式去打破双亲委派模型。
这个打破的意思,就是类加载器可以加载不属于当前作用范围的类,实际上,JVM 本身就存在双亲委派被破坏的情况。
上下文类加载器
,它可以把原本需要启动类加载器加载的类,由应用类加载器进行加载。除此之外,像 Tomcat 容器,也存在破坏双亲委派的情况,来实现不同应用之间的资源隔离。小总结:
有两种方式来破坏双亲委派模型
CMS
(Concurrent Mark and Sweep) 是一种低停顿垃圾回收器
,
它主要通过并发标记阶段
和并发清除阶段
两个并发的阶段来实现垃圾回收
它的整体流程可以分成四个步骤(如图):
初始标记(CMS initial mark)
:这个阶段需要Stop The Word,来标记哪些对象是需要回收的,这个过程只需要标记GC Roots 能够直接关联的对象,所以速度很快,对性能影响比较小。并发标记(CMS concurrent mark)
:扫描整个堆中的对象,标记所有不需要回收的对象。这个阶段不需要 Stop The Word,在应用程序运行过程中进行标记。重新标记(CMS remark)
:为了修正并发标记期间,应用程序同步运行导致标记产生变动的那一部分对象。这个阶段需要 Stop The Word。并发清除(CMS concurrent sweep)
,CMS 会并发执行清除操作,同时应用程序继续运行,最大力度减少对性能的影响。G1是JDK9默认的垃圾收集器,代替了CMS收集器。它的目标是达到更高的吞吐量和更短的GC停顿时间。
不像CMS要全部STW,G1可以渐进式回收,不停顿太久。
// G1CollectedHeap.java
void collectGarbage(G1ConcurrentMark mark) {
initial-mark; // STW
remark(); // Concurrent
cleanup(); // STW
concurrent-cleanup(); // Concurrent
}
不需要一次全堆回收,可以分代增量回收,选择性回收新生代和老年代。
void collectGarbage(boolean collectOnlyYongGen) {
if (collectOnlyYongGen) {
collectYoungGenGarbage(); // only YongGen
} else {
collectGarbage(); // YongGen and Old Gen
}
}
通过Remembered Sets实现空间整合,解决碎片问题。
// G1RemSet.java
void addToRememberedSets(HeapRegion from, HeapRegion to) {
from.addRememberedSetEntry(to);
}
Remembered Sets和Card Tables都是G1用来管理堆和处理垃圾回收的重要数据结构。
它们的工作可以简述为:
可以看到,Remembered Sets和Card Tables是G1高效率回收的关键,它们让G1不需要像CMS那样全堆回收,可以有选择性地、增量式地进行分代、分片的回收,极大的提高了工作效率。
G1的垃圾回收过程可以分为以下几个主要阶段:
标记GC Roots能直接关联的对象,需要Stop The World。
private void initialMark() {
for (Object obj : strongRefs) {
G1CollectedHeap.mark(obj);
}
}
从GC Roots开始对堆中对象进行并发标记,需要部分STW。
修正并发标记期间的错误标记,需要STW。
根据标记和Card Table结果筛选回收区域,回收垃圾,需要STW。
// 筛选待回收区域
void selectGarbageCollectionCandidates() {
Region[] filtered = filterRegions();
garbageCollect(filtered);
}
与用户线程一起工作,对标记和筛选阶段误差产生的垃圾链进行清理。
与用户线程一起工作,为下次GC做准备。
这一过程实际上和CMS非常相似,同为“标记-清除”算法。但G1在并发标记的基础上,通过Remembered Sets和Card Tables实现了分代回收和空间整合,这也是它能达到高性能的关键。
元空间是Java 8及以后版本中的概念,它用来替代永久代,存放加载的类信息、常量、静态变量、JIT编译后的代码等,使用的是本地内存。
元空间中存储的类信息、常量、静态变量等不再受到限制,可以随时地加载、卸载,因此,元空间出现内存溢出(OOM)的情况较多,例如:
解决元空间内存溢出的问题有以下几个方法:
cglib是一个第三方库,用于在运行时生成和修改Java字节码。它能够在运行时生成代理类,这是一种很强大的技术,但也意味着它会创建很多新的类和对象。因此,如果使用不当,它可能会消耗大量的方法区内存,甚至导致内存溢出错误。
"撑爆方法区"这个说法,其实是因为Java虚拟机(JVM)在运行时需要为每个加载的类保留一些元数据,这些元数据存储在方法区中。如果方法区的内存不足以存储这些元数据,就会抛出内存溢出错误。
当使用cglib动态生成大量类时,这些新生成的类也会在方法区中占用空间。如果生成的类过多,方法区的内存可能会被迅速消耗完,导致内存溢出错误。
要避免这种情况,可以尝试以下方法:
减少动态生成的类的数量。
尽量复用对象,避免创建过多的新对象。
调整JVM的参数,如增大方法区的内存大小(-XX:MaxPermSize)。
如果可能,使用其他库或技术替代cglib,如ByteBuddy、Javassist等。
请注意,具体的解决方案可能会根据你的应用场景和具体的问题而有所不同。
应用长时间运行,没有重启的情况下,会导致元空间OOM,是因为应用在运行过程中,随着时间的推移,会创建大量的对象,这些对象会占据内存空间。如果这些对象没有被及时回收,就会导致内存中的空间被逐渐消耗殆尽,最终触发元空间OOM错误。
元空间是 Java 8 及以后版本中用来存储类元数据的区域。它取代了早期版本中的永久代(PermGen)。元空间主要用于存储类的结构信息、方法信息、静态变量以及编译后的代码等。
当程序加载和定义大量类、动态生成类、使用反射频繁操作类等情况下,可能会导致元空间耗尽。
常见导致元空间耗尽的情况包括:
结论:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。