前序:
Jvm调优需要我们对系统有所了解,其中比较关键的是对核心业务的理解,特别是会造成频繁GC的部分,比如高并发造成的不及时回收。
要知道为什么会造成频繁GC,首先我们要懂怎么估算java类的大小
下面列举各个基本类型和字符串估算的表格,以及测试的类
基本类型 | 大小(字节) | 取值范围 | 装箱基本类型 |
int | 4 | -2^31 ~ 2^31-1 | Integer |
char | 2 |
| Character |
byte | 1 | -2^7 ~ 2^7-1 | Byte |
short | 2 | -2^15 ~ 2^15-1 | Short |
long | 8 | -2^63 ~ 2^63-1 | Long |
float | 4 |
| Float |
double | 8 |
| Double |
boolean | 1或4 | true~false | Boolean |
注释:1字节(byte) = 8比特(bit),1kb = 1024字节。为什么boolea需要4个字节,原则上只需要1个比特,但是操作系统以字节作为单位,所以至少要一个字节。又因为jvm用int替代boolean,所以需要4字节。
2字节):
Java规定了字符的内码要用UTF-16编码,一个字符是2个字节。外码字符所占字节取决于具体编码。几种常见的编码换算如下:
1字节编码,只有英文字符,不能编码汉字。
-- GBK编码 英文1字节 汉字2字节。
-- UTF-8编码 英文1字节 汉字3字节。
-- Unicode编码 英文2字节 汉字2字节。
getObjectSize()来获取类的大小(以下是一个demo,可自行测试):
public class Test01 {
public Test01() {
Demo1 demo1 = new Demo1();
long objectSize = getObjectSize(demo1);
System.out.println(objectSize);
}
public static void main(String[] args) throws UnsupportedEncodingException {
new Test01();
// 字符串大小
System.out.println("测试".getBytes("ISO8859-1").length);
}
}
/**
* 对齐填充(8字节补齐)
* 这里有一个注意点:如果大小未满8字节的倍数,会自动补齐8字节倍数。
*/
class Demo1 {
private String str;
// private int intVal;
// private int intVal2;
//private double doubleVal;
}
接下来就举一个简单的jvm调优参考例子:
---------------调优前-----------------
假设有一个日活过亿的电商网站(日均活跃用户500w,平均每人点击20-30次),下单率10%,也就是
下单人数 = 10% * 500w = 50w
把这一千万在整天进行平摊(考虑到用户活跃时间一般只有4-5小时(具体数字见自己系统的分析),所以在这5小时进行平摊)
每秒下单数 = 50w / (5 * 60 * 60) = 27
如果只有27这个数字是很小的,如果把当前服务部署成3个,那每个每秒就9个下单请求,基本不会对系统造成影响,JVM的垃圾回收肯定是正常的。
但是这些系统一般会有一个高峰,在短时间内产品了一天的下单量,比如搞活动双十一啥的,这时候公式计算就变成下面这样:
下单人数 = 10% * 500w = 50w
用户活跃时间就不是4-5个小时了,可能是几分钟内涌入大量订单
每秒下单数 = 30w / 几分钟 = 1000多
这时候我们也部署成3个,每台300来个订单,这时候300明显远大于正常情况,假如三台都是(4G8核)的机器,我们使用2G作为老年代,1G作为新生代(Eden区:s1区:s2区 = 8:1:1 = 800M:100M:100M)
高峰导致的问题(平时都是很平稳的):
高峰的时候会发现系统频繁FullGC告警,频繁FullGC会导致STW(stop the world),让页面产生卡顿,对用户体验不好(正常情况下STW不应该这么频繁)。
假设这是一个订单需求上线后才出现的(也做过jvm参数的变更)。这时候可以从jvm参数来入手如下:
-- 1 估算下单时产生的内存大小
假定每个订单有几十个字段,每个字段8个字节,放大来看:8 * 100 = 800byte,约等于1KB(我们就当它1KB算了)
每秒300个请求就是: 300 * 1KB = 300KB 的对象生成,
然后还有一起杂七杂八的库存、优惠券、积分等,放大20倍:300KB * 20 = 6MB,然后杂七杂八的查询再放大10倍:6MB * 10 = 60MB
然后Eden=800MB,所以每13秒会minor GC一次:800MB / 60MB = 13,并且由于第13次的60M是可能还执行完,其引用还存在(CG root可达),这时候第15次的60MB不会被minor GC,本来应该会从移动s区,但是由于s区只有100MB,60MB 大于 100MB*50%,所以这个60MB会直接移动到老年代,导致老年代一点点上上涨,最后满了触发fullGC,导致STW。
--------------调优解决方法--------------------------------
将新生代设置为2GB,其中Eden=1.8GB,s1=s2=200MB。这样的话60MB放进s区的时候也不会超过50%,随后就能被minorGC掉,从而不会放到老年代中,导致老年代短时间一直涨。导致系统卡顿。
其他编程中的小例子:
1 编程中假设查询出来仅仅有两个字段,就尽量使用只有两个成员变量的类来接收这些数据,不要为了方便用一个含几十个成员变量的类来接收,这样会很占用内容。
2 编程中性能会涉及到网络IO、磁盘IO等,比如我们查数据库,把数据从一次从库查出来和循环中一个个查出来很大区别,主要体现在查数据库的时候有网络IO和磁盘IO,多次查询的网络IO+磁盘IO累计起来很可怕。
3 使用中不要以为用redis就一定很快,比如你用redis在循环中查数据,假设每次吊redis耗时1ms,但是数据一多(假设2w条)就会变成2w * 1ms = 20s,这样是无法忍受的,相反一次性从redis查两位万条可能就1-50ms之间,性能完全没得比。