JVM第二周 JVM内存结构

446 阅读10分钟

JVM分代模型

  • 年轻代
  • 老年代
  • 永久代

对象的生命周期

  1. 大部分对象都是存活期很短的
  2. 少数对象是长期存活的
    1. static静态变量 引用的对象

年轻代、老年代

JVM将堆内存划分成了两块区域,分别是年轻代和老年代。

年轻代:创建完很快就被回收的对象在里面。

老年代:存放创建后长期存放的对象。

为什么堆内存要分成年轻代和老年代

方便垃圾回收。

年轻代里的对象,创建完后就会被回收,针对这样的情况,设计出针对性的垃圾回收算法。

老年代里的对象,创建后长期存放,也需要一种垃圾回收算法。

永久代

TODO

堆内存会进行垃圾回收,方法区会吗

会的,在以下情况下方法区中的类会被回收

  1. 首先该类的所有实例对象都被垃圾回收掉
  2. 加载当前类的ClassLoader被垃圾回收掉
  3. 对该类的Class对象没有任何引用

每个线程都有自己的Java虚拟机栈,保存局部变量表等信息,那么虚拟机栈会进行垃圾回收吗?

个人思考:不会,随着方法执行完成,方法对应的栈桢就会出栈

面试题:你的对象在JVM内存中如何分配?如何流转的?

短期存活的对象,在新生代中;

长期存活的对象,在老年代中

大部分正常对象优先在新生代中分配内存

正常对象:

不正常对象:

什么情况下会触发新生代(年轻代)的垃圾回收

新生代垃圾回收的触发条件

如果新生代我们预先分配的内存空间,几乎都被全部对象给占满了!此时假设我们代码继续运行,他需要在新生代里去分配一个对象,怎么办?发现新生代里内存空间都不够了!

这个时候,就会触发一次新生代内存空间的垃圾回收,新生代内存空间的垃圾回收,也称之为**"Minor GC",有的时候我们也叫“Young GC"**,他会尝试把新生代里那些没有人引用的垃圾对象,都给回收掉。

长期存活的对象会躲过多次垃圾回收

如果一个实例对象在新生代中,成功的在15次垃圾回收之后,还是没有被回收掉,说明它已经15岁了。

15是通过 MaxTenuringThreshold配置项指定。

15岁就是对象的年龄,每次垃圾回收后,对象如果没有被回收掉(而是存活下来),它的年龄就会+1。

此时该实例对象就会被转移到老年代中。你15岁了,已经是一个成熟的老年人了哈哈哈

也就是说老年代就是用来存放那些年龄很大的对象,年龄很大就是在很多次垃圾回收后,存活下来的对象

老年代会触发垃圾回收吗

答案:当然会啊!

  1. 老年代的对象可能随着代码运行,不再被任何人引用了,就需要垃圾回收了
  2. 越来越多的对象进入老年代,老年代空间不断被填充(空间是有限的),是不是会触发垃圾回收呢

对象生存周期的思考

大家能否结合短生存周期的对象的特点,以及长生存周期的对象的特点,思考一下,看看你们手头正在负责的系统,梳理梳理里面短生存周期的对象都有什么,长生存周期的对象都有什么。

短生存周期的对象:

  1. prototype Bean
  2. 方法内部创建的对象

长生存周期的对象:

  1. singleton Bean
  2. static变量

动手实验:亲自感受一下线上系统部署时如何设置JVM内存大小

核心参数

  • 机器配置:4core 8G Linux

  • jdk版本: JDK8

-Xmx5440M -Xms5440M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled

-Xms Java堆内存大小

Java堆内存的初始大小

-Xmx Java堆内存最大大小

Java堆内存允许扩张到的最大大小

-Xmn Java堆内存中的新生代大小

扣除新生代的,剩下的就是老年代的大小了

-XX:PermSize: 永久代大小

-XX:MaxPermSize: 永久代最大大小

-Xss: 每个线程的栈内存大小

线上部署系统如何设置JVM参数

JDK 1.8新增的参数:

-XX:MetaspaceSize=128m (元空间默认大小) -XX:MaxMetaspaceSize=128m (元空间最大大小)

通过jar包启动的系统

java -Xms512M -Xmx512M -Xmn256M -Xss1M -XX:PermSize=128M -XX:MaxPermSize=128M  -jar App.jar

基于容器部署系统

Tomcat catalina.sh

参数图示

-Xmx5440M -Xms5440M -XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled

案例分析

(案例一)每日百万交易的支付系统,如何设置JVM堆内存大小?

每日百万交易的支付系统,压力在哪?

用户发起支付请求,就会创建支付订单(对象)。

仅仅从JVM层面来看,压力在于:每日百万交易,那么每天就会在JVM中创建上百万 支付订单对象。

支付订单对象的频繁创建。

那么问题来了:

  • 支付系统需要部署多少台服务器
  • 每台服务器需要多大内存
  • 每台机器上的JVM需要分配多大的堆内存空间
  • 给JVM多大的内存空间,才能保证可以支撑那么多的支付订单在堆内存中的创建,而不会导致内存不够而直接崩溃

继续思考:

1. 支付系统每秒钟需要处理多少笔订单

要解决线上系统最核心的一个参数,也就是JVM堆内存大小的合理设置,我们首先第一个要计算的,就是每秒钟我们的系统要处理多少笔支付订单

一些假设:

  1. 假设每天100万个支付订单,那么一般用户交易行为都会发生在每天的高峰期,比如中午或者晚上。

  2. 假设每天高峰期大概是几个小时,用100万平均分配到几个小时里,那么大概是每秒100笔订单左右,咱们就以每秒100笔订单来计算一下好了。

  3. 假设我们的支付系统部署了3台机器,每台机器实际上每秒大概处理30笔订单

2. 每个支付订单处理耗时?

​ 如果用户发起一次支付请求,那么支付需要在JVM中创建一个支付订单对象,填充进去数据,然后把这个支付订单写入数据库,还可能会处理一些其他的事情。

继续假设分析:

  1. 假设一次支付请求的处理,包含一个支付订单的创建,大概需要1秒钟的时间。

  2. 大体上你的脑子里可以出现的一个流动的模型,应该是每台机器一秒钟接收到30笔支付订单的请求,然后在JVM的新生代里创建了30个支付订单的对象,做了写入数据库等处理。

  3. 接着1秒之后,这30个支付订单就处理完毕,然后对这些支付订单对象的引用就回收了,这些订单在JVM的新生代里就是没人引用的垃圾对象了。

  4. 接着再是下一秒来30个支付订单,重复这个步骤。

3. 每个支付订单对象需要多大的内存空间

你只要记住一个Integer类型的变量数据是4个字节,Long类型的变量数据是8个字节,还有别的类型的变量数据占据多少字节。

一般来说,比如支付订单这种核心类,你就按20个实例变量来计算,然后一般大概一个对象也就在几百字节的样子。

我们算他大一点好了,就算一个支付订单对象占据500字节的内存空间,不到1kb。

对象占用内存的估算

对象由对象头(4byte),对象体(引用类型 4byte 实例数据 Integer占4字节,Long占8字节 /基础类型),padding区(补足8byte)

其中对象头组成由,markword(8字节)+kclass(开启压缩4字节,默认开启。否则为8字节,注意指向metadata)+如果是数组,记录长度(4字节)

4. 每秒发起的支付请求 对内存的占用

对于一台机器而言,每秒大约30个支付请求。

30* 500字节=15000字节≈ 15kb

5. 对完整的支付系统内存占用需要评估

之前的分析,全部都是基于一个核心业务流程中的一个支付订单对象来分析的,其实那只是一小部分而已。

真实的支付系统线上运行,肯定每秒会创建大量其他的对象,但是我们结合这个访问压力以及核心对象的内存占据,大致可以来估算一下整个支付系统每秒钟大致会占据多少内存空间。

其实如果你要估算的话,可以把之前的计算结果扩大10倍~20倍。也就是说,每秒钟除了在内存里创建支付订单对象,还会创建其他数十种对象。

那么每秒钟创建出来的被栈内存的局部变量引用的对象大致占据的内存空间就在几百KB~1MB之间。

然后下一秒继续来新的请求创建大概1MB的对象放在新生代里,接着变成垃圾,再来下一秒。

循环多次之后,新生代里垃圾太多,就会触发Minor GC回收掉这些垃圾。这就是一个完整系统的大致JVM层面的内存使用模型。

支付系统的JVM参数怎么设置

支付系统 机器配置:2 Core 4G / 4 Core 8G

4G内存的话,其实比较紧凑。

8G内存参考的JVM配置:

-Xms3G -Xmx3G -Xmn2G

新生代内存适当大一些,因为支付订单对象属于存活时间很短的对象。

每个合格的工程师,都应该在上线系统的时候,对系统压力做出预估,然后对JVM内存、磁盘空间大小、网络带宽、数据库压力做出预估,然后各方面都给出合理的配置。

(案例二)每日百万交易的支付系统,JVM栈内存与永久代大小又该如何设置?

永久代大小

一般设置几百MB,基本都是够用的。

栈内存大小

线程私有的栈内存空间,默认 512KB -> 1MB

知识补充

Tomcat为什么要打破双亲委托机制

tomcat需要破坏双亲委派模型的原因:

(1)tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离

(2)同一个第三方类库的相同版本在不同web应用可以共享

(3)tomcat自身依赖的类库需要与应用依赖的类库隔离

(4)jsp需要支持修改后不用重启tomcat即可生效 :为了上面类加载隔离和类更新不用重启,定制开发各种的类加载器