JVM堆内存和方法区详解与垃圾回收

1. 概述

JVM重点存储数据的是堆和方法区(非堆),所以内存的设计也着重从
这两方面展开(注意这两块区域都是线程共享的)。
对于虚拟机栈,本地方法栈,程序计数器都是线程私有的。
可以这样理解,JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现
在这里插入图片描述

2. 堆和方法区(非堆)

在这里插入图片描述
一块是非堆区(方法区),一块是堆区 堆区分为两大块,一个是Old区,一个是Young区 Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区 S0和S1一样大,也可以叫From和To

2.1 对象创建过程

一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区(担保机制)。

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了 挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor 区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的 时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的。

在这里插入图片描述

2.2 堆内存各区详解
名称 介绍
Old区 Old区又称老年代,存储了大量存活时间久的对象, 或者是通过担保机制的超过Young区大小的大对象
Young区 Young区又称新生代,存储了新创建的对象和创建时间不长的对象 ,Young区又分为Eden区和Survivor区(S0+S1),Eden区存储了新创建的对象,Survivor区存储了至少经过一次GC的对象

3. 常见问题

如何理解Minor/Major/Full GC

Minor GC:新生代
Major GC:老年代
Full GC: 新生代+老年代+非堆,因为老年代GC绑定了非堆GC

为什么需要Survivor区?只有Eden不行吗?

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。 这样一来,老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了 Full GC)。
老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。


执行时间长有什么坏处?
频发的Full GC消耗的时间很长,会影响大型程序的执行和响应速度。 可能你会说,那就对老年代的空间进行增加或者较少咯。 假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一 旦发生Full GC,执行所需要的时间更长。 假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。 所以Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保 证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么需要两个Survivor区?

最大的好处就是解决了碎片化。也就是说为什么一个Survivor区不行?
第一部分中,我们知道了必须设置 Survivor区。假设现在只有一个Survivor区,我们来模拟一下流程:
刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor 区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些 存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的, 也就导致了内存碎片化。
永远有一个Survivor space是空的,另一个非空的Survivor space无碎片。

新生代中Eden:S1:S2为什么是8:1:1?

新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1区为8:1
即新生代中Eden:S1:S2 = 8:1:1
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象大概98%是 “朝生夕死”的

堆内存中都是线程共享的区域吗?

JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。
对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。

4. Garbage Collect(垃圾回收)

堆内存中有垃圾回收,比如Young区的Minor GC,Old区的Major GC,Young区和Old区 的Full GC。
但是对于一个对象而言,怎么确定它是垃圾?是否需要被回收?怎样对它进行回收?等等这些问
题我们还需要详细探索。
因为Java是自动做内存管理和垃圾回收的,如果不了解垃圾回收的各方面知识,一旦出现问题我
们很难进行排查和解决,自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,
寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除 。

4.1 如何确定一个对象是垃圾?

要想进行垃圾回收,得先知道什么样的对象是垃圾。

4.1.1 引用计数法

对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。
弊端 :如果AB相互持有引用,导致永远不能被回收。

4.1.2 可达性分析

通过GC Root的对象,开始向下寻找,看某个对象是否可达
在这里插入图片描述
GC Root(不是对象,是对象的引用):
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.方法区中类静态属性引用的对象。
3.方法区中常量引用的对象。
4.本地方法栈中JNI(即一般说的Native方法)引用的对象。

4.2 什么时候会垃圾回收

GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。
当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是
具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决
定。但是不建议手动调用该方法,因为GC消耗的资源比较大。

场景
1.当Eden区或者S区不够用了
2.老年代空间不够用了
3.方法区空间不够用了
4.System.gc()

4.3 垃圾收集算法
4.3.1 标记-清除(Mark-Sweep)

标记:
找出内存中需要回收的对象,并且把它们标记出来

此时堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时

在这里插入图片描述
清除:
清除掉被标记需要回收的对象,释放出对应的内存空间
在这里插入图片描述
缺点:
1.标记和清除两个过程都比较耗时,效率不高
2.会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无 法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

4.3.2 标记-复制(Mark-Copying)

将内存划分为两块相等的区域,每次只使用其中一块,如下图所示:
在这里插入图片描述
当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉。

在这里插入图片描述
缺点:
空间利用率降低。

4.3.3 标记-整理(Mark-Compact)

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果
不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有
100%存活的极端情况,所以老年代一般不能直接选用这种算法。

标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

其实上述过程相对"复制算法"来讲,少了一个"保留区"

在这里插入图片描述
让所有存活的对象都向一端移动,清理掉边界意外的内存。
在这里插入图片描述

4.3.4 分代收集算法

1.Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
2.Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要,不如做个标记再清理)

4.4 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

在这里插入图片描述

4.4.1 Serial

在这里插入图片描述

Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯
一选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。

优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法 适用范围:新生代
应用:Client模式下的默认新生代收集器

4.4.2 Serial Old

在这里插入图片描述
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用"标记-整理算
法",运行过程和Serial收集器一样。

4.4.3 ParNew

在这里插入图片描述

可以把这个收集器理解为Serial收集器的多线程版本。

优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器

4.4.4 Parallel Scavenge

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量。

吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序
的运算任务。
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,
-XX:GCRatio直接设置吞吐量的大小。

4.4.5 Parallel Old

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回
收,也是更加关注系统的吞吐量。

4.4.6 CMS

在这里插入图片描述

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS采用的是标记-清除算法,由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

  1. 初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快。
  2. 并发标记 CMS concurrent mark 进行GC Roots Tracing。
  3. 重新标记 CMS remark 修改并发标记因用户程序变动的内容。
  4. 并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为 浮动垃圾。

优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量

4.4.7 G1(Garbage-First)

在这里插入图片描述

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个
大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再
是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂
如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中
设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域。

(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消 耗在垃圾收集上的时间不得超过N毫秒)

在这里插入图片描述

工作过程:

  1. 初始标记(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂 停用户线程 。
  2. 并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发 执行。
  3. 最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需 暂停用户线程 。
  4. 筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据 用户所期望的GC停顿时间制定回收计划。
4.4.8 ZGC

在这里插入图片描述
JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了
会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题,只能在64位的linux上使用,目前用得还比较少。
(1)可以达到10ms以内的停顿时间要求
(2)支持TB级别的内存
(3)堆内存变大后停顿时间还是在10ms以内

4.4.9 垃圾收集器分类
  • 串行收集器->Serial和Serial Old
    只能有一个垃圾回收线程执行,用户线程暂停。
  • 并行收集器[吞吐量优先]->Parallel Scanvenge、Parallel Old。
    多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
    适用于科学计算、后台处理等若交互场景 。
  • 并发收集器[停顿时间优先]->CMS、G1
    用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行,适用于相对时间有要求的场景,比如Web 。
4.4.10 常见问题

吞吐量和停顿时间

停顿时间->垃圾收集器 进行 垃圾回收终端应用执行响应的时间
吞吐量->运行用户代码时间/(运行用户代码时间+垃圾收集时间)

停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互 的任务

如何选择合适的垃圾收集器

优先调整堆的大小让服务器自己来选择
如果内存小于100M,使用串行收集器
如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
如果允许停顿时间超过1秒,选择并行或JVM自己选
如果响应时间最重要,并且不能超过1秒,使用并发收集器

对于G1收集

JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代。
是否使用G1收集器?

(1)50%以上的堆被存活对象占用
(2)对象分配和晋升的速度变化非常大
(3)垃圾回收时间比较长

猜你喜欢

转载自blog.csdn.net/qq_38306425/article/details/107811833