转载

Java虚拟机-GC机制

垃圾回收(Garbage Collection,GC),在Java程序运行时,虚拟机会自动管理内存的分配和回收,保证内存处于可用状态。但有时也会出现内存泄漏(程序出错)和内存溢出(OOM)导致异常的出现,这需要我们了解虚拟机对内存的管理机制,快速的定位问题。

目录

  1. 概念
  2. 垃圾回收的标准
  3. 垃圾回收算法
  4. 垃圾回收器
  5. GC相关案例
  • 概念

1. JVM运行时数据区

常见的内存划分为:虚拟机栈、本地方法栈、程序计数器、方法区、堆

Java虚拟机-GC机制

对于方法区,jdk1.7之前,也成“永久代”(PermGen space),用于存储虚拟机加载的类信息、常量、静态变量和Class文件等数据,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M。但随着自定义类加载器增多,永久代的内存会出现OOM的异常且不易调节。

jdk1.7时,将字符串常量池从方法区(永久代)中移除至堆等,各部分的迁移是小版本的不断迭代。

jdk1.8时,废除永久代概念,新增类的“元数据”(metadata)概念,其存放于叫做“Metaspace”的本地内存(Native memory)中,优点是充分利用本地内存,减少OOM出现,按类加载器回收空间

2. 堆中对象分配内存的方式

指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存放一边,空闲的内存放另一边,中间放着一个指针作为分界点的指示器,所分配内存就仅仅是把哪个指针向空闲空间那边挪动一段与对象大小相等的举例,这种分配方案就叫指针碰撞

空闲列表:有一个列表,其中记录中哪些内存块有用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,然后更新列表中的记录,这就叫做空闲列表。

根据垃圾回收策略的的选择,Serial、ParNew等复制算法的回收器,采用指针碰撞;CMS等Mark-Sweep算法的收集器,采用空闲列表。

3. 对象的访问

建立对象是为了使用对象,我们的Java程序需要通过栈上的reference引用来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方法也是取决于虚拟机的实现而决定的。目前主流的访问方式有 使用句柄直接指针 两种。

句柄访问:reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变。缺点是增加了一次指针定位的时间开销。

Java虚拟机-GC机制

直接内存访问(HotSpot采用):Java堆对象布局中需要考虑如何存放对象类型相关信息,reference中直接存放对象地址,这种最大的好处就是速度更快,节省一次指针定位的开销,但在对象被移动时reference本身需要被修改。

Java虚拟机-GC机制

对象在内存布局:对象头(自身运行时数据+类型指针)、实例数据、对齐填充

  • 垃圾回收的标准

判断对象是否可被回收:标记并记录被引用的对象,清除其余对象。

1. 引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,这也就是需要回收的对象。

Java虚拟机-GC机制

优点:不需要集中进行GC,随时回收引用为0的垃圾,减少停顿时间(是目前垃圾回收的追求的)。

缺点:循环引用,导致内存泄漏。但也有一些办法来应对, 例如 “弱引用”(‘weak’ references), 或者使用另外的算法来排查循环引用等,华为的方舟编译器采用引用计数法。

card.weibo.com/article/m/s…

四种引用类型:

回收策略 作用
强引用 Object obj = new Object() 强引用存在,永远不会被回收
软引用 SoftReference softRef = new SoftReference(new String("abc"); 在系统要发生内存溢出之前,将其列为回收对象 内存敏感的高速缓存
弱引用 WeakReference weakRef = new WeakReference(new String("abc")); 存活至下一垃圾回收 缓存
虚引用 ReferenceQueue queue = new ReferenceQueue(); PhantomReference ref = new PhantomReference<>(new String("abc"), queue); 没有实际引用关系,设置虚引用的目的是为了对象被回收时收到系统通知

2. 可达性分析

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到GC Roots没有任何的引用链相连时(从GC Roots)到这个对象不可达)时,证明此对象不可用。

Java虚拟机-GC机制
  • 垃圾回收算法

1. 标记-清除算法

标记-清除算法分为两个阶段:标记阶段和清除阶段。

标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。

实现简单,但容易产生内存碎片,导致为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

Java虚拟机-GC机制

2. 复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

但会浪费一部分内存空间。

Java虚拟机-GC机制

3. 标记-整理算法

该算法标记阶段和标记-清除一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

Java虚拟机-GC机制

4. 分代回收算法

针对不同的年代进行不同算法的垃圾回收, 针对新生代选择复制算法,对老年代选择标记整理算法或标记-清除算法

Young GC(又称Minor GC),新生代内存的垃圾收集事件

Old GC,只清理老年代空间的GC事件,只有CMS的并发收集是这个模式

Full GC,清理整个堆的GC事件,包括新生代、老年代、元空间等

Mixed GC,清理整个新生代以及部分老年代的GC,只有G1有这个模式

新生代分为 Eden 和 两个 Survivor 区域,默认Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )

Java虚拟机-GC机制

内存分配策略:

a. 对象优先在Eden区分配大多数情况下,对象在先新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Young GC

b. 大对象之间进入老年代JVM提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为0,代表不管多大都是先在Eden中分配内存),大于参数设置的阈值值的对象直接在老年代分配,这样可以避免对象在Eden及两个Survivor直接发生大内存复制

c. 长期存活的对象将进入老年代对象每经历一次垃圾回收,且没被回收掉,它的年龄就增加1,大于年龄阈值参数(-XX:MaxTenuringThreshold,默认15)的对象,将晋升到老年代中

d. 动态年龄判定新生代对象的年龄可能没达到阈值(MaxTenuringThreshold参数指定)就晋升老年代,如果Young GC之后,新生代存活对象达到相同年龄所有对象大小的总和大于任一Survivor空间(S0 或 S1总空间)的一半,此时S0或者S1区即将容纳不了存活的新生代对象,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

e. 空间分配担保(新生代复制算法需要担保) 当进行Young GC之前,JVM需要预估:老年代是否能够容纳Young GC后新生代晋升到老年代的存活对象,以确定是否需要提前触发GC回收老年代空间,基于空间分配担保策略来计算

  • 垃圾回收器

Jvm有client和server两个版本,分别针对桌面应用程序和服务端应用做了相应的优化,client版本加载速度较快,分配内存空间小,server版本加载速度较慢但运行起来较快,充分利用服务器资源。 我们可以通过运行:java -version来查看jvm默认工作在什么模式

几个概念:

Stop The World:GC过程中分析对象引用关系,为了保证分析结果的准确性,需要通过停顿所有Java执行线程,保证引用关系不再动态变化,该停顿事件称为Stop The World(STW)

Safepoint:代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要GC,线程可以在这个位置暂停。HotSpot采用主动中断的方式,让执行线程在运行期轮询是否需要暂停的标志,若需要则中断挂起

Java虚拟机-GC机制

1. Serial

单线程,适用于新生代,采用复制算法。适用于Client模式,内存不会很大,很快可以清除完,省去线程切换开销

Java虚拟机-GC机制

2. ParNew

Serial的多线程版本(复用大多数代码),适用于新生代,Server模式,与jdk1.5时出现的CMS老年代回收搭配使用

Java虚拟机-GC机制

3. Paraller Scavenge(注重吞吐量)

响应速度(Responsiveness)响应速度指程序或系统对一个请求的响应有多迅速。比如,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。调优的重点是在短的时间内快速响应

吞吐量(Throughput)吞吐量关注在一个特定时间段内应用系统的最大工作量,例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的GC停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求

举例:为了减少停顿时间,原来新生代GC为10秒一次,一次100ms,现在调小新生代空间,5秒一次,一次70ms,这样就提升了响应速度,降低了吞吐量。

jdk1.4出现,多线程版本,因为实现框架不同,故不能与CMS一起用,而Paraller Old在1.6才出现,所以当时Paraller Scavenge+Serial old的组合效果并不好

-XX:UseAdaptiveSizePolicy:打开之后,就不需要设置新生代大小(-Xmn),Edian,survivor比例及(-XX:SurvivorRatio)晋升老年代年龄(-XX:PretenureSizeThreshold),虚拟机根据系统运行状况,调整停顿时间,吞吐量, GC自适应调节策略,区别parNew。

4. Serial old

单线程老年代回收

5. Paraller Old

jdk1.6版本出现,注重吞吐量,多线程版本

6. CMS

多线程回收老年代空间,是一种追求最短回收停顿时间为目标的收集器(因为需要与工作线程并发,占用CPU资源多)。基于标记-清除算法。

Java虚拟机-GC机制

标记-清除注定了会有不连续空间出现,导致不能分配大的空间而提前发生一次Full GC。可通过参数CMSFullGCsBeforeCompaction的值,设置多少次Full GC触发一次压缩,默认值为0,代表每次进入Full GC都会触发压缩,带压缩动作的算法为上面提到的单线程Serial Old算法,暂停时间(STW)时间非常长,需要尽可能减少压缩时间。

CMS无法处理浮动垃圾,即并发清理阶段产生的不能被标记的垃圾,这需要预留一部分空间,但如果预留不够,则会出现Concurrent MarkFailure,这是虚拟机会启动后备方案,采用Serial Old收集器来收集,停顿时间会很长。

7. G1(Java9默认垃圾回收器)

G1(Garbage-First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器,G1最主要的设计目标是: 实现可预期及可配置的STW停顿时间

Java虚拟机-GC机制

为实现大内存空间的低停顿时间的回收,将划分为多个大小相等的Region。每个小堆区都可能是 Eden区,Survivor区或者Old区,但是在同一时刻只能属于某个代。在逻辑上, 所有的Eden区和Survivor区合起来就是新生代,所有的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,不断变动。

每次GC不必都去处理整个堆空间,而是每次只处理一部分Region,实现大容量内存的GC。通过计算每个Region的回收价值,包括回收所需时间、可回收空间, 在有限时间内尽可能回收更多的垃圾对象 ,把垃圾回收造成的停顿时间控制在预期配置的时间范围内,这也是G1名称的由来:** garbage-first**

针对新生代和老年代,G1提供2种GC模式,Young GC和Mixed GC,两种会导致Stop The World。

全局并发标记主要是为Mixed GC计算找出回收收益较高的Region区域,具体分为5个阶段

Java虚拟机-GC机制

每个Region初始化时,会初始化一个remembered set(已记忆集合),如Region1和Region3中有对象引用了Region2的对象,则在Region2的Rset中记录了这些引用。空间换时间。

Java虚拟机-GC机制

8. 查看jvm用的垃圾回收器

java -XX:+PrintCommandLineFlags -version

java -XX:+PrintGCDetails -version

Java虚拟机-GC机制
  • GC相关案例

常用命令:

Jstat:监视虚拟机运行时的状态信息,包括监视类装载、内存、垃圾回收、jit编译信息

jstack:生成线程快照,定位线程长时间停顿的原因

工具JConsole:JConsole是一种基于JMX的可视化监视、管理工具可进行内存管理、线程管理、查看死锁等。

1. FastJson漏洞导致OOM

原因:fastjson < 1.2.60版本,代码bug

cert.360.cn/warning/det…

2. 数据分析平台系统频繁Full GC

原因:内存分配调优

平台主要对用户在APP中行为进行定时分析统计,并支持报表导出,使用CMS GC算法。数据分析师在使用中发现系统页面打开经常卡顿,通过jstat命令发现系统每次Young GC后大约有10%的存活对象进入老年代。

解决:因为Survivor区空间设置过小,每次Young GC后存活对象在Survivor区域放不下,提前进入老年代,通过调大Survivor区,使得Survivor区可以容纳Young GC后存活对象,对象在Survivor区经历多次Young GC达到年龄阈值才进入老年代,调整之后每次Young GC后进入老年代的存活对象稳定运行时仅几百Kb,Full GC频率大大降低

Java虚拟机-GC机制

3. 系统处理大文件导致卡顿

背景:绩效考核系统,会针对每一个考核员工生成一个各考核点的考核结果,形成一个Excel文档,供用户下载。文档中包含用户提交的考核点信息以及分析信息,Excel文档由用户请求的时候生成,下载并保存在内存服务器一份。64G内存。

问题:经常有用户反映长时间卡顿的问题。

监控内存发现经常发生Full GC 20-30s 运行时产生 大对象 (每个教师考核的数据WorkBook),直接放入老年代,MinorGC不会去清理,会导致FullGC,且堆内存分配太大,时间过长。

解决方案:部署多个web容器,每个web容器的堆内存4G,单机TomCat集群。

原文  https://juejin.im/post/5d9f4b9de51d45783a772a06
正文到此结束
Loading...