讲讲 JVM 的内存管理『非专业』

云计算 虚拟化
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

[[399153]]

 jvm 内存布局

一类是每个线程所独享的:

  1. PC Register:也称为程序计数器, 记录每个线程当前执行的指令信息(eg:当前执行到哪一条指令,下一条该取哪条指令)
  2. JVM Stack:也称为虚拟机栈,记录每个栈帧(Frame)中的局部变量、方法返回地址等。线程中每次有方法调用时,会创建Frame,方法调用结束时Frame销毁。
  3. Native Method Stack: 本地(原生)方法栈,顾名思义就是调用操作系统原生本地方法时,所需要的内存区域。

上述3类区域,生命周期与Thread相同,即:线程创建时,相应的内存区创建,线程销毁时,释放相应内存。

  • Heap:即鼎鼎大名的堆内存区,也是GC垃圾回收的主站场,用于存放类的实例对象及Arrays实例等。

注:Heap被所有线程共享,如果严格意义上抠字眼的话,也不完正确,事实上,由于TLAB的存在,为了防止并发对象分配时,多个对象分配到同1块内存,heap中的TLAB区域,在分配时,是被线程独占写入的。

  • Method Area:方法区,主要存放类结构、类成员定义,static静态成员等。
  • Runtime Constant Pool:运行时常量池,比如:字符串,int -128~127范围的值等,它是Method Area中的一部分。

Heap、Method Area 都是在虚拟机启动时创建,虚拟机退出时释放

哪些内存区域需要 GC

thread独享的区域:PC Regiester、JVM Stack、Native Method Stack,其生命周期都与线程相同(即:与线程共生死),所以无需GC。线程共享的Heap区、Method Area则是GC关注的重点对象。

引用类型

强引用:被强引用关联的对象不会被回收。

软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。

弱引用:被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。

虚引用:为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

Minor GC 和 Full GC

Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。

Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。

Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

FULL GC 的触发条件有以下几个:

「调用 System.gc()」

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

「老年代空间不足」

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,1.应当尽量不要创建过大的对象以及数组。2.除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。3.还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

「空间分配担保失败」

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

「JDK 1.7 及以前的永久代空间不足」

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

「Concurrent Mode Failure」

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

如何判断对象是垃圾

引用计数算法

在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

  • 可达性分析算法

以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:

  • 虚拟机栈中局部变量表中引用的对象
  • 本地方法栈中 JNI 中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

除了对象回收之外,还可能会有类的卸载

方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。方法区的回收主要是对常量池的回收和对类的卸载。

为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

finalize()

  • 类似 C++ 的析构函数,用于关闭外部资源。但是 try-finally 等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
  • 当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。

常用的 GC 算法

「标记清除法」:在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。

在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。

优缺点:

  • 标记和清除过程效率都不高;
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

「标记复制法」:思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。

优缺点:

  • 避免了内存碎片问题。
  • 内存浪费很严重,相当于只能使用50%的内存。

现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

「标记-整理(也称标记-压缩)法」:避免了上述二种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于windows的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低GC的效率.

「generation-collect 分代收集算法」:经过大量实际分析,发现内存中的对象,大致可以分为二类:有些生命周期很短,比如一些局部变量/临时对象,而另一些则会存活很久(典型的,比如websocket长连接中的connection对象)。基本思想是将内存分成了三大块:年青代(Young Genaration),老年代(Old Generation),永久代(Permanent Generation),其中Young Genaration更是又细为分eden,S0, S1三个区。

刚开始时,对象分配在eden区,s0及s1区,几乎是空着的。当eden区放不下时,就会发生minor GC(也被称为young GC),第1步当然是要先标识出不可达垃圾对象,然后讲可达对象移到 s0 区。之后当 eden 区又满了之后,s0 和 eden 区的可达对象将会都移到 s1 区。之后 s0 和 s1 区的对象会相互移来移去,每移动 1 次,他们的年龄会 +1。所以当它们的年龄到达一定区域之后,将会移到老年代。如果老年代也满了,那么将会移到永久代。

  • 新生代使用:复制算法
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

垃圾收集器

https://www.jianshu.com/p/b572f69a1b93

**新生代垃圾收集器有Serial、ParNew、Parallel Scavenge,G1,属于老年代的垃圾收集器有CMS、Serial Old、Parallel Old和G1。**其中的G1是一种既可以对新生代对象也可以对老年代对象进行回收的垃圾收集器。然而,在所有的垃圾收集器中,并没有一种普遍使用的垃圾收集器。在不同的场景下,每种垃圾收集器有各自的优势,如下图:

  • 「Serial收集器」

**单线程垃圾收集器,**这就意味着在其进行垃圾收集的时候需要暂停其他的线程。

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

  • 「ParNew收集器」

理解为**Serial收集器的多线程版本,由于存在线程切换的开销,**ParNew在单CPU的环境中比不上Serial(ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数)。

收集过程:暂停所有线程 算法:复制算法 优点:在CPU多的情况下,拥有比Serial更好的效果。单CPU环境下Serial效果更好 应用:许多运行在Server模式下的虚拟机中首选的新生代收集器

  • 「Parallel Scavenge 收集器」

类似ParNew收集器,**Parallel收集器更关注系统的吞吐量。**区别在于Parallel Scavenge收集器更关注可控制的吞吐量(「吞吐量 = 运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)」)。吞吐量越大,意味着垃圾收集的时间越短,则用户代码则可以充分利用CPU资源,尽快完成程序的运算任务。

-XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间,-XX:GCRatio 直接设置吞吐量的大小。

-XX:+UseAdaptiveSizePocily 来动态调整停顿时间或者最大的吞吐量,这种方式称为GC自适应调节策略,这点是ParNew收集器所没有的。

  • 「Serial Old收集器」

「Serial Old收集器是Serial收集器的老年代版本」,也是一个单线程收集器,采用“「标记-整理算法」”进行回收。其运行过程与Serial收集器一样。

  • 「Parallel Old收集器」

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“「标记-整理」”算法进行垃圾回收。

通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。

  • 「CMS 收集器」

CMS(Concurrent Mark Sweep)收集器是一种「以获取最短回收停顿时间为目标的收集器」。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

「基于“标记-清除”算法实现的」,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

其中**初始标记、重新标记这两个步骤仍然需要“Stop The World”。**初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

「由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。」

优缺点:

  • 并发收集、低停顿
  • 产生大量空间碎片、并发阶段会降低吞吐量
  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

「G1 收集器」(整个Java堆:包括新生代和老年代)

G1 的特点是:采用并发与并行、空间整合(整体上类似标记-整理方法,不会产生内存碎片)、分代收集、「可预测的停顿」(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)。

G1收集器将Java堆划分为多个大小相等的Region(独立区域),新生代与老年代都是一部分Region的集合,G1的收集范围则是这一个个Region。

整个工作流程:初始标记、并发标记、最终标记、筛选回收。初始标记阶段仅仅只是标记一下GC Roots能够直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户程序并发运行的时候,能在正确可用的Region中创建对象,这个阶段需要暂停线程。并发标记阶段从GC Roots进行可达性分析,找出存活的对象,这个阶段是与用户线程并发执行的。最终标记阶段则是修正在并发标记阶段因为用户程序的并发执行而导致标记产生变动的那一部分记录,这部分记录被保存在Remembered Set Logs中,最终标记阶段再把Logs中的记录合并到Remembered Set中,这个阶段是并行执行的,仍然需要暂停用户线程。最后在筛选阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划。

内存分配策略

  • 对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。

  • 大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

  • 长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

  • 动态对象年龄判断

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

  • 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

巨人的肩膀

https://github.com/CyC2018/CS-Notes

本文转载自微信公众号「多选参数」,可以通过以下二维码关注。转载本文请联系多选参数公众号。

 

责任编辑:武晓燕 来源: 多选参数
相关推荐

2021-04-29 11:18:14

JVM加载机制

2010-12-10 15:40:58

JVM内存管理

2010-09-26 16:42:04

JVM内存组成JVM垃圾回收

2010-09-26 13:23:13

JVM内存管理机制

2020-07-09 12:50:29

JVM内存管理Java

2019-12-10 08:59:55

JVM内存算法

2010-09-27 13:26:31

JVM内存管理机制

2017-05-04 13:11:28

深度学习AI

2017-05-05 09:13:07

深度学习AI决策树

2020-11-06 07:11:40

内存虚拟Redis

2009-07-09 09:47:26

Sun JVM

2010-08-04 13:30:07

Visual Stud

2012-05-15 02:04:22

JVMJava

2017-09-20 08:48:09

JVM内存结构

2015-07-16 15:16:41

内存泄露解决办法

2011-12-20 10:43:21

Java

2010-09-25 15:40:52

配置JVM内存

2012-01-11 10:45:57

JavaJVM

2023-11-19 23:29:22

Heap DumpJava

2010-09-25 15:52:27

JVM内存JVM
点赞
收藏

51CTO技术栈公众号