JVM生产环境中的堆到底该如何设置(二)

目录:

  1. JVM分代模型:年轻代、老年代、永久代
  2. 你的对象在JVM内存中如何分配,如何流转的?
  3. 线上系统如何设置JVM内存大小参数的?
  4. 每日100万交易量的支付系统,到底该如何设置JVM堆大小?
  5. 总结

1. JVM分代模型:年轻代、老年代、永久代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class JvmTest {

public static void main(String[] args){
long i=0L;
while( i < 1000){
pushWork(i);
}
System.out.println("finish job");
}

public static void pushWork(long id){
Worker worker = new Worker("lishijia", id);
worker.doWork();
}

static class Worker{
private String name;
private Long id;

public Worker(String name, Long id) {
this.name = name;
this.id = id;
}

public void doWork(){
System.out.println(this.name + " do work");
}
}
}

先看看上面这段代码,首先JVM一旦加载执行JvmTest这个类,首先一旦执行main()方法,那么就会把main方法压入到main线程的虚拟机栈中,接下来则执行pushWork方法,照样会把pushWork压入到main线程栈中,大家平常在debug的时候,也可以看到对应线程所有的方法栈。

通过以上的代码执行完之后,java虚拟机里面主要牵涉到两个重要区域,一个Java虚拟机栈,一个Java堆。所有的方法在main线程中产生对应的栈帧,然后当中的局部变量实例化产生在java堆当中,局部变量的引用java堆中的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JvmTest {

static JvmTest jvmTest = new JvmTest();

public static void main(String[] args){
long i=0L;
while( i < 1000){
jvmTest.pushWork(i);
}
System.out.println("finish job");
}

public void pushWork(long id){
Worker worker = new Worker("lishijia", id);
worker.doWork();
}
}

再看上面这段代码,当中的JvmTest实例是静态的,pushWork也改为了实例方法。这个时候JvmTest的这个实例需要长久存在,它首先也是在年轻代产生,经过长达十次以上的MinorGC以后它会晋升到老年代。

年轻代:即大多数新创建的对象一开始都是在老年代

老年代:经过多次MinorGC而没有被回收的对象会存放在老年代,还有就是年轻代经过垃圾回收也存放不下的对象会直接在老年代创建

永久代:一般存储类相关的信息

2. 你的对象在JVM内存中如何分配,如何流转的?

还是上面那个图,当中的jvmTest实例,另外还有包括work实例到底是怎么分配,什么时候流转到老年代的呢?也就是说什么情况下会执行垃圾回收呢?到底在什么时候回收呢?

首先上面那段代码会在几个地方产生实例对象,首先就是一个静态的JvmTest这个实例,另外就是pushWork当中的Work实例,没执行一次pushWork方法都会产生一个Work实例,那么如果当一个pushWork执行完之后即出栈了,那么此时就一定会发生垃圾回收吗?No,准确说应不一定。那么垃圾回收到底发生在什么时候呢?

可以设想下,如果年轻代假设它只能存放两个Work实例,按照上图也就只能放下”两个”,哈哈。只要在还能放的下的情况下,这个时候是不会执行垃圾回收的。当执行到第三次循环的时候,需要产生新的Work的时候,这个时候已经放不下了,那么此时便会执行垃圾回收,把Work1与Work2清除这个时候才能存放新的Work实例对象,这个时候JvmTest也就会标记已经存活了一个周期,当反复执行这个流程的时候,存活周期到达一定的次数10以上它就会往老年代晋升,从而存储在老年代当中。

那么老年代的对象在什么时候进行回收呢?这个我们下次再看。

3.线上系统如何设置JVM内存大小参数的?

-Xms1024M 堆初始大小 -Xmx1024M 堆最大大小 一般两个值设置为一样

-Xmn512M 新生代大小,扣除新生代剩余老年代的大小

-XX:PermSize=256M 永久代初始大小 -XX:MaxPermSize=256M永久代最大大小 通常两个值设置为一样

-Xss128M 每个线程的栈内存大小

4.每日100万交易量的支付系统,到底该如何设置JVM堆大小?

可以看到,市面上大多数的支付系统再中间应该就承担这么个角色,不过这个是最简单的复杂的可能还增加了结算,对账等功能。

那么根据上面这个流程来识别,压力到底会在哪里呢?如果按照100万的交易量来说,也及时业务系统每天至少也会产生100万左右的交易数据,可以看到以上有几个地方会产生数据。创建支付订单,和第三方支付回调,创建支付和支付回调理论上来说是1:1,不过创建支付肯定会大于支付回调,存在很多创建支付订单但是不支付的。所以可以就先拿创建支付订单来分析,到时候在预估翻倍就可以了。

那么针对创建支付订单,每日百万的流水从这个支付服务当中流通,也就是说如果是一台机器的话那么这个JVM虚拟机每天要创建上百万的实例。

那么抛开其他并发上面的因素,就单单指JVM虚拟机的压力,每天JVM虚拟机需要创建上100万的实例,反复的执行创建销毁创建销毁这么一个过程。所以这里就牵涉到几个核心问题。

  • 我们需要部署多台太机器还应对这个情况?
  • 每台器需要多大的内存空间
  • 每台机器上的JVM堆内存设置多少?
  • 给JVM多大的内存空间才能保证整个系统不会崩溃?

要解决上面的这些问题,那我们再来详细细分一下。想想每日100万的订单量,我们需要计算到每秒需要处理多少比订单?

假设每天100万的订单,24个小时。就算高峰期咱们把他归类到3个小时之内产生这个100万订单,那么每秒要出多多少笔呢?算一下大概在100笔左右,也就是每秒要处理100笔支付订单。

假设我们部署了两台支付服务,那么每台每秒就需要处理50笔左右,按照这个策略走的话也就是说每台机器的虚拟机在每秒会产生50笔支付订单 ,在虚拟机的堆中的新生代产生着50个对象,每过一秒反复着执行上面这个步骤。

接下来那我们就需要计算或预估每个支付订单到底会占用多大的内存空间呢?以下是我们支付服务的支付订单实体类。按照基础类型的字节来算的话。一个Integer变量占用4个字节,Long类张勇8个字节还有别的变量等。按照下面的这个实例来算的话接近30个变量,一个变量算它32个子也大概也就900字节左右不到1kb。那么50个订单也就在50kb上下,也就是说每台虚拟机每秒会使用50kb的内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public class PayOrder implements Serializable {
private String payOrderId;
private String mchId;
private String mchOrderNo;
private String smartTripOrderNo;
private String channelId;
private Long amount;
private String currency;
private Byte status;
private String clientIp;
private String device;
private String subject;
private String body;
private String extra;
private String channelMchId;
private String channelOrderNo;
private String errCode;
private String errMsg;
private String param1;
private String param2;
private String notifyUrl;
private String returnUrl;
private Byte notifyCount;
private Long lastNotifyTime;
private Long expireTime;
private Long paySuccTime;
private Date createTime;
private Date updateTime;
private static final long serialVersionUID = 1L;
}

这样子支付系统运行起来,每秒需要50kb左右的内存空间,一秒过后这50个对象在新生代所产生的对象就变成了一个没有被引用的实例了,代表是可以被回收了。但是这个时候还不会马上被回收,接着下一秒过后重复着上面的那个流程。直到某一刻,发现新生代已经存满了上百万的对象了也就是说已经使用了几百兆的新生代的内存空间,可能新生代已经快满了,这个时候就需要执行MinorGC了,清除垃圾空间,腾出空间来在内存中继续创建新的对象。

分析至此基本上就把PayOrder这个单独拉出来分析完了,但实际运行过程当中就这么一个创建支付订单肯定不止一个PayOrder存在的,肯定还会存在其他的对象。如果一定需要估算的话,一般我们把计算结果扩大到10~20倍左右。也就说每秒可能产生几百kb~1MB之间(500kb~1MB)。然后接下来可能就是一秒大概1MB左右的的内存需要再新生代创建,重复以上的那个动作,知道新生代满了之后再执行垃圾回收。

那么针对这种系统JVM的堆内存到底该怎么设置呢?

设想一下如果堆内存设置为1G,新生代与老年代按照一半来分的话。也就500MB左右,按照上面的这个分析方法,也就是说几百秒就需要执行一次MinorGC(一秒差不多1MB的内存空间50个订单),这么频率很搞的执行GC肯定是不利于系统的稳定的。针对这种业务场景肯定需要设置更大的内存空间。

比如设置为:-Xms3G -Xmx3G -Xmn2G。给整个堆设置为3G,新生代设置为2G。这样子设置的话可能办小时才会执行一次MinorGC。就不会频繁的出发MinorGC。如果可以在扩展一台到两台的机器,那这样子是不是压力就相当于更小了。其实很多常见的负载均衡就是这么个道理。

5.总结

  • 一个新上线的系统先分析梳理业务流程(或者性能不加的业务系统)
  • 分析完业务流程之后大概就能知道压力道理会在哪里了
  • 判断每秒需要处理多少笔交易流水才能带到期望值?
  • 每笔流水处理耗时多久?
  • 每笔流水所需要占用的内存空间是多少?
  • 根据预测值判断每秒大概需要占用多大的内存空间?
  • 在心中模拟整个系统的运行分析一下
  • 对完整系统内存占用进行预估
  • 最终对症下药设置对应的堆内存大小
分享到 评论