一、JDK体系结构与跨平台特性介绍
P1:跨平台的根本就是JVM
二、JVM内存模型剖析
代码清单:
1 public class Math { 2 3 public static final int initData = 123; 4 5 public int compute(){ //一个方法对应一个栈帧内存区域 6 int a = 1; 7 int b = 2; 8 int c = (a + b) * 10; 9 return c; 10 } 11 12 public static void main(String[] args) { 13 Math math = new Math(); 14 math.compute(); 15 } 16 }
P1:宏观角度来看JVM组成:
1)类装载子系统:虚拟机把目标类的数据从class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型
2)运行时数据区:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域(程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区)
3)字节码执行引擎:Java虚拟机的执行引擎是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式,执行引擎在执行字节码的时候通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行),所有的Java虚拟机的执行引擎都一致:输入的都是字节码文件,处理过程是字节码解析过程,输出的是执行结果
P2:运行时数据区:
程序计数器:当前线程所执行的字节码的行号指令器,字节码执行引擎工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流程的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成,线程私有,程序计数器直接互不影响
虚拟机栈:虚拟机栈与线程生命周期一样,线程私有,先进后出的结构,每一个方法被执行的时候,Java虚拟机都会创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程:
1)局部变量表:用于存储JVM八大内置基本数据类型和对象引用(reference类型)以及returnAddress类型,容量是以变量槽(Slot)为最小单位,一个Slot可以存放一个32bit的数据类型,只有long和double类型占用两个Slot,其他只占用一个Slot,局部变量表的结构就像是一个数组,索引0的位置始终是this
2)操作数栈:操作数栈也是一个栈结构,代码执行的每个指令动作都要在操作数栈完成,赋值、运算等,和局部变量表一样操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32bit数据类型所占的栈容量为1,64bit数据类型所占的栈容量为2。举个例子来描述操作数栈的运行:例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈
3)动态连接:Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候会被转换为直接引用,这种转化被称为静态解析。另外一部分将在运行期间都转换为直接引用,这部分就成为动态连接。
4)方法出口:方法被调用时的位置,当方法退出时,恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整程序计数器的值以指向方法调用指令的后面一条指令
本地方法栈:为虚拟机使用到的本地(native)方法服务,是线程私有的
Java堆:Java堆又被成为GC堆,是垃圾收集器管理的内存区域,可划分为新生代(Eden、From Survivor、To Survivor 8:1:1)和养老代(Old Generation 占整个堆区的2 / 3),是线程共享区域
方法区:class文件的常量池被加载到内存时就在方法区被成为运行时常量池,方法区包括常量、静态变量、类元信息(代码),是线程共享区域。jdk1.8之前是永久代,jdk1.8之后是元空间,所属内存为OS的直接内存,虽然是OS的直接内存,但JVM仍然可进行管理
三、使用jvisualvm工具观察对象内存流转过程
P1:对象从新生代到养老代的过程:
1)新出生的对象会存放在Eden区,当Eden区达到一定饱和度时就会触发YoungGC
2)根据Gc Root来标记存活对象,将Eden区和From Survivor区的存活对象移动到To Survivor区,清空Eden区和From Survivor区,并且对象年龄+1,最后From Survivor和To Survivor进行交换
3)当对象对象年龄达到一定阈值时,对象会被移动到养老代,当养老代达到一定饱和度就会触发Full GC,当GC过后养老代仍然不足以存放对象时,就会报错,程序中断
4)每个垃圾回收器在进行垃圾回收的时候都会触发STW机制
P2:jvisualvm工具的使用:
代码清单:
1 public class HeapTest { 2 3 byte[] data = new byte[1024 * 100]; //110KB 4 5 public static void main(String[] args) throws InterruptedException { 6 ArrayList<Object> heapTests = new ArrayList<>(); 7 while (true){ 8 heapTests.add(new HeapTest()); 9 Thread.sleep(10); 10 } 11 } 12 }
执行代码
使用命令行输入jvisualvm打开工具
观察堆情况
四、聊一聊Gc Root与STW机制
P1:什么是Gc Root:
1)虚拟机栈中(局部变量表)的引用对象
2)方法区中的静态属性引用的对象
3)方法区中的常量引用对象
4)本地方法栈的JNI(Native方法)的引用对象
P2:什么是STW机制:
stop-the-world机制,全部的工作线程停止工作,只有GC线程进行工作
五、JVM内存参数设置
P1:内存参数设置:
SpringBoot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):
1 java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar demoservice-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,达到该值就会触发FullGC进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要FullGC,这是非常昂贵的操作,如果应用在启动的时候发生了大量的FullGC,通常都是由于永久代或者元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,这两个值都设置为256M。
六、日均万级订单预估调优
P1:预估调优:
每14秒触发一次YoungGC,每次YoungGC都会有60MB的对象正在执行任务中,所以不会被判定为垃圾对象,根据动态年龄判断机制,该60M对象会移动到老年代,本次预估调优增加新生代内存大小,来尽可能的减少触发进入老年代的机制,从而减少FullGC
结论:尽可能让对象在新生代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃圾回收