垃圾回收的分代
Java 将堆分成不同的代,这样分类的原因如下
- 多数对象生命期非常短,朝生夕死
- 另一部分类生命周期很长:常量,池化资源
老年代:oldGen,新生代 youngGen。新生代分为两个survivor存活区和eden区
JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。一些常用参数:
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace)
在java 1.8之前。jvm存在一个区域叫做方法区,方法区和堆是同一层的内存区域,用来存储虚拟机加载的类型信息,常量,静态变量等等不太变更的内容。这部分区域还有一个别称叫做为,非堆内存。但其实并不是所有的jvm都存在永久代这一设计的。其实永久代是HotSpot针对于《Java虚拟机规范》中方法区的一种实现方式。JRockit等虚拟机就不按照HotSpot设计的虚拟机规范去实现,所有从一开始就根本就没有永久代。
- Oracle收购了BEA获得了JRockit的所有权后,准备把JRockit中的优秀功能,譬如Java Mission Control管理工具,移植到HotSpot虚拟机时,因为两者对方法区实现的差异而面临诸多困难。考虑到HotSpot未来的发展,在JDK6的时候HotSpot开发团队就有放弃永久代,逐步改为本地内存(Native Memory)来实现方法区的计划了。而到了JDK7,已经把原本放在永久代的字符串常量、静态变量等移出;到了JDK8,终于完全废弃了永久代的概念,改用与JRockit、J9一样的本地内存中实现元空间(Metaspace)来代替,把JDK7中永久代剩余的内容(主要是类型信息)全部移到元空间中。
- 永久代这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用上限,例如32位系统中的4GB限制,就不会出问题。)
- 有极少数方法(例如String::intern())会因为永久代的原因而导致不同虚拟机下有不同的表现 1.《Java虚拟机规范》对方法区的约束非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾回收。
因为永久代中的垃圾回收策略常常不尽如人意,还导致过几次严重的bug,原因就是因为低版本的 HotSpot虚拟机堆永久代的内存回收不够完全,导致了内存的泄露。综上所述,java 1.8之后,jvm就完全把永久代的功能迁移到了元空间。
Tlab 本地线程分配缓冲区 Jvm为每个线程都分配了一个私有缓存区域,包含在eden空间内 先尝试在Tlab中分配,称为快速分配策略,如果分配不成功再放在eden区域分配
堆内存分配 优先分配到eden区 大对象和长期存活的对象会在young gc 的时候放置到老年代
Gc算法:
引用计数法: 所有资源存在引用计数,初始为0,当有对象引用时候+1;当对象不再引用减一,gc时候查看有没有被使用,没有被使用就回收
根对象包括
- jvm栈的对象引用
- 本地方法栈中的对象引用
- 常量池的对象引用
- 方法区中类静态属性的对象引用
问题:两个资源之间互相之间引用的难以回收 对象之间的引用关系
-
强引用 :直接引用 对象有直接引用关系,强引用存在时gc不会回收对象
-
软引用 系统将要发生oom之前,就会进行回收
-
弱引用 当垃圾收集器工作时,无论内存是否足够,都会进行回收耦
-
虚引用
它的作用在于跟踪垃圾回收过程,在对象被收集器回收时收到一个系统通知
- 虚引用是最弱的引用
- 虚引用对对象而言是无感知的,对象有虚引用跟没有是完全一样的
- 虚引用不会影响对象的生命周期
- 虚引用可以用来做为对象是否存活的监控 是最弱的一种引用关系
标记清除算法
容易实现,思路简单分为两个阶段
marking 标记, 遍历所有的可达对象,在本地内存中分门别类记下 需要被回收的对象
Sweeping (清除)这一步保证了,不可达对象所占用的内存,在之后进行内存分配可以用
标记清除算法是并行gc和cms的基本原理
优势:可以处理循环依赖,只扫描部分对象
标记复制算法
- 将内存分为大小相同的两块
- 一个的内存用完以后,把还存活的对象复制到另一块上
- 再把刚刚复制的区域全部清理
- 能够使用的内存会缩减到原有的一半 标记清除-整理算法
不直接清理可回收对象,而是把存活对象移动到一端
分代回收算法
对新生代一般都是用标记复制算法 将新生代划分为 较大的eden空间,两块较小的survivor空间,比例一般为 8:1:1。 老年代一般都是用标记清除-整理算法, 每次只回收少量对象
垃圾回收先在新生代分配空间,新生代填满以后回收,称为younggc,从eden移动到存活区和old老年代 包含 老年代的回收,称为fullgc,会导致长时间停顿。 除了清除,还要做压缩,整理清除以后导致的内存碎片化 怎么才能标记和清除上百万对象呢? 答案就是STW ,让整个jvm所有线程全部暂停 ,除了垃圾回收器以外全部都停止,stw, 会在safepoint 安全点,代码执行的特殊位置把所有应用线程全部暂停 造成整体应用停滞,除了垃圾回收器以外全部都停止。
主流的垃圾回收器
新生代单线程收集器
- 采用复制算法,标记和清理都是单线程 老年代单线程收集器
- 采用标记整理算法,老年代的单线程回收器 parallel scavenge 收集器
- 并行收集器,追求高吞吐量
- 适合后台应用对交互要求不高的场景,是server 级别默认采用的gc方式 parallelnew收集器
- 新生代收集器,可以认为就是新生代单线程收集器的多线程版本 parallel old
- 老年代版本,使用多线程的标记整理算法 cms收集器
- 采用标记清理算法,高并发,低停顿,追求最短的gc回收停顿时间 g1 :分块处理内存回收
java 7-8 parallel gc 并行gc java 9 G1 Gc
cms gc
最大可能性的并发的标记清除算法 -xx +userConcMarkSweepGC
对年轻代使用标记复制:对老年代使用标记-清除算法
cms gc的设计目标是避免在老年代垃圾收集时候出现长时间的卡断
- 不对老年代进行整理,而是使用空闲列表来管理内存空间的回收
- 在mark-and-sweep标记-清除的阶段大部分工作和应用线程一起并发执行 也就是说,没有明显的应用线程暂停,默认情况下。cms使用了1/4的核心数进行处理,但是值得注意的是,它仍然还是和应用线程争抢cpu资源的, 如果服务器是多核cpu,并且调优的主要目标是降低stw的时间,那么就可以考虑用cms, 进行老年代并发回收时,可能伴随多次的年轻代的minor gc
cmsgc 六个阶段
-
inital mark 初始标记:这个阶段伴随着stw暂停,初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及年轻代中所有存活对象所引用的对象(老年代单独回收)
-
concurrent mark 并发标记 :在这个阶段,cmsgc遍历老年代,标记所有的存活对象,从前一阶段initalmark找到的根对象开始算起,并发标记就是与应用程序同时运行,不用暂停
-
concurrent preclean 并发预清理:这阶段同样是和应用线程并发执行,前一阶段的引用关系可能已经发生了改变如果在并发标记过程中引用关系发生了变化,JVM 会通过“Card(卡片)”的方式将发生了改变的区域标记为“脏”区,这就是所谓的 卡片标记(Card Marking)。
-
final mark 最终标记 :最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW停顿。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。通常 CMS 会尝试在年轻代尽可能空的情况下执行 Final Remark阶段,以免连续触发多次 STW 事件。
-
concurrent sweep 并发清除 :此阶段与应用程序并发执行,不需要 STW 停顿。JVM 在此阶段删除不再使用的对象,并回收他们占用的内存空间。
-
concurrent reset 并发重置 这阶段和应用程序并发执行,重置cms算法内部的数据,为下次循环做准备
CMS 垃圾收集器在减少停顿时间上做了很多复杂而有用的 工作,用于垃圾回收的并发线程执行的同时,并不需要暂停 应用线程。 当然,CMS 也有一些缺点,其中最大的问题就 是老年代内存碎片问题(因为不压缩),在某些情况下 CMS GC 会造成不可预测的暂停时间,特别是堆内存较大的情况下。
G1 gc
G1 的全称就是 garbage -first 意为垃圾优先,哪里一块的垃圾多就优先清理这部分, G1 gc 最主要的设计目标就是将stw暂停的时间和分布,变成可预期并且可以配置的, g1 gc堆不再分为年轻代和老年代,而是划分为多个(2048)个小块 region 每个小块可能一会被定义为eden区,一会是survivor区或者old区,g1 gc还包含了一个 存放大数据的区域, humongous ,存放大内存的数据的地方。
这样划分以后,使得g1不必要每次回收整个堆空间,而是增量的处理一部分的内存快,称为此次gc的回收集。 每次gc暂停都会收集所有年轻代的内存快,但是只包含部分的老年代的内存块 G1的另一项创新是,在并发阶段估算每个小块存活对象的总数,构建回收集合的原则就是,垃圾最多的小块会被优先收集,这也是g1的名称来源
G1 处理过程:
-
年轻代模式转移暂停(Evacuation Pause) G1 GC 会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂 停时间。在应用程序刚启动时,G1 还没有采集到什么足够的信息,这时候就处于初始的 fully-young 模式。当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还 没有存活区,则任意选择一部分空闲的内存块作为存活区。 拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。
-
并发标记(Concurrent Marking) 同时我们也可以看到,G1 GC 的很多概念建立在 CMS 的基础上,所以下面的内容需要对 CMS 有一定的理解。 G1 并发标记的过程与 CMS 基本上是一样的。G1 的并发标记通过 Snapshot-At-The-Beginning(起始快 照)的方式,在标记阶段开始时记下所有的存活对象。即使在标记的同时又有一些变成了垃圾。通过对象的存 活信息,可以构建出每个小堆块的存活状态,以便回收集能高效地进行选择。 这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。 有两种情况是可以完全并发执行的: 一、如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾; 二、在 STW 转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。 当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM参数 InitiatingHeapOccupancyPercent 来设置。和 CMS 一样,G1 的并发标记也是由多个阶段组成,其中一些阶 段是完全并发的,还有一些阶段则会暂停应用线程。
-
阶段 1: Initial Mark(初始标记) 此阶段标记所有从 GC 根对象直接可达的对象。
-
阶段 2: Root Region Scan(Root区扫描) 此阶段标记所有从 "根区域" 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。
-
阶段 3: Concurrent Mark(并发标记) 此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。
-
阶段 4: Remark(再次标记) 和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 G1 收集器会短暂地停止应用线程, 停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。
-
阶段 5: Cleanup(清理) 最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC 的效率,维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的: 例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停。
-
-
转移暂停: 混合模式(Evacuation Pause (mixed)) 并发标记完成之后,G1将执行一次混合收集(mixed collection),就是不只清理年轻代,还将一部 分老年代区域也加入到回收集中。混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数 据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有必要启动 混合模式。 因此,在并发标记与混合转移暂停之间,很可能会存在多次 young 模式的转移暂停。 具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时 性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收 集的过程,很大程度上和前面的 fully-young gc 是一样的。
G1 退化
特别需要注意的是,某些情况下 G1 触发了 Full GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单线程来完成 GC 工作,GC 暂停时间将达到秒级别的。
-
并发模式失败 G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期。 解决办法:增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads 等)。
-
晋升失败 没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC(to-space exhausted/to-space overflow)。 解决办法: a) 增加 –XX:G1ReservePercent 选项的值(并相应增加总的堆大小)增加预留内存量。 b) 通过减少 –XX:InitiatingHeapOccupancyPercent 提前启动标记周期。 c) 也可以通过增加 –XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
-
巨型对象分配失败 当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。 解决办法:增加内存或者增大 -XX:G1HeapRegionSize