上一篇文章介绍了jvm的内存模型相关知识,本文就来介绍一下垃圾回收的相关内容。
垃圾回收器的作用
1、对象的分配与引用
我们先来看一个代码片段,内如如下:
public class User {
public static void main(String[] args) {
loadDisk();
}
public static void loadDisk() {
Role role = new Role();
role.load();
}
}
在上述代码中,一旦“role.load()”执行结束,那么方法就执行完毕了,对应的栈帧就要从main线程的java虚拟机栈里出栈。此时一旦loadDisk方法的栈帧出栈,那么栈帧里的局部变量“role”也就没有了。也就是说,没有任何一个变量指向java堆内存里的Role实例对象了,这个对象实际上已经没有用了。
大家知道内存资源是有限的。一般来说,我们在服务器上启动一个java系统,机器的内存资源是有限的,比如就是4个G的内存。我们启动一个java系统本质就是一个jvm进程,那么这个jvm进程本身也是会占用机器上的部分内存资源,比如占用1G的内存资源。
我们在jvm的java堆内存中创建的对象本质也是会占用jvm的内存资源,比如Role实例对象会占用500字节的内存。所以我们到这里应该清楚的知道:我们在java堆内存里创建的对象都是占用内存资源的,而且内存资源是有限的。
2、不再需要的对象怎么处理?
既然创建的对象不被任何方法的局部变量引用,它还占用着有限的资源,那我们应该怎么处理它呢?
大家肯定都想到了:JVM的垃圾回收机制。
jvm本身是有垃圾回收机制的,它是一个后台自动运行的线程。你只要启动一个jvm进程,它就会自带一个垃圾回收的后台线程。
如果某个实例对象没有任何一个方法的局部变量指向它,也没有任何一个类的静态变量,包括常量等地方在指向它,那么这个垃圾回收线程就会把这个没人指向的实例对象回收掉,从内存里清除,释放它占用的资源。
jvm分代模型
我们继续看代码片段,内容如下:
public class User {
public static void main(String[] args) {
while(true) {
loadDisk();
Thread.sleep(1000);
}
}
public static void loadDisk() {
Role role = new Role();
role.load();
}
}
这个代码片段我们稍微做了修改,在main方法里会周期性地执行“loadDisk”方法,首先,一旦执行main方法就会把main方法的栈帧压入main线程的java虚拟机栈。然后每次在while循环里,调用loadDisk方法就会把该方法的栈帧压入自己的java虚拟机栈。接着执行“loadDisk”方法的时候会在java堆内存里创建一个Role对象实例,在“loadDisk”方法里会通过“role”局部变量去引用这个实例对象,然后就会执行Role对象的“load”方法。
大部分对象都是存活周期及短的
现在有一个问题,在上面的代码中,那个Role对象实际上存活时间及短,此时一旦没人引用这个Role对象,就会被jvm的垃圾回收线程给回收掉,释放内存空间。循环往复,Role对象被不断地创建和回收。我们的程序里其实大多数都是这种对象。
少数对象是长期存活的
但是我们来看另外一个代码片段:
public class User {
private static Role role = new Role();
public static void main(String[] args) {
while(true) {
loadDisk();
Thread.sleep(1000);
}
}
public static void loadDisk() {
role.load();
}
}
上述代码的意思就是给User这个类定义一个静态变量,也就是role,这个User类是在jvm的方法区里。然后让role引用了一个在java堆内存里创建的Role实例对象。这个时候的Role实例对象会一直被User类的静态变量引用,所以它会一直驻留在java的堆内存里,是不会被垃圾回收掉的。
年轻代和老年代
现在大家已经看到根据你写代码方式的不同,采用不同方式来创建和使用对象,其实对象的生存周期是不同的。所以jvm将java堆内存划分为了两个区域,一个是年轻代,一个是老年代。
其中年轻代就是把第一种代码示例中的那种,创建和使用完之后立马就回收的对象放在里面。
老年代就是把第二种代码示例中的那种,创建之后需要一直长期存在的对象放在里面。
为什么需要区分年轻代和老年代?
因为这跟垃圾回收有关,对于年轻代里的对象,它们的特点是创建之后很快就会被回收,所以需要用一种垃圾回收算法。老年代里的对象,它们的特点是需要长期存在,所以需要另外一种垃圾回收算法。所以要区分成两个区域来放不同的对象。
什么是永久代
很简单,jvm里的永久代其实就是我们之前说的方法区,你可以认为所谓的永久代就是放一些类的信息。
对象在jvm中如何分配?
大部分正常对象都优先在新生代分配内存
我们回头看下上面的代码片段,类静态变量“role”引用的Role对象是会长期存在的,但是哪怕这种对象,刚开始通过“new Role()”代码来实例化对象时它也是分配在新生代里的。
何时触发新生代的垃圾回收
现在我们来看一种场景,大家都应该知道,当方法执行完毕后,这个方法的栈帧出栈,会导致没有任何局部变量引用之前创建的实例对象,那么此时就一定会立即发生垃圾回收,去回收掉java堆内存里哪个没人使用的实例对象吗?
大家别想的那么简单,实际上垃圾回收有它的触发条件。
其中一个比较常见的场景是这样的,我们在方法中创建了大量的对象,方法执行完毕之后这些对象就不再被引用,这样的对象会随着程序的运行越来越多,这个时候,新生代预先分配的内存空间几乎被对象占满了,此时代码继续运行,它需要在新生代里分配一个对象,发现新生代内存空间不够了,怎么办?
这个时候就会触发一次新生代内存空间垃圾回收,新生代内存垃圾回收也被称之为“Minor GC”,有的时候我们叫“Young GC”, 它会尝试把新生代里那些没有被引用的垃圾对象给回收掉。
长期存活的对象
接着我们看下一个问题,就是类静态变量引用的实例对象,它是长期存活的对象。虽然新生代随着系统的运行不停的创建对象、回收对象,但是例如类静态变量引用的实例对象会一直存在,不会被回收。
那么此时jvm就有一条规定了:
如果一个实例对象在新生代中成功的在15次垃圾回收之后还是没有被回收掉,就说明它已经15岁了,这就是对象的年龄,每垃圾回收一次,如果一个对象没被回收掉,那么它的年龄就会增加1岁,15次过后它就会被转移到java堆内存的老年代中去,顾名思义,老年代就是存放这些年龄很大的对象。
老年代会垃圾回收吗?
老年代里的那些对象会被垃圾回收吗?
答案是肯定的!因为老年代里的对象也有可能随着代码的运行不再被任何变量引用了,所以也需要被垃圾回收。当越来越多的对象进入老年代,一旦老年代也满了,就会对老年代进行垃圾回收。至于回收的细节我们这里先不剖析。
常用参数介绍
讲了这么多原理性的东西,下面我们来具体看看jvm参数如何设置。
在jvm内存分配中,有几个参数是比较核心的,如下所示:
1、-Xms:java堆内存大小
2、-Xmx:java堆内存的最大大小
3、-Xmn:java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小
4、-XX:PermSize:永久代大小
5、-XX:MaxPermSize:永久代最大大小
6、-Xss:每个线程的栈内存大小
下面我们对上述参数一一说明。
-Xms和-Xmx分别用于设置java堆内存的初始大小和允许扩张到的最大大小。对于这对参数,通常来说,我们都会设置为完全一样的大小,至于细节,我们后续讨论。但是大家要清楚,这两个参数是用来限定java堆内存的大小的。
-Xmn这个参数也是很常见的,它用来设置java堆内存中的新生代大小,然后扣除新生代大小之后的剩余内存就是给老年代的内存大小。
-XX:PermSize和-XX:MaxPermSize分别限定了永久代初始大小和永久代最大大小。通常这两个参数值也是设置为一样的,至于原因,我们后面再介绍。
如果是jdk1.8以后的版本(包括1.8版本),那么这俩参数被替换为-XX:MetaspaceSize 和-XX:MaxMetaspaceSize。
-Xss这个参数限定了每个线程的栈内存大小,每个线程都有一个自己的虚拟机栈,然后每次执行一个方法,就会将方法的栈帧压入线程的栈里,方法执行完毕,那么栈帧就会从线程的栈里出栈。
那么在线上部署系统应该如何设置jvm参数呢?
其实比较简单,比如说采用“java -jar”的方式启动一个jar包里的系统,那么就可以采用类似下面的格式:
java -Xmx512m -Xms512m -Xmn256m -XX:PermSize=128m -XX:MaxPermSize=128M -Xss1m -jar app.jar
每日百万交易的电商支付系统如何设置jvm堆内存大小
一个每日百万交易的电商支付系统,它的系统压力到底集中在哪里呢?我们应该知道,支付最核心的环节就是在用户发起支付请求的时候生成一个支付订单。这个支付订单需要记录清楚比如是谁发起支付?对哪个商品的支付?通过哪个渠道进行支付?还有发起支付的时间等等。
如果每日百万交易,那么它的压力有很多方面,包括高并发访问、高性能处理请求、大量的支付订单数据需要存储等技术难点,但是我们现在抛开这些系统架构层面的东西来看看jvm层面。
我们的支付系统最大的压力就是每天jvm内存里会频繁的创建和销毁100万个支付订单,所以这里就牵扯到一个核心的问题。
- 我们的支付系统需要部署多少台机器?
- 每台机器需要多大的内存空间?
- 每台机器上启动的jvm需要分配多大的堆内存空间?
- 给jvm多大的内存空间才能保证可以支撑这么多的支付订单在内存里创建对象,而不会导致内存不够?
我们首先第一个要计算的就是每秒钟系统要处理多少笔支付订单。假设每天100万个支付订单,那么一般用户交易行为都会发生在每天的高峰期,比如中午或者晚上。
假设每天高峰期大概就是几个小时,用100万平均分配到几个小时里,那么大概是每秒100笔订单左右。然后假设我们的支付系统部署了3台机器,每台机器实际上每秒大概处理30笔支付订单。
用户发起一次支付请求,支付需要在jvm中创建一个支付订单对象,填充数据进去,然后写入数据库,可能还会处理其它一些事情,我们现在假设一次支付请求的处理,包含一个支付订单的创建,大概需要1秒钟的时间。
那么现在大体上我们的脑子里可以出现一个流动的模型:每台机器一秒钟接收到30笔支付订单的请求,然后在jvm的新生代里创建了30个支付订单的对象,做了写入数据库等处理。接着1秒钟之后,这30个支付订单就处理完毕,然后这些支付订单对象的引用就回收了,这些对象在jvm新生代里就变成了垃圾对象。接着下一秒来30个支付订单,重复这个步骤。
其实不考虑别的,我们可以大概来估算一下,比如说支付订单类如下所示:
public class PayOrder {
private Integer userId;
private Long orderTime;
private Integer orderId;
}
你只要记住一个Integer 类型的变量数据是4个字节,Long类型的变量数据是8个字节,还有别的类型的变量数据占据多少字节,百度一下都可以查到,然后就可以计算出每个支付订单对象大致占据多少字节。一般来说,比如支付订单这种核心类,你就按20个实例变量来计算,然后一般大概一个对象也就在几百字节的样子。我们算它大一点好了,就算一个支付订单对象占据500字节的内存空间,也就不到1kb的样子。
之前说过,假设有3台机器,每秒钟处理30笔支付订单的请求,那么在这1秒内肯定是有方法里的局部变量在引用这些支付订单的。那么30个支付订单,大概占据的内存空间是30*500字节 = 15000字节,大概也就是15kb而已,其实非常小。
我们现在对完整的支付系统内存占用进行预估。之前的分析,全部都是基于一个核心业务流程中的一个支付订单对象来分析的,其实那只是一小部分而已。真实的支付系统线上运行肯定每秒会创建大量的其它对象,但是我们结合这个访问压力以及核心对象的内存占据,大致可以来估算一下整个支付系统每秒钟大致会占据多少内存空间。
其实如果要估算的话可以把之前的计算结果扩大10倍 ~ 20倍。也就是说,每秒钟除了在内存里创建的支付订单对象还会创建其它数十种对象。那么每秒钟创建出来的被栈内存的局部变量引用的对象大致占据的内存空间就在几百KB ~ 1MB之间。
其实结合支付系统的核心业务流程分析清楚了之后,大家就完全知道这么一个线上系统每个机器部署上线的时候jvm的堆内存应该如何设置了。一般来说这种线上业务系统,常见的机器配置是2核4G或者4核8G。如果我们用2核4G的机器来部署,其实还是有点紧凑的,因为机器有4G内存,但是机器本身也要用一些内存空间,最后你的jvm进程最多也就是2G内存。然后这2G内存还得分配给方法区、栈内存、堆内存几块区域,那么堆内存可能最多就是1G多的内存空间。
然后堆内存还分为新生代和老年代,你的老年代总需要放置系统的一些长期存活的对象吧,怎么也得给个几百MB的内存空间吧,那么新生代可能也就几百MB的内存了。这样的话,我们上述的核心业务流程只不过仅仅是针对一个支付订单对象来分析的,但是实际上如果扩大10倍 ~ 20倍换成对完整系统的预估之后,我们看到,大致每秒会占据1MB左右的内存空间。那么如果你的新生代就几百MB的内存空间,是不是会导致运行几百秒之后,新生代内存空间就满了?此时是不是就得触发Minor GC了?
这么频繁的触发Minor GC会影响线上系统的性能稳定性,具体原因后续再说。因此你可以考虑采用4核8G的机器来部署你的线上系统,那么你的jvm进程至少可以给4G以上内存,新生代至少可以分配到2G内存空间,这样就可以做到即使新生代每秒消耗1MB左右的内存,但是需要将近半小时到1小时才会让新生代触发Minor GC,这就大大降低了GC的频率。
举个栗子:机器采用4核8G,然后-Xms和-Xmx设置为3G,给整个堆内存3G内存空间,-Xmn设置为2G,即给新生代2G内存空间。如果你的业务量更大,你可以考虑不只部署3台机器,可以横向扩展部署5台机器,或者10台机器,这样每台机器处理的请求更少,对jvm的压力更小。
什么情况下jvm内存中的一个对象会被回收呢?
通过之前的学习,相信大家都知道平时我们系统运行创建的对象都是优先分配在新生代里的,当新生代被填满的时候就会触发垃圾回收,把新生代没人引用的对象给回收掉,释放内存空间。那么我们下面来看下它到底是按照一个什么样的规则来回收垃圾对象的。
1、被哪些变量引用的对象是不能回收的?
这个问题非常好解释,jvm中使用了一种来判断哪些对象是可以被回收的,哪些对象是不可以被回收的。这个算法的意思就是说对每个对象都分析一下有谁在引用它,然后一层一层往上去判断,看是否有一个 GC Roots。这句话比较抽象,不好理解,下面我们通过示例代码来一步一步分析,代码片段如下:
public class User {
public static void main(String[] args) {
loadDisk();
}
public static void loadDisk() {
Role role = new Role();
}
}
在上述代码中,“main()”方法的栈帧入栈,然后调用“loadDisk()”方法,栈帧入栈,接着让局部变量“role ”引用堆内存里的“Role”实例对象。假设现在“Role”实例对象被局部变量给引用了,那么此时一旦新生代快满了,发生垃圾回收,会去分析这个“Role”对象的可达性,这时发现它是不能被回收的,因为它被人引用了,而且是被局部变量“role”引用的。
在jvm规范在中,只要一个对象被局部变量引用了,那么就说明他有一个GC Roots,此时就不能被回收了。另外一个比较常见的情况,其实就是类似下面的这种情况:
public class User {
private static Role role = new Role();
}
此时垃圾回收的时候一分析,发现这个“Role”对象被User类的一个静态变量“role”引用了。在jvm的规范里,此时只要一个对象被GC Roots引用了就不会被回收。
一句话总结,只要你的对象被引用了,就不会回收它们。
2、java中对象的不同引用类型
java里有不同的引用类型,分别是。下面分别用代码来示范一下。
强引用,就类似下面的代码:
public class User {
private static Role role = new Role();
}
这个就是最普通的代码,一个变量引用一个对象,只要是强引用的类型,那么垃圾回收的时候绝对不会去回收这个对象。
接着是软引用,类似下面的代码:
public class User {
private static SoftReference<Role> role = new SoftReference<Role>(new Role());
}
就是把“Role”实例对象用一个“SoftReference”软引用类型的对象给包装起来,此时这个“role”变量对“Role”对象的引用就是软引用。正常情况下垃圾回收是不会回收软引用对象的,但是如果你进行垃圾回收之后,发现内存空间还是不够用,内存都快溢出了,此时就会把这些软引用对象给回收掉,哪怕它被变量引用了,但是因为它是软引用,所以还是要回收。使用场景:在内存足够的情况下进行缓存,提升速度,内存不足时JVM自动回收。
接着是弱引用,类似下面的代码:
public class User {
private static WeakReference<Role> role = new WeakReference<Role>(new Role());
}
这个弱引用就跟没引用是类似的,如果发生垃圾回收,就会把这个对象回收掉。
虚引用,这个暂时忽略,因为很少用。
堆内存相关的介绍到此结束,下篇文章介绍一下垃圾回收算法。