文章目录
一、JDK体系结构与跨平台特性
JDK体系结构
Java语言的跨平台特性
二、JVM内存模型深度剖析
堆(公共)
堆:new出来的对象一般都存放在这里(有时候也会存放在栈上)
它包括老年代(图中Old区域)和新生代(图中Eden/S0/S1三个统称新生代,分为Eden区和两个Survivor区域),他们默认是8:1:1分配内存
栈(私有)
栈(线程):栈的内存区域存放局部变量,官方叫法是 虚拟机栈(Virtual Machine Stacks ),我个人更喜欢叫 线程栈(每当一个方法运行的时候,栈会给当前线程分一块独立的内存空间,用来存放这个方法需要用到的局部变量的内存空间(栈内存空间))
栈帧
栈帧:一个方法对应一块栈帧区域(main方法执行分配一个栈帧内存空间,compute方法执行分配另一个栈帧内存空间),通过栈帧把不同方法的局部变量隔离开了
数据结构里也有一个栈(FILO),栈帧也是用的这种数据结构,这也符合代码逻辑(main方法先执行,先开辟一个main方法的栈空间,然后调用compute方法,compute的栈空间在main栈空间的上面,等局部方法执行完,局部变量全部释放掉,等于把compute这块栈帧内存空间释放掉,说白了就是出栈,结束之后才会回调main方法,然后走完main方法,先进后出)
栈帧包含了局部变量表、操作数栈、动态链接、方法出口
iconst_1: 将int类型常量1压入操作数栈
istore_1:将int类型值存入局部变量1
iconst_2: 将int类型常量2压入栈
istore_2: 将int类型值存入局部变量2
iload_1: 从局部变量1中装载int类型值
iload_2: 从局部变量2中装载int类型值
iadd: 执行int类型的加法
bipush: 将一个8位带符号整数压入栈
imul: 执行int类型的乘法
istore_3: 将int类型值存入局部变量3
iload_3: 从局部变量3中装载int类型值
ireturn: 从方法中返回int类型的数据
- 首先把常量1压入到操作数栈
- 然后在局部变量表分配一块内存空间给a,从操作数栈中取出1放入到局部变量表中做赋值操作(局部变量0放的是this,调用这个方法的对象,局部变量1放的是a)
- 重复1,2操作执行常量2(至此,程序计数器目前是4)
- 把a的值1和b的值2装载出来(就是从局部变量表取出入栈到操作数栈中)
- 把1和2出栈到cpu内部的缓存,计算好结果3之后再压入操作数栈
- 把10压入操作数栈(10这个常量也占用内存地址,上述代码中分配的是序号8)
- 把3和10出栈到cpu内部的缓存,计算好结果30之后再压入操作数栈
- 重复2,5操作执行计算出来的结果30
- 返回数据
局部变量表
局部变量表也被称之为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类本数据类型、对象引用(reference),以及ReturnAddress类型
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maxmum variables数据项中。在方法运行期间是不会改变局部变量表大小的。
操作数栈
每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈。 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈操作。
注意:main方法中的math也属于局部变量,但是比较特殊,这个new出来的对象是存放在堆中的,栈中存放的是堆中的引用地址
动态链接
在程序运行的时候,把符号引用(方法在常量池中的)转换成符号对应的代码的直接地址(内存地址),也就是符号的直接引用代码,所以这些代码的位置就放在动态链接里(我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另一部分将在每一次运行期间都转化为直接引用,这部分就被称为动态链接。)
方法出口
存放的是一些需要继续往下执行的哪些代码的信息
程序计数器(私有)
存的是即将运行的代码在内存地址中的指针的位置,每次执行完一行代码,字节码引擎会取修改程序计数器的值
设计原因:多线程的时候,一个程序在执行,如果被另外一个优先级更高的线程把cpu时间抢占过去,那么当前线程需挂起,等到恢复执行的时候,需要通过程序计数器去定位执行到哪一行代码了
方法区(公共)
存放了运行时常量池(常量+静态变量+类元信息)
new出来的User是放在堆里的,所以方法区存放的是user在堆中的引用地址
JDK8之前叫永久代,8之后叫元空间(用的是物理内存)
本地方法栈(私有)
本地方法:比如说Thread().start();底层是c或者c++实现的,也就是通过Java区调用一些c语言的实现的。
程序在运行过程中如果需要用到本地方法了,那么分配的内存空间就在本地方法栈中
三、从jvisualvm来研究对象内存流转模型
new出来的对象一般都存放在Eden区(但不一定),假如不停地有对象产生,那么Eden区会放满,放不下的时候就会触发minorGC(总体过程大致如下:会从方法区、栈、本地方法栈中找很多GCRoot(栈中的本地变量,方法区中的静态变量,本地方法栈中的变量),从GCRoot出发,去所有引用对象,直到找到一个对象不再引用任何其他对象,那么这条GCRoot链上的都属于非垃圾对象,将这些非垃圾对象复制到Servior区,而Eden区剩下的对象就是垃圾对象,直接干掉),如果一个对象经历过一次minorGC,他的分代年龄(存放在是对象头中)会+1;如果发生再次MinorGc的时候,会回收Eden区和s0区,存活的对象会复制到s1区,分代年龄再+1;如果再次MinorGC,那么会回收Enen区和s1区,存活的对象会复制到s0区,分代年龄再+1;
当存活的对象在s0和s1区不停复制,分代年龄增长到15的时候,这个对象会被挪到老年代;当老年代放满之后,会先触发full GC(过程与minorGC类似,但是回收的整个堆以及方法区,如果回收不到什么垃圾对象,那就会内存溢出(OOM));
Q1:什么样的对象会被放到老年代
静态变量、静态变量引用的对象,对象池、缓存、缓存 对象,spring容器的对象
四、讲透GC Root与STW机制
GC Root
包含栈中的本地变量,方法区中的静态变量,本地方法栈中的变量
STW(stop the world)机制
在minor GC和Full GC的时候,会触发这个机制;实际上就是停止掉用户发起的所有线程(比如电商用户下单,凡是用户发起的线程),用户会感知到网络卡顿,这种机制对用户体验和网站性能是有一些影响的(JVM性能调优主要就是减少FullGC和Minor GC,主要是full GC,因为收集堆的时间比较长,所以STW时间也会比较久)。
Q2:那为什么要设计这套机制嘛?为什么要停止用户线程
假如没有这套机制,那么出现Full GC的时候,用GC Root去找非垃圾对象的过程中,如果用户线程执行完毕了,那么线程中的局部变量出战了,全部释放掉了,那么栈中的引用信息就没有了,那么之前GC Root中的非垃圾对象就是垃圾对象了;那GC过程就白做了(不同的垃圾收集器不一样)
五、JVM参数设置通用模型
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):
java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
‐jar microservice‐eureka‐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。
栈的内存大小默认是1M,但是死循环的方法会不停地往栈中加入栈帧,那么就会下出现StackOverflowError
-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多