JVM学习笔记(2):JVM分代模型与内存参数设置
一、JVM分代模型
1、背景
public class Kafka {
// 年轻代
public static void main(String[] args) throws InterruptedException {
while (true) {
loadReplicasFromDisk();
Thread.sleep(1000);
}
}
private static void loadReplicasFromDisk() {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.load();
}
}
我们可以看到在main()
方法的while
循环中,不断调用loadReplicasFromDisk()
方法生成新对象,这个新生成对象的存活时间是极其短的,大致过程是loadReplicasFromDisk()
入栈,在堆内存中生成对象,栈帧里的局部变量指向这个对象,然后方法结束,出栈,这个对象就没人指向了,结果如下图
但有些对象是长期存活的,比如下面这段代码。
public class Kafka {
// 老年代
private static ReplicaManager replicaManager = new ReplicaManager();
public static void main(String[] args) {
while (true) {
loadReplicasFromDisk();
Thread.sleep(1000);
}
}
private static void loadReplicasFromDisk() {
replicaManager.load();
}
}
在Kafka类中定一个一个静态变量,这个静态变量指向堆内存中的对象,while循环中不停得调用这个对象的方法,使得这个对象一直被引用,这样就长期存活下来了。
2、年轻代、老年代、永久代
-
JVM将Java堆内存分成两个区域,即年轻代和老年代,年轻代就是生命周期极短、用完就要被回收的对象所在的区域,老年代就是生命周期较长对象存在的区域,需要一直存在堆中,让程序后续不断的去使用。
-
为什么要设计两个区域呢?因为针对每个对象的生命周期特点来设计不同的GC算法,保证GC的稳定性。
-
永久代:之前讲过的方法区就是永久代,它存放了类相关的信息
-
永久代会产生回收嘛?肯定是会的,有这样几种情况会导致回收
- 该类在堆中的所有实例对象都被回收了
- 加载这个类的ClassLoader也被回收了(比如自己定义的类加载器也是个对象,没人用就会被回收)
- 最后对该类的Class对象也没有引用(如果有变量引用类的Class对象,就是有引用)
- 比如利用反射,来获取一个对象的类的Class对象实例,如
Class c = replicaManager.getClass()
,可以通过replicaManager
引用的对象来获取ReplicaManager
类的Class对象,这个变量c
就能引用这个Class对象
- 比如利用反射,来获取一个对象的类的Class对象实例,如
public class Kafka {
private static ReplicaFetcher fetcher = new ReplicaFetcher();
public static void main(String[] args) {
//年轻代
loadReplicasFromDisk();
//老年代
while (true) {
loadReplicasFromRemote();
Thread.sleep(1000);
}
}
private static void loadReplicasFromDisk() {
ReplicaManager replicaManager = new ReplicaManager();
replicaManager.load();
}
private static void loadReplicasFromRemote() {
fetcher.fetch();
}
}
对应下面这张图
二、JVM内存如何分配?
虽然有了年轻代和老年代,但是对于大部分正常对象,都是先升在新生代中分配内存的,新生代就是年轻代和老年代的总和,一开始并没有区分。我们的对象有两种类型,一种是像ReplicaManager这样的,用完一次就没人指向他了,这样的对象在新生代内存中不断囤积,最后导致空间几乎都被它占满了,那就会触发Minor GC
,又称之为Young GC
,它就把新生代中的没用的这些垃圾回收掉;另一种对象是像ReplicaFetcher这样的,始终有人指向它,JVM中有规定,垃圾每回收一次,如果一个对象没被回收掉,它的年龄就增加1,如果一个对象年龄超过15,就会把它放置到老年代区域(这个阈值我们可以修改,默认值是15),也就是说老年代存放了年龄很大的对象,下面两张图就很形象。同样的,如果老年代也满了也会被回收。
小结:
- 一般对象先分在新生代,新生代满了就GC,有些对象满足条件进入老年代,老年代满了也会GC,清除没用的
对象分配的机制有很多,如
- Minor GC后存活对象太多,大量对象直接进入老年代
- 超大对象不经新生代直接进入老年代
- 动态对象年龄判断机制
- 空间担保机制
ReplicaManager的对象太多导致内存不够
ReplicaManager的对象被回收掉,且ReplicaFetcher对象符合要求进入老年代
三、JVM内存分配参数
-Xms:Java堆内存大小
-Xmx:Java堆内存最大大小
-Xmn:Java堆内存中新生代大小,扣除新生代就是老年代的大小了
-XX:PermSize:永久代大小
-XX:MaxPermSize:永久代最大大小
-Xss:每个线程栈内大小,一般0.5~1M
四、JVM堆内存、栈内存、永久代大小设置
1、堆内存大小设置
背景是电商支付系统,首先看一下支付流程:
这个系统的支付压力就是每日百万订单的交易,从JVM角度来看,就是在堆内存中生存了百万个订单对象,而这些订单频繁的创建和销毁就是核心问题。
我们怎么去设定堆内存的大小呢?按照下边的步骤估计:
- 确定业务规模,分析系统压力点
- 确定我们的系统部署了多少台机器,计算每秒请求数,考虑每个请求的耗时
- 结合每个请求消耗的内存,确定机器的参数,分配多少内存空间合适
- 确定每台机器JVM分配的内存空间(方法区、栈内存、堆内存,堆中新生代、老年代大小)
假设我们一天100万个订单,共三台机器,把这100w个订单分配到几个小时里(考虑高峰时间段),假设算下来一秒钟100个订单。把这100个订单的任务分配个3台机器,每台机器一秒30个订单,那这30个订单占多少内存?就要看我们订单对象是多大了,看订单对象里面的变量来计算(Integer变量4字节,Long类型8字节…),假设1kb,那么30个订单也就30kb,大概情况如下。系统运行起来后,一台机器每秒有30个对象不被使用了,然后直到新生代空间快满了,就发生一次Minor GC,这就是这个业务运行的模型。但是实际情况下,每秒钟创建出来的对象不单单就订单对象,还会创建许多其他对象,所以每秒创建出来的被栈内存中局部变量引用的对象所占用的空间大致在几百KB~1MB之间。
做完了前三步后,我们要估算JVM堆内存如何分配了,常见的机器配置也就2核4G或4核8G。以2核4G为例,一半内存要给机器本身运行,另一半分配给JVM,也就是说分配到了2G,这2G要分配给方法区、栈内存、堆内存,堆内存最多再分得一半,也就是1G,堆内存中要划分新生代和老年代,假设都给一半,实际业务情况下1秒占1M那么过不了多了就会GC,频繁的GC影响了系统的稳定性,所以不行,还是得换4核8G,这样堆内存还能分得多一点,比如分3G(-Xms和-Xmx设置为3G,整个堆内存,然后-Xmn给新生代2G),能够大大降低GC频率,当然部署机器越多,对JVM压力更小。
如果我们设置内存过小会出现什么情况?比如双十一,这会每秒支付不再是1秒100个了,而是秒1000个,这时候所有资源都会吃紧,当然有些支付请求并不是立即支付的,它可能卡好久,使得这些数据在Minor GC后都放到了老年代,与此同时新生代中又不断增加对象,最后新生代又爆满,然后再Minor GC放到老年代。。。最后老年代对象越来越多,可能频繁触发老年代的垃圾回收,老年代的垃圾回收速度是很慢的!,然后不断影响系统性能
2、栈内存和永久代大小设置
栈内存:不用特别的去估计和设置,默认的是512KB~1MB,差不多够了。
永久代:存放类相关信息,一般设置几百MB是够的,当然有些系统会导致永久代内存溢出,后面分析。