JVM
一 JVM内存区域
1.运行时数据区介绍
根据官方文档显示:JVM内存区域分为6块,分别是:
- The pc Register (程序计数器)
- Java Virtual Machine Stacks (线程栈)
- Heap(堆)
- Method Area (方法区)
- Run-Time Constant Pool (运行时常量池)
- Native Method Stacks(本地方法栈)
如下图:
官方把内存区域分为6块,实际上运行时常量池是方法区的一部分,只不过官方把他单独划分了出来,所以jvm内存区域实际分为5块。如下图:
1.1 栈详细解析
只要有1个线程开始运行我们的main方法,Java虚拟机马上会在线程栈内为我们分配出一大块独立的栈内存空间,用来放我们在线程执行过程中需要用到局部变量的内存空间。下面以代码举例说明。
当线程开始运行main方法,马上会分配出一块自己的线程栈,同时在线程运行main方法时,
会单独开辟出一块main方法专属的栈帧内存区域,用来放main方法自己的局部变量,所以math是存放到main方法的栈帧内存中。在第17行调用了compute方法,Java虚拟机马上又会为compute分配自己专属的栈帧内存区域,用来放compute方法的局部变量,所以a,b,c就是存放到compute方法的栈帧内存中。
**JVM在程序运行时,会为每一个方法分配1个自己专属的内存空间,我们把这一块的内存空间叫做栈帧内存空间。
栈帧内部除了放局部变量外,还有操作数栈、动态链接、方法出口,一共包括这四块。
这四块的作用如下:
- 局部变量:存放局部变量
- 操作数栈:程序在运行过程中存放操作数的一块临时中转空间,例如a=1,那存放的就是1
- 动态链接:程序运行过程中将符号引用转换成符号代码的直接地址
- 方法出口:存放main方法中调用方法结束后的位置
在上面代码图中我们可以看到main方法中有Math对象,Math math = new Math();大家都知道对象一般都存在堆中,但是这个math是存在栈中的,存的是对象在堆中的内存地址。
1.2 程序计数器
程序计数器也是每一个线程独有的,和栈一样,用来存放程序正在运行的那一行的代码内存位置。
可以理解为0-12就是标记程序的位置,而程序将要运行的位置就是存放到程序计数器中的。程序每执行一行字节码执行引擎都会去修改程序计数器的位置。
1.3 方法区
先简单介绍一下方法区的存储,一般常量、静态变量、类信息。在上述代码图中,public static final int initData = 666;public static User user = new User();在这段代码initData和user就是放在方法区。这个静态变量user在方法区中存的是对象在堆中的内存地址。类信息就是程序代码的信息,比如方法名。
1.4 本地方法栈
在Java中看到的native就是本地方法,比如new Thread().start(); 点进源码就可以看到底层调用了本地方法,该start0是由c或c++语言实现的。如下图
1.5 堆
在介绍堆之前,先说明1点,堆和栈是共享的,方法区、本地方法栈、程序计数器是每个线程独有的。
1.5.1 jdk版本堆的区别
** 在jdk1.7及jdk1.7之前,堆分为三块:**
- 年轻代
- 老年代
- 永久代
在jdk1.8及jdk1.8之后,对堆做了部分修改也分为三块:
- 年轻代
- 老年代
- 元空间
1.5.2 年轻代及老年代介绍
年轻代分为三块:
- Eden区
- Survivor0区
- Survivor1区
年轻代和老年代的比例为 1:2
Eden区、Survivor0区和Survivor1区比例为 8:1:1,大多数New出来的对象基本是存放在Eden区的,当然也有种情况就是如果对象太大,Eden区放不下会直接放到老年代中。
具体流程如下:
New 的对象先放在Eden区,如果Eden区空间满了,会触动垃圾回收 MinorGc,一个对象经历过GC之后如果还存活着,他的分代年龄会+1 (分代年龄存在对象头中),存活的对象会放到幸存者 0 区,如果再次触碰垃圾回收,会把幸存者 0 区仍然存 活的对象放到幸存者 1 区,如果再次触碰垃圾回收,会把幸存者 1 区 仍然存活的对象放到幸存者 0 区,直到分代年龄达到15(默认的年龄是 15),此时就会把幸存者 0 区的对象放 到老年代当中,(空的区是 to 区,非空的幸存者区是 from 区)。 当老年代中承受不了新来的对象时,就会触发 FullGC,对老年代进行 垃圾回收,如果垃圾回收老年代仍然承受不了对象时,就会内存溢出OOM(OutOfMemory)。
介绍到这里不得不介绍下STW(Stop The World)的简称,实际上STW就是停掉用户的线程。在GC的过程中就会停止掉用户发起的所有线程,当然时间不会很长,给用户的感觉就是卡顿一下,这对性能是有影响的,JVM调优就是减少MinorGc和FullGC的执行时间和执行次数
最主要的是FullGC。
为什么要有STW机制?
在GC的过程中,目的是找一些非垃圾对象,把存活的对象存到老年代,如果说一次GC过程中,没有STW(没有停掉用户的所有线程),那么会把用户所有存活的对象当作垃圾对象来处理。
2. JVM整体结构及内存模型
二. 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:每个线程的栈大小
-Xms:初始堆大小,默认物理内存的1/64
-Xmx:最大堆大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewSize:设置新生代初始大小
-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。