3 JVM内存模型
3.1 JVM整体结构
- 说明
- 执行
java Math.class
命令,通过类装载子系统将类信息装载到方法区(元空间),生成class对象; - jvm通过字节码执行引擎执行
Math#main()
方法,分配线程栈/本地方法栈空间,生成的对象放到堆内存; - 线程栈存放的就是一个线程的数据,一个方法对应一个栈帧,栈帧数据包括局部变量表
a=1、b=2、c=3
,操作数栈,动态链接,方法出口; - 程序运行中产出的堆数据放入推内存,通过垃圾回收机制清理堆内存
- 为什么一定要STW,如果不要STW会如何?
STW时就是通过gcroot查找非垃圾对象,当gc线程找到了一部分gcroot为非垃圾对象后,如果存在线程还在运行并且随后执行完成,线程结束后所涉及到的对象都成了垃圾对象,如果刚好这些非垃圾对象存在于之前通过gc线程找到的非垃圾对象,gc线程做的工作就白做了。所以还不如直接停止所有的线程即SWT,来执行gc操作。
3.2 JVM内存模型
3.2.1 运行时数据区
- 方法区(线程共享)
存放jvm加载的类元信息、静态变量、常量信息(运行时常量池信息);
1.8之前为永久代,为jvm内存。1.8之后为元数据区,为物理直接内存,默认不限制大小,具体大小受服务器内存大小限制;
直接内存,也是服务器的物理内存,比如jvm的元数据区。
java代码实现:
ByteBuffer buffer = ByteBuffer.allocate(1000); // 分配在堆内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1000); // 分配在物理内存,直接内存
对比:
堆内存读写效率低,直接内存读写效率高;
堆内存分配空间效率高,直接内存分配空间低;
- 堆(线程共享)
存放对象、字符串常量;
垃圾回收的主要区域,分为年轻代(Eden+Survivor)+老年代;
- 线程栈(属于线程)
java方法执行的地方,属于线程,每一个方法运行的时候都会分配对应的栈帧用于存储相关信息,先进后出(LIFO),方法执行完及时出栈,每一个方法调用是一个栈帧从入栈到出栈的过程;
包括:
局部变量表,存放方法临时变量,包括基本类型和引用变量(值为对象内存地址);
操作数栈,存放计算结果,比如存放a+b的值3;
动态链接,找到子方法,存放的方法内存地址;
方法出口,标识栈帧方法结束,比如从math.compute()
方法何时返回到main方法;
也可能存放对象,当jvm认为方法引用的对象是临时使用,就会放在方法栈,避免放在堆区增大垃圾回收压力,逃逸分析;
- 本地方法栈(属于线程)
jvm调用navtive方法的服务,本地方法实际调用的是c/c++程序,比如windows系统的.dll文件;
- 程序计数器(属于线程)
当前线程执行字节码的行号,类似于操作系统的pc计数器,多线程时保证cpu切换线程回来知道从哪里继续执行;
- 字节码执行引擎
执行字节码的程序;
垃圾回收;
- 类加载器子系统
C++实现,加载类到jvm;
3.2.2 JVM参数设置
- 示例
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
- 参数说明
-Xss:每个线程的栈大小,默认1M;
-Xms:设置堆的初始可用大小,默认物理内存的1/64 ;
-Xmx:设置堆的最大可用大小,默认物理内存的1/4;
-Xmn:新生代大小;
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3;
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存;
- 关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
3.2.3 JVM参数设置实战
日均百万级订单交易系统如何设置JVM参数?
- 说明
- 每日点击上亿次,每个用户点击二三十次,大约日活用户就是500w,如果付费转化率为10%,那么日均就有订单50w单,高峰大促活动时几分钟(比如10分钟)就会产生50w单,每秒就有1000多单;
- 三台服务器,每台服务器配置为内存8G,那么每台服务器处理300单/秒;
- 如果每个订单对象是1KB(大约可以存500个字符),每秒就有300KB的对象生成,还有其他对象放大20倍,还有查询对象再放大10倍,最后每秒有300KB2010=60M对象产生,这些对象1秒后变为垃圾对象;
- 默认年轻代与老年代比例为1:2,大概为1G,先将年轻代大小调大为2048M,让对象尽量在年轻代中被回收
- 结论
尽可能让对象都在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收;