文章目录
JVM
JVM是Java Virtual Machine (爪哇虚拟机)的缩写。它是虚构出来的计算机,
引入Java虚拟机后,Java语言使用Java虚拟机屏蔽了与具体平台的相关的信息,将java程序生成在java虚拟机上运行的目标代码(class字节码),在不同的平台上不需要重新编译,就可以在多种平台上不加修改的运行。
简介
常见的Java虚拟机
- SUN ClassicVM:第一款商用java虚拟机,1996年1月jdk1.0中带的java虚拟机,只能使用纯解释器的方式来执行java代码
- Exact VM:准确式内存管理,编译器和解释器混合工作以及两级即时编译 ,只在Solaris平台发布
HotSport VM
:即时编译,节约了时间和存储,称霸武林(重要)- KVM:简单,轻量,高可以执行,主要在手机平台使用
- j9:IBM Technology for java virtual Machines IT4J
- dalvik:不能直接指向class文件,寄存器架构,执行dex文件,由class文件转化而来
- MicrosoftJvm:只能在windows平台运行
JVM内存模型的分区
- 堆(Heap)
- 方法区(Method Area)
- 程序计数器(Program Counter Register)
- 虚拟机栈(VM Stack)(就是我们常说的栈)
- 本地方法栈(Native Method Stack)(调用原生代码,比如C写的底层代码)
JVM(HotSport VM)结构概览
灰色部分(Java栈,本地方法栈和程序计数器)是线程私有,不存在线程安全问题,橙色部分(方法区和堆)为线程共享区。
类加载器
类加载器将class文件字节码内容加载到内存中,并将这些内容转换成方法区中运行的数据结构。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由执行引擎Execution Engine决定
类加载器的分类
- 启动类加载器 (BootstrapClassLoader):
也叫根加载器,用于加载$JAVA_HOME/jre/lib/rt.jar包内的class文件。rt.jar是Java基础类库,包含Java运行环境所需的基础类
- 扩展类加载器(ExtClassLoader)<JDK1.9后改名为标准扩展类加载器(PlatformClassLoader)>:
由Java语言实现,用于加载$JAVA_HOME/jre/lib/ext/.jar目录下的class文件 - 应用程序类加载器(AppClassLoader):
用于加载当前应用的classpath的所有类
,也就是我们自己写的那些Java代码.
可以用以下代码,查看类所属的加载器
public class Test {
public static void main(String[] args) {
System.out.println(Test.class.getClassLoader());
}
}
用户自定义加载器
通过继承Java.lang.ClassLoader抽象类自定义一个类加载器
加载器的关系图
通过代码验证:
public class Test {
public static void main(String[] args) {
Class<Test> testClass = Test.class;
System.out.println(testClass.getClassLoader());
System.out.println(testClass.getClassLoader().getParent());
System.out.println(testClass.getClassLoader().getParent().getParent());
}
}
运行结果:
类加载步骤
分为三布
- 加载 Loading
通过一个类的全类名获取其二进制字节流,然年后转换成方法区的运行时数据结构,然后在内存中生成一个代表这和类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。 - 链接 Linking
该过程分为三个阶段:验证、准备、解析-
验证阶段
用于确保加载的Class文件的字节流包含的信息是否符合虚拟机要求,保证其合法性。 -
准备阶段
为类变量(静态变量)分配内存并跟俊对象类型赋对应的默认值。这里并不包含常量,常量在编译的时候就分配了,准备阶段会显示其对应的常量值,而实例变量不会在这和阶段初始化
-
解析阶段
用于将符号引用转换为直接引用。
实例代码:
-
public class Test { public static void main(String[] args) { String str = "hello"; System.out.println(str); } }
在保存Calss出打开cmd,使用javap -v Test.class
命令查看其对应的字节码
可以看到常量池中有许多符号引用(比如#2),解析阶段就是将其解析为直接引用(比如#2表示 字符串常量hello)的过程
- 初始化Initialization
该阶段就就是执行类的构造方法的过程。
clinit(构造器方法)不是类的构造器,不需要我们自己定义,是javac编译器自动搜集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
一个类不包含类变量(静态变量)和静态代码块,那么它的字节码中就不会有构造器方法()。
示例代码:
public class Test {
private static int aaa = 1;
static {
aaa = 200;
System.out.println(Test.bbb);
bbb = 300;
System.out.println(Test.bbb);
}
private static int bbb = 2;
public static void main(String[] args) {
System.out.println(Test.bbb);
}
}
运行结果
0
300
2
为什么呢?
可以看出bbb为类变量,在链接阶段会分配内存并且会赋默认值0,所以会首先输出0,然后赋值为300,在赋值为2。所以在静态代码块中可以进行输出修改。
变量的初始化顺序
常量
:在编译时就会被分配具体值静态变量
:在类加载的过程的初始化阶段会被赋值(在链接阶段时分配内存空间,并赋默认值)成员变量
:在对象初始化时赋初值
双亲委派机制
所谓的 双亲委派机制
就是:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此。只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
实例:
在src/main/java目录下新建java.lang包,然后在该包下新建一个String类
代码如下:
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("helo");
}
}
程序输出结果:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public staticvoid main(String[] args) 否则 JavaFX
应用程序类必须扩展javafx.application.Application
什么意思呢?明明在类中我们有main方法为什么说找不到呢?
所以上面的例子中,AppClassLoader委派给它的父类ExtClassLoader去加载,ExtClassLoader又委托给它的父类BootstrapClassLoader去加载。BootstrapClassLoader从它的加载路径$JAVA_HOME/jre/lib/rt.jar 下找到了 java.lang.String 类,即rt.jar包下的String类,而该类里并没有main方法,所以便抛出了如上异常。
优点:
采用双亲委派的好处是:不管那个加载器加载这个类,最终都是委托给顶层的启动类(根)加载器进行加载。这样就保证了使用不同的类加载器最终得到的都是同一个string对象,所以我们自定义的java类并不会去污染jdk自带的类,这种保护机制也叫做沙箱安全机制
程序计数器
程序计数器
又叫做PC寄存器,每个线程都有一个程序计数器,是线程私有的一小块内存空间。
它是一个指针,指向方法区中的方法字节码,用来存储指向下一条指令的地址,也就是将要执行的指令代码,由执行引擎读取下一条指令。
如果执行的是一个Native
方法,那这和计数器的值为undefied
CPU需要不停地切换各个线程,有了程序计数器后,当CPU切换回来后,我们就可以知道接着从哪开始继续执行程序.
虚拟机栈(Java栈)
每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个栈帧
。每个方法被执行的时候,java虚拟机都会同步创建一个栈帧。
- Java虚拟机栈式线程私有的,它的生命周期与线程相同(同生共死)。
- 栈帧包括局部变量表(8种基本数据类型,对象引用地址)、操作数栈、动态链接、方法返回地址和一些附加信息。
- 每一个方法被调用直至执行完毕的过程,就对相应这一个栈帧在虚拟机从入栈到出栈的过程。
JVM对虚拟机栈的操作只有压栈(入栈)和出栈操作,遵循FILO原则;在一个活动线程中,一个时 间点只会有一个活动的栈帧,即当前正在执行方法对应的栈帧(当前栈帧);如果一个方法调用了另一个方法,那么对应的新的栈帧将会被创建出来,放在栈顶,成为新的当前栈帧
Java方法执行结束正常退出和抛出异常这两种情况会导致栈帧被弹出(退出)
栈帧内部结构
每个栈帧包括5个组成部分:局部变量表(Local Variables)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、方法返回地(Return Address)和一些附加信息
局部表白量表
局部变量表式一个数字数组,用于存储方法参数和方法体内部的局部变量
例如:
public class Test {
public static void main(String[] args) {
Date date = new Date();
int count = 1;
}
}
通过javap -v XXX.class查看字节码:
如果是没静态方法,则变量表中会多一个this变量,就是当前对象,这就是为什么实例方法种可以使用this调用对象。
操作数栈
每一个独立的栈帧中除了包含局部变量表外,还包含一个FILO的操作数栈,用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。
实例:
public class Test {
public static void main(String[] args) {
double a = 1;
int b = 2;
double c = a+b;
}
}
通过javap -v XXX.class查看字节码:
①处:stack操作栈个个数为4,这里代表的需要使用的最大操作栈,double类型栈两个,最后运算的时候两个double类型的运算,所以需要四个操作栈,通常int,char,byte,short等占一个,double占两个。locals代表的是局部变量的大小有两个double所以是6.
②处:是对应的命令,这里我们大概讲解一下,dconst_1前面的d代表这是一个double类型的值,将其压入栈,然后dstore_1,将栈中的double取出存进局部变量区位置为1,2,3步类似,dload_1,从局部变量区的1处出去变量压入栈。
③处:局部变量表
动态链接
在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池里,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
方法返回地址
存放调用该方法的pc寄存器的值,一个方法的结束分以下两种方式:
- 正常执行结束
- 出现未处理异常,非正常退出
无论哪一种方法退出时,在方法退出后都返回到该方法被调用的位置
- 方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法的指令的下一条指令地址;
- 异常退出时,返回地址需要通过异常表来确定。
一些附加信息
比如对程序调试提供的支持信息
虚拟机栈大小的调整
Java虚拟机规范允许虚拟机栈的大小固定不变或者动态扩展。
- 固定情况下:如果线程请求分配的栈容量超过Java虚拟机允许的最大容量,则抛出
StackOverflowError
异常; - 可动态扩展情况下:尝试扩展的时时候无法申请到足够的内存;或者在创建新的线程的时候没有足够的内存去创建对应的虚拟机栈,则会抛出 OutOfMemoryError 异常。
不同平台的虚拟机栈默认大小不同:
- Linux/x64 (64-bit): 1024 KB
- macOS (64-bit): 1024 KB
- Windows: 默认值取决于虚拟内存。
通过-Xss(-XX:ThreadStackSize简写)设置虚拟机栈大小,默认单位为字节。
例如:-Xss1m
虚拟机栈越大则方法调用深度越深
本地方法接口
本地方法接口
(Native Interface)的作用就是为融合不同的编程语言为Java所用,最初是为了融合C/C++程序。
就是java种的native方法
本地方法栈
是为了管理本地方法的调用
堆(Heap)
堆(Heap)
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。堆中保存着所有引用类型的真实信息(即 对象 ),以方便执行器执行。
堆分区
java8
Java8后的堆物理上只分为新生区和养老区,Metaspace(元空间)不占用堆内存,而是直接使用物理内存。
为什么要分区
Java程序中不同对象的生命周期不同,70%~99%对象都是临时对象,这类对象在新生区“朝生夕死”。如果没有分区,GC时搜集垃圾需要对整个堆内存进行扫描;分区后,回收这些“朝生夕死”的对象,只需要在小范围的区域中(新生区)搜集垃圾。所以,分区的唯一理由就是为了优化GC性能。
关于GC(垃圾回收)在另外一个博客详细介绍。
方法区
方法区并不是所谓的存储方法的区域 ,而是供各线程共享的运行时内存区域。它存储了已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
方法区也是一种规范,在不同虚拟机里头实现是不一样的,最典型的实现就是HotSpot虚拟机Java8之前的永久代(PermGen space)和Java8的元空间(Metaspace)。
方法区、堆、栈关系
方法区的内部结构
类型信息
对每个加载的 类型(类calss、接口interface、枚举enum、注解annotation),Jvm必须在方法区中存储以下类型信息:
- 这个类型的完整有效名称(包名+类名)
- 这个类型直接父类的完整有效名(不包括interface和object)
- 这个类的修饰符(public、abstract、finally)
- 这个类型直接接口的一个有序列表(一个类可以实现多个接口)
方法信息
方法信息包含了这个类的所有方法信息(包括构造器),这些信息和其声明顺序一致:
- 方法名称;
- 方法的返回值类型(没有返回值则是void);
- 方法参数的数量和类型(有序);
- 方法的修饰符(public,private,protected,static,final,synchronized,native,
abstract); - 方法的字节码、操作数栈、局部变量表及其大小(abstract和native方法除外);
- 异常表(abstract和native方法除外)。
域信息
域Field我们也常称为属性,字段。域信息包含:
- 域的声明顺序;
- 域的相关信息,包括名称、类型、修饰符(public,private,protected,static,final,volatile,transient)。
静态变量
静态变量就是使用static修饰的域信息。静态变量和类关联在一起,随着类的加载而加载,它们成为类数据在逻辑上的一部分。静态变量也成为类变量,类变量被类的所有实例共享,即使没有类实例时你也可以访问它。
设置方法区大小
方法区的大小决定了系统可以加载多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机则会抛出内存溢出错误
- Java 8:java.lang.OutOfMemoryError: Metaspace
设置方法:
- 设置元空间初始大小:-XX:MetaspaceSize=size
- 设置元空间最大值:-XX:MaxMetaspaceSize=size
默认情况下,在windows平台上,-XX:MetaspaceSize值为21M,-XX:MaxMetaspaceSize值为-1,即没有限制,所以极端情况下如果不断地加载类,虚拟机会耗尽所有可用的系统内存。
方法区的演进
随着JDK的迭代升级,Hotspot中方法区的存储的内容发生了如下变化(上面介绍的方法区的内部结构是经典情况下的,具体还是需要看JDK是什么版本):
版本 | 描述 |
---|---|
jdk1.6及之前 | 有永久代(permanent generation),静态变量存放在永久代上 |
jdk1.7 | 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中 |
jdk1.8及之后 | 无永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍然保存在堆中 |
上面说的静态变量在JDK1.6之前存放在永久代,JDK1.7后移动到堆空间指的是变量本身,变量对应的对象实例一直都是在堆
再简要介绍以下对象的内存结构
HotSport虚拟机中,对象在内存中存储的布局可以分为三块内容:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
谢谢大家支持!谢谢!