JVM内存模型深度剖析
java如何实现跨平台的?
java程序主要通过jvm来实现跨平台的,对每一个操作系统都有一个特定的jvm版本,这些jvm会将同一个java程序,生成对应操作系统的机器码,不同操作系统生成的机器码不同,但是在jvm上运行是相同的,从而保证了java程序的跨平台。
jvm的主要组成部分
jvm包含两个子系统和两个组件。两个子系统为类装载子系统和执行引擎。两个组件为运行时数据区和本地接口。
类装载子系统:根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
执行引擎):执行classes中的指令。
本地接口:与本地方法库交互,是其它编程语言交互的接口。
运行时数据区域:这就是我们常说的JVM的内存。
一个java程序的执行过程:
首先通过编译器把 Java 代码转换成字节码,类装载子系统(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
jvm的内存结构
jvm的内存结构有:堆、栈、本地方法栈、方法区和程序计数器
栈主要是存放局部变量的。栈其实是线程栈,只要有一个线程执行,会在栈内存中分配一个小的线程栈,用来存放线程内部的局部变量。在每个线程栈中,对每个方法都会生成一个“栈帧”,先执行的方法会放在栈底,后执行的方法会放在栈顶。
每个栈帧中还包含:局部变量表,操作数栈,动态链接和方法出口。
局部变量表:存放局部变量
操作数栈:具体的值,做操作的时候用它来作临时空间,执行完后结果会放到局部变量表中
动态链接:动态链接会将方法的符号引用改为指向内存地址的直接引用,这里是用来存地址的值的
方法出口:内层方法返回外层方法的时候,需要知道外层方法执行到第几行了,方法出口就是存储外层方法执行的位置
程序计数器(线程独有):记录代码执行的位置,每执行一行代码会修改一次。目的是为了多线程的情况下,从线程A切换回线程B的时候,能知道线程B已经执行到哪里了
方法区:存常量池,运行时常量池。存常量、静态变量、类信息、如果静态变量为对象类型,则存储一个指向堆的内存地址
堆:存放内存的空间,包含年轻代与老年代
年轻代分为Eden区与s0,s1。默认年轻代占堆的1/3,老年代2/3。Eden:s0:s1=8:1:1
本地方法栈(线程私有):调用本地方法分配的内存(一般来说是C++实现的代码)
堆的gc回收
gc回收的过程:一个对象最初的时候存在Eden区,当Eden区满了后,会触发一次minor gc,会从gc root开始查找,将没有对象引用的对象给回收掉,还有引用的对象会移到s0区,这个还存活的对象的分代年龄会+1;之后Eden区再一次满,又会触发一次minor gc,s0区还存活的对象会移到s1区,分代年龄再+1;之后Eden区再一次满,又会触发一次minor gc,每执行一次minor gc的时候s1区的存货对象年龄都会+1,直到加到阈值(默认15)这个时候s1区的对象会移动到老年代;以此类推,一再触发minor gc,直到老年区存满,则会触发一次full gc。full gc会回收整个堆和方法区。如果full gc后再存对象还是存不下,则会触发OOM异常。
full gc的STW时间很长,minor gc的STW时间较短,但次数较多。
在gc回收的时候会触发STW(stop the world),STW会暂停当前运行的线程,这个时候对用户来说会有明显的卡顿,因此jvm调优的目的就是减少STW的次数。
为什么要STW?
因为每次gc的时候从gc root去查找对象是否存活的计算十分复杂,耗时很长。如果不暂停正在运行的线程,会出现一个对象可能在gc root检查的时候是存活对象,然后检查完后,移动到s0区之后对象执行完毕,被释放掉了。这个时候就浪费了之前的gc root检查。会导致当前的gc root检查出来的结果不正确。
jvm内存参数
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):
java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar microservice‐eurek a‐server.jar
关于元空间的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。
-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多。
栈的总大小是根据系统内存减去堆大小和方法区大小来计算的。