JVM内存分配和垃圾回收

一、JVM堆分代

1、JVM堆被分为了 年轻代老年代。年轻代的GC过程称为Yong GC,速度快较频繁。老年代的GC过程称为Full GC,速度较慢应该尽量避免。

2、对象被创建后,除了少部分大对象会在老年代分配内存外,大部分的对象首先都是在年轻代进行内存分配,而且大部分的对象都是”朝生夕死”,很快就会被年轻代的Yong GC回收掉。

3、老年代的内存空间一般会比年轻代的内存空间大,能存放的对象多,老年代的空间不足后会进行Full GC操作,比Yong GC耗时,所以应尽量避免频繁的Full GC操作。

二、年轻代的分区

1、年轻代中分为一个Eden区和两个Surviver区,比例为8:1:1,两个Surviver区分别称为”From”区和”To”区。对象在Eden区创建,经过一次Yong GC后,还存活的对象将会被复制到Surviver区的”From”区,此时”To”区是空的。到了下一次GC的时候,Eden区还存活的对象会复制到Surviver区的”To”区,而”Form”区的对象有两个去处,”From”区的对象会根据经过的GC次数计算年龄,如果年龄到达了阈值(默认15),则会被移动到老年代中,否则就复制到”To”区,此时”From”区变成了空的,然后”From”区和”To”区进行角色互换,到下一次进行GC时,还是有一块空的”To”区,用来存放从eden区和”From”区移动过来的对象。

2、那这种分区有什么好处呢?

a、在年轻代新增Surviver区,有利于减轻老年代的负担,尽可能的让大部分对象在年轻代通过较高效的Yong GC回收掉,不至于老年代里存放的对象过多导致内存不足而进行频繁的Full GC操作。

b、这种分区有利于减少内存碎片的产生。

首先我们来看看,如果年轻代只分为Eden区和Surviver区两个区域并且比例是8:2的时候,内存的回收和分配情况会怎么样。第一次Yong GC后,Eden区还存活的对象移动到Surviver区,Surviver区还存活的对象保留在Surviver区,而这些对象的内存是不连续的,Surviver区里就会产生很多内存碎片,这就会导致有些大对象要移动到Surviver区的时候,没有足够的连续内存进行分配,而不得不移动到老年代中,增加老年代的负担,降低效率。

然后我们看看Eden区和Surviver区的比例是8:1:1时会有什么样的效果。第一次Yong GC后,Eden区还存活的对象复制到Surviver区的”To”区,”From”区还存活的对象也复制到”To”区,再清空Eden区和From区,这样就等于”From”区完全是空的了,而”To”区也不会有内存碎片产生,等到第二次Yong GC时,”From”区和”To”区角色互换,很好的解决了内存碎片的问题。

如下图所示:

JVM内存分配和垃圾回收

三、内存分布

JVM内存分配和垃圾回收

对象一般在堆上分配,但JVM支持一种在栈上分配内存的机制。

通过-XX:+DoEscapeAnalysis开启逃逸分析(默认开启),JVM会针对不会逃逸的对象分配在栈上。好处是,栈可以自动弹出,不需要垃圾回收参与处理这些对象。

此外TLAB(Thread Local Allocation Buffer)是一个线程独占的堆空间。一般的堆空间是共享的,在内存分配时,多个线程需要同步,但TLAB区域由于线程独占,所以不必在分配内存时进行同步。TLAB本身占用eden区域。

关于逃逸分析和TLAB参见jvm 优化篇-(4)-栈上分配与逃逸分析 -XX:+DoEscapeAnalysis -XX:+UseTLAB -XX:TLABRefillWasteFraction

三、分代算法

现代垃圾回收的基本算法是标记清除(Mark-Sweep),但依然要面临内存碎片的问题。JVM采用分代机制解决内存碎片问题。

新生代采用复制算法。新生代的特点是,大部分对象是可以回收的。新生代区进一步分为eden区、from区、to区。from区和to区是两块大小相同内存区域,有时也叫S0/S1,作用是交换存活对象。新生代内存分配发生在eden区。当新生代需要垃圾回收时,假设此时S0中是上一次GC留下来的存活对象,那么eden中的存活对象和S0中的对象都将复制到S1(并对齐),然后eden和S0可以直接清空;下一次垃圾回收时,eden和S1的存活对象复制到S0(并对齐)。所以说S0和S1相互交换存活对象。如果S0和S1无法容纳对象,那么部分对象将进入老年代区。由于新生代中大部分对象是可以回收的,所以采用这种复制算法压缩内存最为高效。

老年代采用标记压缩算法。因为老年代活动对象多,垃圾对象少

四、分区算法

G1采用分区算法。分区的思想是将推内存划分为多个区,如果每次只收集若干区域,而不是整个堆,可以有效的控制停顿时间。

垃圾回收器的发展

垃圾回收器经历了串行、并行(多线程)、并发(不阻塞应用)的发展。参考[深入JVM读书笔记(四)——Java的垃圾收集器]

  • Serial/Serial Old: 串行收集器,收集过程会阻塞应用程序线程,并用单线程完成收集
  • ParNew: 适用于新生代的并行收集器,收集过程会阻塞应用程序线程,并用多个线程完成对新生代区的收集
  • Parallel Scavenge: 与ParNew类似,但可以调节停顿时间和吞吐等参数
  • Parallel Old: 针对老年代的并行收集
  • CMS: 针对老年代,可针对某些收集阶段支持并发收集。意味着某些情况下可以不阻塞应用程序运行
  • G1: 1.7开始引入,同时具有并行、并发能力,同时支持新生代和老年代,并采用分区的思想控制停顿时间。当Java堆非常大的时候,G1的优势更加明显

G1

G1收集器的总体效果是好于CMS的,有更好的自我调节能力而G1从JDK9开始才是默认垃圾回收器。所以JDK8的情况下,最好主动设置G1垃圾回收器:

-XX:+UseG1GC

G1收集器用Region来划分内存,虽然逻辑上依然保留新生代和老年代,但是新生代和老年代是由若干Region组成的,并且并不一定要求连续。每个分区Region也不会确定地为某个代服务,可以按需在新生代和老年代之间切换。

18.657: [GC pause (G1 Evacuation Pause) (young) (initial-mark) 26M->24M(32M), 0.0025448 secs]18.659: [GC concurrent-root-region-scan-start]18.660: [GC concurrent-root-region-scan-end, 0.0008815 secs]18.660: [GC concurrent-mark-start]18.696: [GC concurrent-mark-end, 0.0357099 secs]18.696: [GC remark, 0.0037490 secs]18.703: [GC cleanup 24M->24M(32M), 0.0004163 secs]18.892: [GC pause (G1 Evacuation Pause) (young) 26M->25M(32M), 0.0027587 secs]19.014: [GC pause (G1 Evacuation Pause) (mixed) 26M->24M(32M), 0.0042025 secs]

上面是一段G1的gc日志

  • initial-mark: 初始标记,伴随一个新生代GC,有暂停
  • concurrent-root-region-scan-start/end: 根区域扫描,并发的无暂停
  • concurrent-mark-start/end: 并发标记,并发的无暂停
  • remark: 重新标记,有暂停
  • cleanup: 独占清理,有暂停
  • young: 新生代GC,有暂停
  • mixed: 同时有新生代和老年代GC,无暂停。在并发标记中得知哪些Region垃圾比例比较高,会在这个阶段对这些Region进行清理(Gargage First的由来)

并发标记可能被young gc和full gc打断,例如下面的日志展示了被full gc中断的concurrent-mark

G1的参数

选项说明 -XX:MaxGCPauseMillis 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标. -XX:InitiatingHeapOccupancyPercent 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示”一直执行GC循环”. 默认值为 45 -XX:ParallelGCThreads 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同

常见OOM

  • StackOverFlowError: 栈内存溢出,用于深度方法调用(循环递归)
  • OutOfMemoryError: Java heap space。用于变量申请的空间大于jvm的最大值
  • OutOfMemoryError: GC overhead limit exceed。GC回收的过长时会抛出OutOfMemoryError,过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出GC overhead limit 错误会发生什么情况?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果
  • OutOfMemoryError: Direct buffer memory。堆外内存溢出,主要呈现在写NIO程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
  • OutOfMemoryError: unable to create new native thread。应用创建了太多线程
  • OutOfMemoryError: Metaspace。元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存的限制。

jstat

jstat -gc <pid>  查看某个进程垃圾回收情况
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT1088.0 1088.0  0.0   117.2   8704.0   8641.6   21888.0    21888.0   24832.0 24320.1 2560.0 2340.1     26    0.134  13      0.810    0.944
  • S0C:第一个幸存区的大小
  • S1C:第二个幸存区的大小
  • S0U:第一个幸存区的使用大小
  • S1U:第二个幸存区的使用大小
  • EC:eden区的大小
  • EU:eden区的使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • MC:方法区大小
  • MU:方法区使用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • YGC: 年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC: 老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

jinfo

查看进程的jvm flag。例如,验证DoEscapeAnalysis默认是开启的

$ jinfo -flag DoEscapeAnalysis 6953-XX:+DoEscapeAnalysis

也可以动态修改部分参数

$ jinfo -flag +PrintGCDetails 6953

Original: https://www.cnblogs.com/lei-z/p/15903139.html
Author: 风光小磊
Title: JVM内存分配和垃圾回收

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/587175/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 查找Java maven项目,和reactJS项目的license

    ReactJS: npm install -g license-checker npm install -g yui-lint license-checker –pro…

    Java 2023年5月29日
    062
  • 面对对象

    Java基础-面对对象 面向过程&面对对象1 计算机语言发展史: 例题:两数的互换 面对过程与面对对象的区别: 面向过程: 当事件比较简单的时候,利用面向过程,注重的是事件…

    Java 2023年6月5日
    084
  • JavaWeb 03_创建servlet项目(详细)

    一、创建web项目 File–New–Project 设置项目相关信息 设置项目名称及工作空间 web项目目录结构如下 二、Servlet的实现 新建包&#…

    Java 2023年6月7日
    074
  • Javaweb10-javaweb其他知识点

    1、详解DefaultServlet与JspServlet 当服务端收到关于 Servlet的请求之后交由 自定义Servlet处理。 当服务端收到关于 静态资源的请求时交由 De…

    Java 2023年6月15日
    064
  • JUC的数据库连接池小练习

    JUC练习数据库连接池实现 通过一个连接数组来充当连接池 一个原子的标记数组 通过cas来保持多线程下的安全,用synchronized来进行暂停和唤醒 @Slf4j public…

    Java 2023年6月16日
    0100
  • SpringCloud微服务实战——搭建企业级开发框架(二十九):集成分布式对象存储服务及记录管理

    微服务应用中图片、文件等存储区别于单体应用,单体应用可以放到本地读写磁盘文件,微服务应用必需用到分布式存储,将图片、文件等存储到服务稳定的分布式存储服务器。目前,很多云服务商提供了…

    Java 2023年6月9日
    075
  • mybatis 字段类型映射一览表

    Original: https://www.cnblogs.com/tangzeqi/p/13155854.htmlAuthor: instrTitle: mybatis 字段类型…

    Java 2023年6月6日
    067
  • 面向对象(上)

    面向对象(上) java面向对象学习三条主线 * 1.java类及类的成员:属性、方法、构造器、代码块、内部类 * 2.面向对象的三大特征:封装,继承,多态 * 3.其他关键字:t…

    Java 2023年6月6日
    073
  • SpringBoot系列之从入门到精通系列教程

    本专栏基于Springboot2.2.3,配套自己写的代码例子,内容涉及配置用法,web,数据库,Redis,也涉及到企业级开发的消息队列,dubbo,单点登录,OAuth2,搜索…

    Java 2023年5月30日
    076
  • 修改Linux系统的网卡名称

    一、网卡配置文件名称重命名为eth0 二、禁用网卡命名规则 此功能通过/etc/default/grub文件来控制,要禁用此功能,在文件中加入 “net.ifnames…

    Java 2023年6月7日
    086
  • 并发编程之:线程池(一)

    大家好,我是小黑,一个在互联网苟且偷生的农民工。 池化 线程池是在计算机开发中常见的一种 池化技术,是为了提高资源的利用率,将一些资源重复利用,避免重复的构建来提高效率。类似字符串…

    Java 2023年6月7日
    079
  • CSharp: Iterator Pattern

    csharp;gutter:true; /// /// A simple file handlng class /// Iterator Patterns 迭代器模式 /// 20…

    Java 2023年6月16日
    060
  • 第一个HTML

    DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-…

    Java 2023年6月5日
    073
  • SpringBoot集成RabbitMQ简单举例

    操作步骤: 引入依赖包、配置连接参数、新建队列、生产者代码、消费者代码 依赖包 配置连接参数 新建队列 生产者代码 消费者代码 来源项目:dms-test-data Origina…

    Java 2023年5月30日
    062
  • 【springcloud】环境搭建与Rest使快速上手

    SpringCloud环境搭建 — Rest使用 个人主页:https://www.cnblogs.com/xbudian/ 今天来到SpringCloud的学习,我们…

    Java 2023年6月9日
    089
  • 【java多线程】jdk的线程池

    一、JDK的线程池的核心参数 int corePoolSize :线程池中的核心线程数 int maximumPoolSize :线程池中的最大线程数 long keepAlive…

    Java 2023年5月29日
    095
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球