1.Java语言的跨平台特性
2.JVM整体结构及内存模型
JVM是由类装载子系统,运行时数据区,java执行引擎三部分组成的
这三部分是相辅相成共同工作了,不能单纯的说只有执行完某一个部分才由下一个部分来工作,从JVM运行开始,三个类加载器就要开始创建,并且类加载器的一些信息也要放在运行时数据区
1.类装载子系统
.class文件通过类装载系统进行加载,验证等一系列操作,在这个过程中将需要存储到内存的数据放到运行时数据区
类装载子系统也是之前的类加载机制介绍的
2.运行时数据区
1. 虚拟机栈栈: 线程独享 每个线程栈存放执行的方法,每个方法为一个栈帧
2. 程序计数器: 线程独享 存放每个线程中指令执行到了哪一个
3. 本地方法栈: 线程独享 类似于虚拟机栈,只不过这里存放的是Native(调用C++)的方法
4. 堆: 线程共享 对象存放位置
5. 方法区: 线程共享 类信息,常量,静态变量等
类信息(C++的一些对象) 静态变量,如果指针指向的是一个对象,存放的依然是堆中对象的地址
虚拟机栈的结构
每个线程的虚拟机栈中有大量的栈帧,表示每个方法,如main方法等
栈帧中:
局部变量表: 存放每个方法中的局部变量,用一张表存储,当一个变量指向对象时,存放的是堆空间中对象的地址
操作数栈: 当我们进行数值操作时,先将数字压入操作数栈,再进行运算,无论是更新局部变量表还是进行数值运算
例如: a=1,将常量1压入操作数栈,再将1赋值给局部变量表的a
c = b+a,将b,a的值从局部变量表取出,压入操作数栈,弹出操作数栈交给cpu运算,结果压入操作数栈,再赋值给局部变量表
动态链接: 在程序运行的过程中,动态的找寻方法区中方法名所所在的内存地址
方法出口: 存放当前方法应该返回到调用者的哪里
方法区结构
宏观来看,主要我们关注的信息有这些
微观看,如下这些信息.
1. 类进行加载解析等以后,创建出类元信息和静态变量和常量放入到方法区中
2. 运行时常量池 Class文件中的资源仓库
3. 域信息,成员变量信息
4. 方法信息,方法的参数,返回值,局部变量等信息
当类被加载到内存以后.class文件就被打碎,分配到方法区中的各个部分,方法区中不再存放.class的源代码,如下,是把源代码每个部分分开
Last modified 2020-4-22; size 1626 bytes
MD5 checksum 69643a16925bb67a96f54050375c75d0
Compiled from "MethodInnerStrucTest.java"
//类型信息会被加载到方法区
public class com.atguigu.java.MethodInnerStrucTest extends java.lang.Object // 类的全限定名以及父类
implements java.lang.Comparable<java.lang.String>, java.io.Serializable //类实现的接口信息
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER // 类的权限修饰符
Constant pool:
#1 = Methodref #18.#52 // java/lang/Object."<init>":()V
#2 = Fieldref #17.#53 // com/atguigu/java/MethodInnerStrucTest.num:I
#3 = Fieldref #54.#55 // java/lang/System.out:Ljava/io/PrintStream;
...
{
//域信息会被加载到方法区
public int num; // 域名称
descriptor: I // 域类型
flags: ACC_PUBLIC // 域权限
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
//方法信息会被加载到方法区
public com.atguigu.java.MethodInnerStrucTest(); // 方法名称
descriptor: ()V //方法参数及方法返回值类型
flags: ACC_PUBLIC //方法权限修饰符
Code: //方法对应的字节码
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 10: 0
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/atguigu/java/MethodInnerStrucTest;
......
}
3.java执行引擎
用于执行栈中指令,一条一条的指令读取,类似一个cpu,我们的虚拟机栈中操作数栈弹栈进行运算的运算就是由执行引擎进一步完成的
执行引擎执行的并不是.class的源代码,源代码被打碎分配以后,执行引擎如何执行?
我们.class被加载以后,首先会将main方法压入栈中,去执行main方法的方法信息,需要运行时常量池信息就去常量池中取,进行一步一步的局部变量赋值等操作,当执行到其他方法调用时,就会动态链接去寻找这个方法在方法区中的方法信息,一步一步执行.
例如: main函数里面有compete方法,当执行到compete方法时,main函数里面的动态链接会找寻compete方法存放的方法区地址(动态链接),然后为compete创建栈帧,在栈帧中为其局部变量分配在局部变量表中,右执行器执行
1.分代GC模型
2.JVM内存参数分配
堆: JVM内存分配,在我们JVM启动的参数或者由JVM自动分配的一个大小,这个大小在JVM启动以后就不会改变了
使用JVM启动时的默认最大和最小不一样的,可以根据使用情况自动扩缩容,但是最大是不会超过默认上限的,超出OOM(内存溢出)
方法区: 在直接内存分配,比如默认16g硬件内存,除去系统开销可以一直分配的10几G
但是可以调整方法区触发FGC的大小,默认比较小,也会根据FGC的情况弹性扩缩容调整FGC触发的大小
我们可以使用参数MetaspaceSize将触发FGC大小调大并且设置为固定值,也是调优的方式
栈: 在直接内存分配,也是可以分配10几G,栈是线程独立,多个线程就会产生多个栈空间,我们不能控制所有线程栈总和的大小
但是我们可以通过参数调整每单个线程栈的大小,-Xss,把这个调小了那么我们在同等硬件内存大小的情况下,就能运行更多的线程
每个线程栈中有栈帧的概念,栈帧的大小是不能确定的,每个方法一个栈帧
-Xss设置越小,说明一个线程栈里能分配的栈帧就越少,但是对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: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。
4.整体工作模型
public class Math {
public static final int initData = 666;
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
User user = new User();
while (true){
math.compute();
}
}
}
class User{
}
1. 将Math.class放入到类装载子系统,进行加载,验证等,将二进制字节码放入到内存中,变成方法区的运行时常量,方法信息等,并mian压栈
2. 执行器执行main方法的信息
3. 执行到new Math,创建Math的对象分配在堆中
4. 执行到new User时,检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载
5. 如果没有加载,进行双亲委派加载
# 用到那个类将哪个类进行加载,将.class打碎,每个部分按照自己的类型放到运行时常量,静态变量,常量,类元信息等放入到方法区
# 通过栈完成整个代码的执行过程,执行器执行栈中信息
版权声明:本文为weixin_56892092原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。