JVM系列二:jvm内存模型与实战jvm调优demo

前言

一、jvm内存模型结构图

在这里插入图片描述
顺带贴一下One.java文件的内容

public class One {

    public int sum() {
        int a = 3;
        int b = 10;

        return (a + b) * 5;
    }

    public static void main(String[] args) {
        One one = new One();
        System.out.println(one.sum());
    }
}

二、线程私有区域

  • 线程私有的区域包含:程序计数器、线程栈、本地方法栈,其中线程栈内部存储的是栈帧,其中栈帧又包括局部变量表、操作数栈、动态链接、方法出口。说的有点绕,接下来将以表格的方式来呈现。
  • 整理下jvm线程私有的内存结构
    类目作用备注
    程序计数器类似于pc寄存器,用来存储下一步jvm要处理的指令
    本地方法栈jvm中(eg: hotspot)的原生方法,eg: UNSAFE类中的方法
    栈帧线程每调用一个方法都会以栈帧的方式存储在栈中
    栈帧-局部变量表存储方法内部定义的变量1. 方法中具体定义的变量名,在内部都不会存在,jvm不在乎你的变量名是什么
    2. 当执行store相关指令时,会将变量存储到局部变量表中
    栈帧-操作数栈临时存储方法内部定义的变量1. 当执行const相关的指令时,会将变量临时存储到操作数栈中
    2. 当执行load相关指令时,会将局部变量表中的变量移动到操作数栈中
    栈帧-动态链接java中多态的机制就是靠它来完成的
    栈帧-方法出口栈帧执行结束的出口方法结束的出口一共有两个:
    1. 正常return
    2.方法出异常

三、线程间共享的区域

  • 线程间共享的区域包含:堆和本地方法区,其中堆中存储的就是我们经常new出来的对象,而本地方法区存储的就是我们经常写的常量、静态变量、类信息。其中,我们最需要关注的区域就是,因为jvm调优就是针对于区域进行调优的。关于的内存结构,我也画了两幅图来总结:

在这里插入图片描述
在这里插入图片描述
再啰嗦一下:堆分为新生代和老年代,是按1:2的比例分配堆内存的。新生代分为eden区、survivor from区、survivor to区,分别按8:1:1的比例分配新生代内存。上图不仅仅是画出了堆内存的结构,还描述了jvm何时触发minor gc何时触发full gc,以及jvm的一些内置条约,应该要好好消费这张图。

四、以一个亿级流量的项目来实战jvm调优

  • Demo背景:亿级流量电商(每日用户点击上亿次)

    每日用户点击上亿次,假设一个用户在购买东西的过程中,每个用户可能会按这样的步骤进行购物:浏览商品、看评价、咨询商家、加购物车、下单。平均下来可能每个用户点击二三十次(平均25次)。进而推算此电商项目的用户量为:100000000 / 25 = 400万。假设每个用户按照上面的流程走,最终支付的用户只占10%。也就是每日有40万的下单量。

  • 上述的项目背景已经知道了,每天正常的下单量为40万。一天有24个小时,这40万个订单肯定不会在某一个集中的时间段产生的,而是分布在这24个小时内。我们算一下,假设40万个订单分布在24个小时,计算下每秒产生 400000 / (24 * 3600 ) ≈ \approx 5个订单。项目肯定是集群部署的,假设我们的订单服务部署了3台物理机(4核8G),按照负载均衡策略,每台机器可能处理的订单请求可能只有一两个。这种情况下,我们完全不用担心项目会频繁出现full gc触发STW机制。当然这是正常情况下
  • 假设在某个时间段做了促销,比如在20:00有一个大促活动。订单的下单量突然集中在20:00。我们此时来算一下,40万笔订单都集中在20:00这个时间段附近产生,假设是在20:00 ~ 20:05这5分钟时间内产生。所以平均下来每秒产生 400000 / (5 * 60) ≈ \approx 1333笔订单。进而说明3台物理机(负载均衡为轮询策略)每台物理机每秒需要处理444笔订单。要下订单就要创建订单实体,订单实体中肯定包含的字段有各种类型:Long、Integer、List、String……。我们假设一个订单的实体占用内存大小为4kb,于是每秒需要占用 444 * 4kb = 1776kb. 下完订单后还需要走其他业务逻辑,比如创建短信实体类发短信、创建日志实体类记录日志、积分、库存等等,我们放大40倍,所以在大促期间,每台订单物理机需要耗尽 40 * 1776kb = 71040kb ≈ \approx 69M1s后变成垃圾对象(假设1s处理完下单流程)。假设我们以此命令启动的项目java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxx.jar.设置了堆内存为3G。按照咱们上面所说,所以它的年轻代只占用1G,年轻代中的eden区占819M,survivor from区占 102M,survivor to区占102M。在给堆内存设置3G的情况下,在大促期间,系统会频繁出现full gc,造成页面卡顿(STW机制)。
  • 分析原因:一台物理机在大促情况下:需要经历819 / 69 ≈ \approx 11.7秒,eden区才会被占满。在11.7秒后会进行minor gc。于是会将一批批对象放入survivor区,不管是from区还是to区,它最大内存是102M。就将一批对象(大小一共为69M)放入survivor区,此时发现69M已经占用from/to区的50%了,于是就直接把这些对象直接移动到老年代。所以每隔11.7秒就会往老年代塞819M的对象。老年代总共才2G。一次minor gc就占了将近1G,于是得出一个结论:当执行三次minor gc就会触发一次full gc,最终造成的结果就是页面卡顿。所以针对这种情形下,我们调优的点就在于一批批对象在minor gc时达到了survivor from/to区的50%就会移到老年代。因此我们可以把survivor区的大小往上调,而survivor区的大小依赖于新生代的大小。我们设置堆内存中新生代的内存为2个G,那么eden区就是:1638M,survivor from/to区分别为:204M。此时69M并没有达到204M的50%,所以对象需要在survivor经历15次才能进入老年代,不会频繁的出现full gc了。调优后的启动参数为:java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxx.jar
  • 上述流程用图来描述就是:
    在这里插入图片描述
  • 未调优之前(java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxx.jar)启动jar包对应的jvm内存结构:
    在这里插入图片描述
  • 调优后(java -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar xxxx.jar)启动jar包对应的jvm内存结构(主要是显示的指定了年轻代的内存要2个G,-Xmn2048M参数):
    在这里插入图片描述

五、总结

  • 本次主要介绍了jvm的内存模型以及jvm调优demo。近距离接触jvm调优细节,不再认为调优虚而不实。但这多多少少是个demo,因为其中很多地方都是假设的,比如上述假设了一个订单实体的大小为4KB,其实这很小很小,在java中一个Long类型的都占8个字节了。这里就将就下,毕竟咱们要理解的是调优思路。
  • I am a slow walker, but I never walk backwards.
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值