11. 堆

三种JVM

  • Sun:hotSpot
  • BEA公司
  • IBM

先问是不是HotSpot堆

12. 新生区养老区

新生区

类:诞生和成长,死亡的地方

  • 伊甸园:所有的对象都是在伊甸园区new出来的
  • 幸存者区
    • 幸存区0区
    • 幸存区1区

养老区

GC垃圾回收,主要是在伊甸园区和养老区

13. 永久区

JDK8以后,永久区叫做元空间
永久区常驻内存,用来存放JDK自身携带的Class对象
Interface元数据,存储的是java运行时的一些环境。
不存在垃圾回收,虚拟机关闭,内存释放
一个启动类加载大量的第三方jar包或者tomcat部署了大量应用、大量动态生成的反射类==》》》OOM

  • jdk1.6 之前: 永久代、常量池在方法区
  • jdk1.7:永久代,但是慢慢退化了 ,去永久代,常量池在堆中
  • jdk1.8之后:无永久代,常量池在元空间

默认情况下:分配的总内存是 电脑内存的 1/4,初始化内存是 电脑内存的 1/64

14. 堆内存调优

打印gcdetail
-XX:+PrintGCDetails
image.png
年轻代+永久代
57344k+131072k /1024
image.png
image.png

问题来了,元空间占用的是哪里的内存?
元空间占用的是本地内存

如果报OOM的错误,那么

  • 调整堆内存空间
  • 还报错就是代码有问题

image.png
image.png

在一个项目中,突然出现了OOM故障,那么该如何排除?研究为什么出错?

  • 能够看到代码第几行出错:内存快照分析工具,JProfiler
  • Debug,一行行分析代码

image.png

image.png

MAT(eclipse插件),JPofile作用

  • 分析Dump内存文件,快速定位内存泄露
  • 获得堆中的数据
  • 获得大的对象

内存泄露还是内存溢出?

GC

复制算法

幸存区两个区的问题
谁空谁是to

  1. 每次GC都会将Eden中活的对象移到幸存区中,一旦Eden区被GC后,就会是空的

对象经历了15次GC还没有死,进入养老区(对象头有四位代表对象的代数)

  • 好处:没有内存碎片
  • 坏处:浪费了内存空间,多了一半空间永远是空 to。假设对象100%存活(极端情况),就要复制所有的对象

复制算法最佳使用场景:对象存活度较低的情况(新生区)

标记清除算法

  • 优点:不需要额外的空间
  • 缺点:两次扫描,浪费时间,会产生内存碎片

    标记压缩

    压缩可以防止内存碎片产生
    多了移动成本

《深入理解JVM虚拟机》第三章

  1. 垃圾回收需要考虑的问题:
  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。

  1. 程序运行的不确定性

一个接口的不同实现类需要的内存不一样
一个方法所执行的不同条件分支需要的内存不一样
只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理

引用计数器法

教科书判断对象是否存活的算法是这样的:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

在Java领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要原因是,这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
**

可达性分析法

主流的商用程序语言(Java、C#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

image.png
GC - 图9

GC Roots

image.png

为什么是这些 可以作为GC Roots

方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots,被GC roots引用的对象不被GC回收。

跨代引用对象?

已知GCRoots对象同属一个CardTable的其他对象

HotSpot的算法实现细节

分代收集理论

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行收集

分代收集符合大多数程序运行实际情况的经验法则,建立在两个分代假说之上:
1) 弱分代假说:绝大多数对象都是朝生夕灭的
2) 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡

分代收集的困难:

  • 划分内存区域
  • 对象不是孤立的,对象之间存在跨代引用

3) 跨代引用假说:跨代引用相对于同代引用来说仅占极少数

  1. Partial GC: 指目标不是完整收集整个Java堆的垃圾收集
    1. 新生代收集(Minor GC/Young GC):目标只是新生代的垃圾收集
    2. 老年代收集(Major GC/Old Gc):目标只是老年代的垃圾收集
    3. 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集
  2. Full GC:收集整个Java堆和方法区的垃圾收集

    根节点枚举

    根节点:
    全局性的引用(常量或者类静态变量==》》 方法区)
    执行上下文(Java栈和本地方法栈)

但是!!!方法区就已经数百上千兆,如何枚举根节点?

所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的。

记忆集与卡表

为了解决跨代引用的问题,垃圾收集器在新生代中建立了名为记忆集(Remembered Set)的数据结构,用以避免把整个老年代加进GC Roots扫描范围。

在同一个CardTable上的对象也是GC Roots

并发的可达性分析

可达性分析理论上要求全过程都基于一个能保障一致性的快照中才能够进行,这意味着必须全程冻结用户线程的运行。

随着堆的扩大,标记过程自然会等比例增加停顿时间。

问题:
如果在进行可达性分析的同时,用户线程修改引用关系

JDK7-JDK11的垃圾收集器

image.png

Serial收集器

最基础、历史最悠久的收集器
JDK 1.3.1之前时HotSpot虚拟机新生代的唯一选择

单线程工作的收集器

  • 使用一个处理器或者一条收集线程去完成垃圾收集工作
  • 在垃圾收集时,必须暂停其他所有工作线程

“Stop the World”

事实上: 仍然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器

优点:

  • 简单高效:对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的。
  • 对于单核处理器或者处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然获得最高的单线程收集效率

image.png

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为都与Serial收集器完全一致。

image.png
它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集
器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS
收集器配合工作。

Parallel Scavenge收集器

新生代收集器
CMS等收集器的关注点: 尽可能地缩短垃圾收集时用户线程的停顿时间
Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)
image.png
**
“吞吐量优先收集器”

Serial Old 收集器

HotSpot客户端 老年代
在服务端模式下,它也可能有两种用途:

一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用[1],
另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。
image.png

CMS收集器(Concurrent Mark Sweep)

是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。

用在老年代的垃圾收集器

工作步骤

  1. 初始标记

stop the world
标记一下GC ROOTS能直接关联到的对象,速度很快

  1. 并发标记

从GC ROOTS的直接关联对象开始遍历整个对象图的过程
耗时比较长但是不需要停顿用户线程
可以与垃圾收集线程一起并发运行(垃圾收集????)

  1. 重新标记

stop the world
修正并发标记期间,因为用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

既然在上一步的时候不用停顿用户线程,那么就要付出代价

  1. 并发清除

清理删除掉标记阶段的已经死亡的对象,由于不需要移动存活对象,所以这个姐u但也是可以和用户线程同时并发的。

优点

并发收集
低停顿

缺点

  1. 对处理器资源敏感

占用大量的CPU资源
只要是并行执行,都会占用大量的CPU资源
CMS占用的线程更多

  1. 无法处理浮动垃圾

什么叫浮动垃圾?
用户线程与垃圾收集线程同时执行,肯定无法收集所有的垃圾

  1. 生成大量的内存碎片
  2. 出现 Concurrent Mode Failure

如果内存区域指定不合理的话
触发 Serial Old

G1收集器(Garbage First收集器)

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

像是之前的新生代,老年代,都是连续的内存

JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。

设计者们希望做出一款能够建立起“停顿时间模型”(Pause Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标(还是吞吐量的思想吗?),这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了。

G1首先在内存结构上采用了region化的方法,将堆内存划分成2000块左右的小块,每块大小1-32M(2的幂次),每块region都可以作为E、S、O任意一种,分配灵活,但是存在大对象问题。解决方法是:

  • 小于一半region size的可以正常存入E区
  • 一半到一个region size的直接存入O区一个region中,这个region又叫Humongous region,我们也可以把它叫做H区(他本质还是O区的)
  • 比一个region size还要大的对象,需要存入连续的多个region中,这多个region都是H区。

G1面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

image.png
由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。

Mixed GC

在收集Old区的垃圾时,同时收集了Young区的垃圾

  • 初次标记:GCRoot 对象,和GCRoot对象所在的region(Root Region)

Stop the world

  • 通过Root Region,扫描Old区的所有Region,Old区的所有Region中的Rset中是否含有Root Region
    • 如果有,标记
    • 没有,不标记
  • 并发标记:同CMS,遍历上一步Old 区被标记的区域
  • 重新标记:作用同CMS,但是算法是SATB

Stop the world

  • 清理:复制清理

stop the world
**

目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其
优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,以上这些也仅是经验之谈,不
同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也
会让对比结果继续向G1倾斜。

问题

  1. JVM的内存模型和分区,详细到每个区放什么?
  2. 堆里面的分区有哪些?Eden,from,to,老年区,说说他们的特点
  3. GC的算法有哪些?标记清除法,标记整理法(标记压缩),复制算法,引用计数法
  4. 有哪些垃圾收集器,介绍一下?
  5. 轻GC和重GC分别在什么时候发生

G1问题列表

RootRegionScan这个阶段是干嘛的?

标记出RootRegion指向O区的region,标记这些region是为了降低并发标记的扫描范围,因为并发标记需要扫描GCROOT引用或间接的所有对象,而这些对象一定是在RootRegion出发指向的Region中的。MIXGC中Y区本来就要全扫,所以这里再按照O区过滤下,这样就缩小了扫描范围。该阶段的操作为遍历O区region查询Rset是否有来自RootRegion的,(RootRegion是初始标记得到的)。

Rset作用有哪些?

上题中的作用是一个,还有个作用是YGC时,O区不GC因而认为O区全为‘GCroot’,需扫描全部O区。有了Rset只需要查看所有Y区region的Rset就知道被哪些O区region跨带引用了,避免了扫描整个O区。

G1提高效率的点有哪些?

1 重新标记时X区域直接删除。 2 Rset降低了扫描的范围,上题中两点。 3 重新标记阶段使用SATB速度比CMS快。 4 清理过程为选取部分存活率低的Region进行清理,不是全部,提高了清理的效率。

对比CMS,有哪些不同?

1 region化的内存结构,采用复制清理的方式,避免了内存碎片。但是这种清理也造成了STW。 2 SATB速度更快。 3 初始标记,并发标记,重新标记,清理垃圾四个阶段很像,但是G1中有很多标记region的操作,并借助Rset进行了范围的缩小,提高了并发标记的速度。小结下就是初始标记和YGC的STW一起了,提高了效率;并发标记因为rset的设计,扫描范围缩小了,提高了效率;重新标记因为使用了SATB提高了效率;清理虽然造成了STW,但是复制使内存紧凑,避免了内存碎片。同时只清理垃圾较多的region,最大限度的降低了STW时间。