爱看书的阿东

赐他一块白色石头,石头上写着新名

深入理解JVM - 分代的基本概念

深入理解JVM - 分代的基本概念

目录

[TOC]

前言

​ 本次讲述jvm分代模型的基础概念,这个专栏会由浅入深的不断构建起来,循序渐进,和上一篇一样,是非常基础的内容

​ 其实这一篇才是对象分配的内容:深入理解JVM虚拟机 - jvm的对象分配策略 会发现里面有很多东西需要消化,而这一篇会讲一些基础的内容。

概述:

  1. 讲述JVM的基础分代模型以及版本升级的处理。
  2. 对象分配的基础概念和知识。
  3. 长期存活的对象是如何躲过垃圾回收进入到老年代
  4. jvm内存的核心参数解释以及基础配置

JVM的分代模型

​ JVM的基础分代模型:年轻代,老年代,永久代。这里需要注意内存模型通常是和垃圾回收期相辅相成的,现代的垃圾收集器已经十分复杂了,甚至已经没有了分代的概念,我们所讲的新生代老年代是最初设计的一些理念,很多人学到更为先进的垃圾收集器可能会蒙蔽,比如堆内存怎么变成一块一块的了,以前不是说堆内存只有新生代和老年代这两块完整大的空间呢?

  • 年轻代:大部分的对象都是朝生夕灭的。同时JVM总是把对象优先分配在新生代,并且新生代触发的垃圾回收通常被称为 Minor GC。

  • 老年代:属于长期存活的对象贮存地区,由新生代晋升而来。老年代在通常情况下占有堆中最大的一块内存空间。老年代触发的垃圾回收通常被称为 Full GC

  • 永久代:注意永久代不等同于方法区,主要存放一些静态常量或者存放.class类信息,方法区是可以被垃圾回收的,但是触发的条件十分苛刻,同时里面最常用的常量池以及在JDK8之后挪到了堆中,将永久代并且改名为元空间。

    为什么要把老年代拉进来讲呢?其实永久代的设计是一个失败的设计,JDK8废弃永久代同时用使用本地内存的元空间来替代,大大减少了永久代溢出的可能,因为已经不使用虚拟机的内存了,而是直接使用本机的内存存储,还有一个好处是永久代也不会再去抢堆内存了。

对象分配的基础了解

​ 下面基础了解一下对象分配的基础概念,这些概念可能在学习JAVA的时候就已经接触过了,所以也都是简单提一下:

  1. 大部分正常对象优先在新生代分配。在垃圾回收线程开启之后,会将长期存活对象晋升到老年代存储

  2. 即使是静态变量存在于方法区当中的对象,实例对象也是在堆中分配。但是只有被栈帧局部变量使用的时候才会触发初始化

  3. 在堆上分配的依然优先选择在新生代分配,当长期存活之后会晋升老年代长久贮存内存。

  4. 当新生代的内存空间占满的时候,会触发minor gc。老年代空间被占满之后会触发Full GC,同时Full GC

下面用一张图解释对象分配的基础概念:

​ 我们以下的代码为例简单讲解对象分配的方式。

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

private static final Properties properties = new Properties();

public static void main(String[] args) throws IOException {
InputStream resourceAsStream = OneWeek.class.getClassLoader().getResourceAsStream("app.properties");
properties.load(resourceAsStream);
System.out.println("load properties user.name = " + properties.getProperty("user.name"));
}/*运行结果:
load properties user.name = 123

#app.properties:
user.name=123
*/
}

对象分配的细节

​ 首先,当线程开启的时候,首先会加载并且初始化OneWeek.class对象,同时将Main()方法压入到虚拟机栈中,同时创建栈帧以及局部变量表等内容。

​ 然后,执行字节码引擎执行字节码里面的指令,根据代码可以看到,方法首先会拿到当前类的class文件,并且调用当前类加载器加载app.properties这个文件到内存当中,注意在加载的过程中会创建char[]数组存储加载的内容,以及创建文件IO流读取文件等操作,这部分的对象都是优先分配在新生代的。

​ 当程序计数器执行到:properties.load(resourceAsStream);这一行代码对应的字节码指令的时候,会发现Properties.class没有加载,同时又发现他是一个静态对象,所以会把当前的对象引用分配到方法区进行贮存,注意方法区存放的是对象的引用不是对象的实例,实例依旧优先分配在新生代。但是这里为什么直接划分到老年代了呢?因为我们知道这个静态常量如果被其他的类引用,那么可以算作是长期存活对象,那么长期存活的对象迟早是要进入到老年代的,所以图中直接划分到老年代了。

​ 同时我们也可以发现如果新生代在垃圾回收之后存在长期存活的对象,会在垃圾回收之后自动晋升到老年代进行存储。

​ 特别要注意我们平时new出来的对象都是强引用。哪怕是栈帧局部变量只被使用过一次对象的引用随着栈帧回收,也是不会立马回收的,而是要等到垃圾回收线程开启之后被回收掉。

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

  1. 如果对象年龄到达15岁,会自动进入到老年代。
  2. 大对象进入新生代如果无法容纳,也会直接进入老年代。
  3. 老年代也会触发垃圾回收,会把没有人引用的垃圾对象清理掉
  4. 在满足某些垃圾回收器晋升机制或者满足一些特定条件的情况下,新生代对象会提前进入老年代。

​ 下面分别说明一下这四个点是如何来的:

​ 什么是对象年龄?对象年龄就是在JVM运行的时候,新生代中的对象只要每躲过一次垃圾回收,内部的引用计数器就会把当前年龄的对象+1,当对象的年龄累加到15之后,该对象在下一次垃圾回收之后就会晋升到老年代。当然此时并不是高枕无忧了,当老年代也被占满的时候如果当前对象已经没有被GC ROOT引用了,也还是会被当做垃圾回收的。

​ 大对象最典型的案例就是大字符串或者很大的字节数组,因为需要占用 连续的内存空间,如果新生代无法容纳,那么毫无疑问是需要老年代作为兜底放到老年代直接存放,至于具体参数后续的文章会一一解释,这里了解基础概念即可。

​ 老年代什么时候会触发垃圾回收的操作?条件毫无疑问是老年代放不下对象了,那么老年代为什么会满的,上一段我们说过老年代的对象都是从新生代来的,所以毫无疑问是新生代来到老年代发现老年代放不下了,所以老年代此时就会进行垃圾回收了,老年代的回收叫做Full GC。

​ 看完上面这些,我们需要考虑的是 新生代进入老年代的时机,为什么要考虑这个东西,我们来分析一下:

​ 首先是大对象,大对象进入新生代发现新生代放不下,如果老年代也发现放不下就直接Full GC了?这未免也太悲观了,万一垃圾回收之后放下来了,那不是白白浪费性能,不合适。其次,新生代一定要等到自己满了才进入老年代么,这样未免又太乐观了,因为万一新生代总有一些存活对象活在“等待区”(survior区)又不肯进入老年代,中间赖着不走,那么这一片区域反而失去了他的价值,所以也是不合适的,不如提前进入老年代。

jvm内存的核心参数:

​ 分代的核心参数如下,需要注意的是要注意区分大小写,输错会导致参数不生效:

  • -Xms:java堆内存的大小

  • -Xmx:java堆内存的最大大小

  • -Xmn:java堆当中的新生代大小,扣除新生代剩下就是老年代的内存大小

  • -XX:PermSize:永久代大小(JDK8废弃,被替换为:-XX:MetaspaceSize)

  • -XX:MaxPermSize:永久代最大大小(JDK8废弃,被替换为:-XX:MaxMetaspceSize)

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

-xms和-xmx用来限定java堆的总大小以及扩张的最大大小,但是通常会设置为一样的参数,因为扩容需要stop world极大的影响系统性能。

-Xmn:是新生代的空间大小,老年代会自动根据总的堆大小 - 新生代大小算出来。

-xx:permsize和-xx:maxpermsize。会限制永久代大小和最大的大小,通常情况下设置为256M够用

- Xss 参数限制 每一个线程的栈内存大小。其实就是每一个线程对应虚拟机栈的大小,注意这区域不能太大,当然也不能太小。

  • 注意到了jdk1.8之后,这两个参数被替换为-XX:MetaspaceSize 以及 -XX:MaxMetaspceSize

启动参数的设置?

​ 在IDEA当中的启动参数设置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-Xms1024m
-Xmx2048m
-XX:ReservedCodeCacheSize=500m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=50
-ea
-XX:CICompilerCount=2
-Dsun.io.useCanonPrefixCache=false
-Djava.net.preferIPv4Stack=true
-Djdk.http.auth.tunneling.disabledSchemes=""
-XX:+HeapDumpOnOutOfMemoryError
-XX:-OmitStackTraceInFastThrow
-Djdk.attach.allowAttachSelf=true
-Dkotlinx.coroutines.debug=off
-Djdk.module.illegalAccess.silent=true

Tomcat和spring boot启动的时候参数如何设置

  1. Tomcat在bin的Catalina 下面进行参数配置。

  2. Spring boot 直接在vm.options里面加入虚拟机参数即可

方法区的对象到底会不会被回收?

  • 首先该类的所有实例对象都已经被回收

  • 加载该类的classLoader类加载器已经被回收

  • 对该class对象已经没有任何引用。

满足上面这些条件就可以被回收,可以发现方法区的回收条件十分的严格。

总结:

​ 本文讲述了JVM的分代模型,新生代,老年代,接着,我们对于JVM对象在分代里面分配的一些基础概念,比如对象优先分配在老年代,对象年龄晋升到老年代以及垃圾回收之后长期存活对象进入老年代,同样,JVM也存在一些特殊的判断机制让新生代提前进入老年代,这些都是十分重要的优化,在后续的系列文章中会深入讲解。

写在最后

​ 了解分代的概念以及熟悉JVM的内存模型是非常重要的,因为现代垃圾收集器不断进化以及复杂甚至放弃分代的理念,十分有必要了解分代的历史以及分代的进程,同时不分代势必会是未来趋势。

支付系统的调优实战

支付系统的大致业务:

​ 需要处理支付的订单请求,同时需要根据商户的支付请求,从微信等平台划款到电商公司到账户去。

​ 整个支付到交易流程如下:

  1. 用户提交支付请求到商场系统
  2. 商场系统提交支付订单到支付系统
  3. 支付系统回调,让用户指定付款方式
  4. 用户完成支付,支付系统处理三方支付请求渠道,同时返回支付结果。

支付系统等性能瓶颈:

​ 主要问题时每次创建订单都要创建一个订单对象,而百万级的创建订单就是jvm层面的主要压力。

​ 同时引发如下问题:

  • 支付系统需要部署多少台机器
  • 需要多大的内存空间
  • Jvm需要多大的堆内存
  • 给jvm多大的内存空间才可以保证支撑这么多订单在内存创建,而不是导致直接崩溃

支付系统每分钟要处理多少订单

​ 大约每30个订单为15kb左右的空间占用,但是一个支付接口需要创建至少数十个对象。所以总占用大概是每秒回消耗1mb的空间。

​ 按照2核心4g的系统,jvm进程最多分配2g的空间,按照jvm分区的设置,方法区,栈,堆内存这些空间分摊之后,堆空间一般就1G多(JVM本身需要一定多空间运行)。再加上新生代老年代的分区划分,那么实际可以使用的新生代也就几百M

​ 如果每秒创建1mb,60秒就是60M,那么基本上每过几分钟就要minor gc一次,这样会造成频繁的gc。

​ 但是如果换成4核心8G的内存,那么jvm可以扩展到4g,可以将这个时间延长到几个小时gc一次。

性能过小的机器,反面教材

​ 一个百万级别的系统在促销的时候,如何面对千万级的请求量。

​ 新生代因为回收速度跟不上请求速度,导致很多请求在新生代积压,而新生代积压导致后续被移动到老年代,老年代的内存占用不断扩大。如果老年代被占满,那么会触发老年代的垃圾回收,full gc ,Full Gc会导致整个系统肉眼可见的卡顿。

如果自己的系统突然暴增100万的交易,如何分析jvm的内存消耗

​ 不管自己的系统多大或者多小,都要严格去分析一下扩大100倍之后的情况是什么?