JVM调优、JVM内存模型

在JVM中,JVM内存模型无疑是一块非常重要的内容,理解JVM内存模型对于我们进行JVM调优有很大的帮助。

本篇博客将会介绍JVM内存模型的组成部分,其中堆将会重点进行介绍,因为它是我们JVM调优的主要场所,而至于JVM内存模型中的其他部分我可能只会简单的介绍一些。

JVM内存模式简介

在这里插入图片描述

JVM虚拟机主要由三部分组成
• 类装载系统:负责类的加载。类的加载也是一块很大的内容,但不在今天的讨论范围内
• 字节码执行引擎:负责执行代码,以及修改程序计数器
• JVM内存模型:这是我们今天讨论的重点

其中JVM内存模型由五部分组成,下面我们分别介绍一下:

在JAVA每个线程都会被分配一个栈空间,我们每调用一个方法,就会往栈中压入一个栈帧,方法执行完后,就从栈中弹出一个栈帧。栈帧主要由局部变量、操作数栈、动态链接、方法出口组成。

在上图中,紫色部分都是线程私有的,而灰橙色部分则是线程共享的。

本地方法栈

本地方法栈其实和栈基本上是一样的,只不过它是本地方法使用的栈。

本地方法:在JAVA中有一些native关键字修饰的方法,这就是本地方法,这些本地方法实际上调用的都是C语言写的代码。

方法区

方法区主要存放一些静态变量、常量、类元信息(字节码文件中的内容)。

方法区在JDK1.8之前的实现是永久代,永久代使用的内存还是JVM的内存。而在JDK1.8后方法区的实现是元空间,元空间使用的是物理机的内存,而不在是虚拟机的内存。

方法区与永久代、元空间的关系就像是接口与实现类的关系,方法区是接口,而永久代和元空间是不同的实现类。

程序计数器

程序计数器也是每个线程私有的,程序计数器当前线程执行代码的地址,一旦发生线程上下文切换时,就需要借助程序计数器从切换前的代码继续往下执行。

堆,我们创建的对象大部分都是分配在堆中的,而堆也是JVM垃圾回收的主要场所。

在介绍堆的组成之前,先简单的介绍一下垃圾回收。在其他语言,如C语言中,使用完对象是需要程序员手动释放对象占用的空间的,而在JAVA中,对象空间的释放是由JVM帮我们做的,这个释放对象空间的操作就叫做垃圾回收。简单来说,就是JVM帮我们找到哪些对象是垃圾对象(不在使用的对象),然后回收这些对象。但因为JVM在进行垃圾回收前,会STW(stop the world)停止所有的用户线程,体现在用户的感知中就是程序卡顿了,因此我们应该减少垃圾回收的频率,这也是我们JVM调优要做的事。
在这里插入图片描述
堆主要可以分为俩部分,年轻代与老年代。默认情况下,老年代栈整个堆大小的三分之二,而年轻代占整个堆大小的三分之一。

年轻代,又由三部分组成伊甸园区(Eden)、Survivor0、Survivor1,默认情况下他们的比例是8:1:1
打个比方,对于一个3G大小的堆来说,老年代可能占2GB,而年轻代的Eden占800MB,S0和S1各占100MB。
在这里插入图片描述

minor gc和full gc

当年轻代的Eden区被放满时,会触发垃圾回收,我们将发生在年轻代的垃圾回收称为minor gc或者young gc,minor gc只会回收年轻代的垃圾对象,相对来说STW的时间较短。

当老年代的被放满,触发的垃圾回收称为full gc。full gc会回收年轻代、老年代、元空间中的垃圾对象。由于回收的范围较广,所以STW的时间也会较长,因此我们需要尽量减少full gc的频率

对象的分配

通常,我们创建的对象都是直接分配在Eden区的。

  1. 当Eden区第一次被放满的时候,触发minor gc,会将Eden区中还存活的对象复制到S0,同时将这些对象年龄+1,然后清空Eden
  2. 当Eden区第二次被放满时,触发minor gc,会将Eden区和S0中还存活的对象复制到S1,同时将这些对象年龄+1,然后清空S0区
  3. 当Eden区第三次被放满时,触发minor gc,将Eden区和S1区中还存活的对象复制到S0中,同时将这些对象年龄+1,然后清空S1
  4. 依此反复,重复2、3

在一轮minor gc中存活下来的对象,年龄会加1,默认情况下,当一个对象年龄达到15岁,也就是经历了15轮minor gc后还能够存活的对象,会被移动到老年代。而一旦老年代被放满,就会触发我们之前所说的full gc

哪些场景下对象会进入老年代

因为我们说过,JVM调优的目的就是为了减少full gc的频率。而老年代放满会触发full gc,因此,我们需要清楚除了上面所说的年龄到底15岁会进入到老年代外,还有哪些场景对象会进入到老年代。

大对象进入老年代

默认情况下,如果一个对象大到整个Eden区都放不下,那么这个对象就直接被放入到老年代。当然如果我们使用的是Serial和ParNew俩个垃圾收集器,可以通过-XX:PretenureSizeThreshold指定大对象的大小。如,指定超过1MB的对象就是大对象。

在对我们的系统比较熟悉的情况下,我们是可以指定大对象的大小的,如果这些大对象是长期存活的话。因为这么做可以避免频繁复制大对象带来的性能损耗。以及大对象也会让我们的GC变得更为频繁。

动态年龄判断

如果Eden存活的对象过多,导致Survivor都放不下的话,那么这些对象也会被放入到老年代中。到实际上,他还有一个动态年龄判断,如果存活的对象超过Survivor区的大小的一半,那么就会有一批对象提前进入到老年代,即使他们的年龄还没有到达15岁。

比如S0的大小是100MB,Eden区经过垃圾回收后,还有60MB的对象存活,超过了S0总大小的一半。

假如年龄1+年龄2+年龄3+年龄4+。。。年龄8这些对象加起来大小是50MB,那么年龄大于等于8的这些对象就会提前进入到老年代。

哪些场景会触发full gc

其实除了,我们上面所说的老年代放满了会触发full gc外,还有几种场景也会触发full gc。

元空间动态扩容

前面我们说过,在JDK1.8后方法区的实现是元空间,元空间使用的是我们物理机的内存。理论上 ,元空间的内存容量与我们物理机的容量一样大,但实际上JVM提供了俩个参数来控制元空间的内存大小,分别是:

  1. -XX:MetaspaceSize:元空间的初始大小。以字节为单位,默认是21M,当达到该值时,就会触发full gc。同时会动态调整该值,如果full gc释放了大量的空间,就会适当降低该值。反之,就会适当提升该值
  2. -XX:MaxMetaspaceSize:元空间的最大大小
    元空间默认的21M是很容易达到,因此为了避免元空间带来的full gc,建议将MetaspaceSize的值设置成和MaxMetaspaceSize相同。比如对于8G的物理内存机器来说,一般可以将这个值设置为256M。

老年代担保机制

这个机制简单了解一下,可能会相对比较复杂一丢丢。

其实,JVM在年轻代满的时候,并不会立马进行minor gc,而是会检查一下当前年轻代中的对象总大小(包括垃圾对象)是否大于老年代的剩余可用空间,如果大于的话,会直接进行full gc。

上面说的是没开启老年代担保机制的场景。开启老年代担保机制后,它并不会直接进行full gc,而是会看前几次minor gc后平均存活下来的对象总大小是否小于当前老年代的剩余可用空间,如果否的话,那么才会直接进行full gc。
而且,它进行full gc后,还是在进行依次minor gc的,只不过由于进行过full gc了,minor gc会很快完成。

流程图大概如下:
在这里插入图片描述
我们通过-XX:-HandlePromotionFailure参数可以开启老年代担保机制,JDK1.8默认已经开启了这个参数。

到这里,JVM内存模型已经介绍得差不多了,但你可能心里有个疑惑,既然STW会导致用户体验不好,那么为什么垃圾回收时,还要做STW呢?这里我讲一下我的理解:

  1. 如果垃圾回收的过程中,不做STW,而是垃圾回收线程与用户线程并行执行,那么可能上一秒被标记的不是垃圾的对象,随着用户线程的执行变成了垃圾对象,相当于前面做的都是无用功。
  2. 用户线程在执行的过程中,是会不断的制造垃圾的,理论上,到制造垃圾的速度与寻找垃圾的速度持平的时候,那么就不能及时进行垃圾回收,释放空间,从而导致OOM。我觉得这点才是最关键的。

其实JVM有非常多种垃圾回收器,而这些垃圾回收器的升级迭代,目的也为了减少尽可能的STW的时间,从而提升用户的体验。目前的垃圾回收器已经很强大了,同时也非常复杂,后面专门写一篇详细介绍。

JVM参数的设置思路

通过上面的介绍,相信你已经对JVM调优要做的事情有一定的认识了。接下来,利用一个小例子巩固一下:
在这里插入图片描述

下面提到的数据都是估算的,不要太纠结,主要是理解思路

  1. 现有一个每日50万比订单的系统,根据二八法则,假设大部分的订单都是三四个小时内完成的。那么可能每秒也就几十单。几十单分摊到多台机器中,每台机器每秒也就处理十几单,压力不大,所以不是我们讨论的主要范围
  2. 假如是大促或者其他活动,大部分的订单集中在前几分钟内完成。那么每秒就需要处理一千多比订单。分散到多台机器,假设每台机器每秒处理300笔订单
  3. 假设每笔订单1kb,那么每秒就会产生300kb的订单对象。由于下单时,还会涉及到其他对象,因此我们放大20倍,每秒生成6MB的对象,一个程序除了下单外,还会有其他的操作同步进行,如订单查询等,因此我们将这个大小在放大10倍
  4. 最终,每秒会有60MB的对象产生,放入堆中

假如,我们目前机器使用的JVM参数如下:

java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar order-server.jar

• -Xms:堆的初始化大小,这里设置为3GB。这个值通常与Xmx设置为相同
• -Xmx:堆的最大大小,同样是3GB
• -Xss:线程栈的大小,1MB。默认就是1MB
• MetaspaceSize、MaxMetaspaceSize:元空间的初始大小和最大大小,都为512MB

对应的JVM内存模型如下:
在这里插入图片描述

根据我们的分析,每秒会有60MB的对象被放入Eden区,大概14秒左右的时间,Eden区就会被放满,而触发minor gc。

触发minor gc时,最后一秒的对象由于请求还没处理结束,这些对象还没有变成垃圾对象,因此在minor gc后会被放入到Survivor区。

但是注意,我们的S区只有100MB的空间,存活的对象有60MB,超过了S区的一半,根据动态年龄判断,这些对象会直接进入到老年代。也就是说每14秒就会有60MB的对象进入到老年代。这样的操作来个34次左右,老年代就会半放满,也就是说每8分钟左右老年代就会被方法,从而促发full gc。

但我们仔细观察,就会发现,其实这60MB的对象都是马上会变成垃圾的对象,他们本应该在年轻代中的minor gc中被回收,却因为动态年轻判断而进入到了老年代,最终导致full gc。为此,我们应该调整S区的大小,让它不会促发动态年龄判断,修改后的JVM参数如下:

java -Xms3072M -Xmx3072M -Xmn2048MB  -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar order-service.jar

我们通过Xmn参数,指定年轻代占2GB的空间,现在我们的JVM内存模型图如下:

在这里插入图片描述
60MB小于204MB的一半,这样就不会触发动态年龄判断,这些对象也会在minor gc的时候被回收。通过这次优化,我们的程序将会基本不发生full gc,从而提升了用户的体验。

总结一下,我们调优的目的就是为了让对象尽可能的在年轻代中就被回收掉,而不是进入老年代,最终导致full gc。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值