JVM上篇学习笔记
(学习尚硅谷JVM记下的笔记,感谢宋红康老师)
一.JVM概述
①. 什么是JVM?
1.什么是JVM?
- ①. JVM 是 java虚拟机,是用来执行java字节码(二进制的形式)的虚拟计算机
- ②. jvm是运行在操作系统之上的,与硬件没有任何关系
②. Java的跨平台及原理
2.Java的跨平台及原理
- ①. 跨平台:由Java编写的程序可以在不同的操作系统上运行:一次编写,多处运行
- ②. 原理:编译之后的字节码文件和平台无关,需要在不同的操作系统上安装一个对应版本的虚拟机(JVM)
③. JVM的分类
3.JVM的分类
- ①. 类加载子系统
- ②. 运行时数据区 [ 我们核心关注这里 的栈、堆、方法区 ]
- ③. 执行引擎(一般都是JIT编译器和解释器共存)
JIT编译器(主要影响性能):编译执行; 一般热点数据会进行二次编译,将字节码指令变成机器指令。将机器指令放在方法区缓存
解释器(负责相应时间):逐行解释字节码
二.类加载子系统
1、类加载器的作用
类加载器负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Enqine(执行引擎)决定。类加载器只充当一个快递员的角色。
2、类的加载过程
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过类的加载、类的链接、类的初始化
这三个步骤来对类进行初始化。 如果不出现意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或者初始化 :
1.loading加载:通过一个类的全限定名获取定义此类的二进制字节流,再将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java. lang.Class对象,作为方法区这个类的各种数据的访问入口。此步骤主要是创建该class的一个大的实例。
2.链接:链接分三步
- ①. 验证:确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性
- ②. 准备:
- 为类变量分配内存并且设置该类变量的默认初始化值,即零值。
- 不会为实例变量分配初始化
- ③. 解析:
(将常量池中的符号引号转换为直接引用的过程)
3.初始化:
-
初始化阶段就是执行类构造器方法< clinit >()的过程,(构造器方法中指令按语句在源文件中出现的顺序执行)
-
< clinit > ()不同于类的构造器,构造器是< init >()
-
若该类具有父类,Jvm会保证子类的< clinit >() 执行前,父类的< clinit >() 已经执行完成!
-
虚拟机必须保证一个类的< clinit > ()方法在多线程下被同步加锁。
当有static修饰的时候才会有< clinit >()方法,否则没有!
例如下面例子,可以说明是指令按语句在源文件中出现的顺序执行!
public class ClassInitTest { private static int num = 1; static{ num = 2; number = 20; System.out.println(num); //System.out.println(number);//报错:非法的前向引用。 } private static int number = 10; //linking之prepare: number = 0 --> initial: 20 --> 10 public static void main(String[] args) { System.out.println(ClassInitTest.num);//打印2,因为 System.out.println(ClassInitTest.number);//10 } }
3、类的加载器的介绍
- ①. JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
- ②. 自定义类加载器指的是所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
- ③. 无论类加载器的类型如何划分,在程序中我们常见的类加载器始终只有3个,如下所示:
对于用户自定义类来说:默认使用系统类加载器进行加载 。Java的核心类库都是使用引导类加载器进行加载的。
①. 启动类加载器(引导类加载器)
- ①. 这个类加载使用C/C++语言实现的,嵌套在JVM内部
- ②. 它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容),用于提供JVM自身需要的类
- ③. 并不继承自java.lang.ClassLoader,没有父加载器
- ④. 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
- ⑤. 由于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
②. 扩展类加载器(Extension ClassLoader)
- ①. Java语言编写,由sum.music.Launcher$ExtClassLoader实现
- ②. 派生于ClassLoader类
- ③.父类加载器为启动类加载器
- ④.从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
③. 应用程序类加载器(系统类加载器)
- ①. java语言编写,由sum.misc.Launcher$AppClassLoader实现
- ②. 派生于ClassLoader类
- ③. 父类加载器为扩展类加载器
- ④. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- ⑤.该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- ⑥. 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器
4、双亲委派机制
加载类的时候,JVM采用的双亲委派机制。
简单来说就是当需要加载一个类的时候,该加载器不会立即去加载这个类,而是把这个请求交给父类,一层一层往上去委托,直到达到顶类的类加载器,如果父类能够加载成功就返回,父类加载器加载失败,就让子类去加载,一层一层往下去委托。
举个例子:
假如用户自己定义了一个与String 字符串API同名的类,包名也一样,然后其他类调用这个类的时候,其实还是加载的API的那个类,因为调用这个类的时候,该加载器不会立即加载这个类,而是一直向上请求委托,直到顶层的加载器,而顶层的加载器发现它能够加载 JDK中的String字符串API(因为引导类加载器专门加载Java的核心类库),所以就把JDK中的String加载了!加载成功返回,所以自定义的那个类就加载失败了。假如定义了一个父类都不能加载的类,就一直向下委托,让子类去加载,比如定义了一个Person类,父类加载器都不能加载,此时就由默认的系统类加载器加载!
工作原理图如下:
为什么要采用双亲委派机制 ?
1)避免核心API被篡改
2)避免类的重复加载
大家考虑下下面这个代码为什么出错:
public class String {
public static void main(String[] args) {
System.out.println("xixi"); } }
因为什么?因为main方法在String中,要去加载String,但是由引导类加载器加载的时候找到了核心类库中的String,但是核心类中根本就没有main方法,所以报错咯。所以你不要去定义跟核心API一样的包名!
5、沙箱安全机制
三.运行时数据区
灰色的为单独线程私有的,红色的为多个线程共享的。即:
➢每个线程:独立包括程序计数器、栈、本地栈。
➢线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
1.程序计数器(PC寄存器)
-
①. 作用,是用来存储指向下一条指令的地址,也就是即将要执行的指令代码。由执行引擎读取下一条指令
-
②. 特点:①. 是线程私有的 ②. 不会存在内存溢出
-
③. 注意:在物理上实现程序计数器是在寄存器实现的,整个cpu中最快的一个执行单元
-
④. 它是唯一一个在java虚拟机规范中没有OOM的区域
举例如下图:
使用PC寄存器存储字节码指令地址有什么用呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
为什么使用PC寄存器记录当前线程的执行地址呢?
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
PC寄存器为什么设定为线程私有?
为了能够准确记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器。如果多个线程公用一个的话,假如A线程保存的下一个指令地址是2,这时候切换到B线程,保存的指令地址变成了10,切回A线程,这时候A线程就会从10开始执行!这样会造成程序崩溃的!
2.虚拟机栈 (重点)
栈是运行时的单位,而堆是存储的单位。
即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
1. 什么是Java虚拟机栈?
每创建一个线程就会创建一个Java虚拟机栈,其内部保存着很多栈帧(局部变量表、操作数栈、动态链接、方法返回地址、一些附加信息)
解释:
(1). 虚拟机栈和线程是紧密联系的,每创建一个线程时就会对应创建一个Java栈,所以Java栈也是"线程私有"的内存区域,这个栈中又会对应包含多个栈帧**,每调用一个方法时就会往栈中创建并压入一个栈帧,栈帧是用来存储方法数据和部分过程结果的数据结构,每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程** [先进后出]
(2). 栈帧中由如下部分组成:
因为方法一调用方法二,所以一先入栈,以此类推!调用完依次出栈!
栈顶对应的方法就是当前正在执行的方法!
栈的常见异常:栈内存溢出(StackOverflowError)
- 栈帧过多导致栈内存溢出(方法的递归调用,没设置正确停止条件)
- 栈帧过大(栈帧大小>栈内存)
Exception in thread "main" java.lang.StackOverflowError
//sayHello()发生了递归
public class DemoT {
public static void main(String[] args) {
sayHello();
}
public static void sayHello(){
sayHello();
}
}
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
2.栈的运行原理
3.栈帧的元素
局部变量表,操作数栈,动态链接,方法返回地址,一些附加信息!
1.局部变量表(LocalVariables)
局部变量表也被称之为局部变量数组或本地变量表
- ①. 它被定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量(这些数据类型包括各种基本数据类型、对象引用(reference)以及return Address类型)
- ②. 由于局部变量是建立在线程的栈上,是线程私有数据,因此不存在数据安全问题
- ③. 局部变量表所需容量大小是在编译期确定下来的。(并保存在方法Code属性的maximum local variables数据项中,在方法运行期间不会改变局部变量表的大小的)
//使用javap -v 类.class 或者使用jclasslib
public class LocalVariableTest {
public static void main(String[] args) {
LocalVariableTest test=new LocalVariableTest();
int num=10;
test.test1();
}
public static void test1(){
Date date=new Date();
String name="xiaozhi";
}
}
jclasslib反编译说明:
关于slot的理解(引用数据类型(方法的返回地址)占用1个slot)
-
局部变量表最基本的存储单元是slot(变量槽)
-
在局部变量表中,32位以内的类型只占有一个slot(包括引用数据类型),64位的类型(long和double)占有两个slot
byte、short、char在存储前被转换为int,boolean也被转换为int(0表示fasle,非0表示true)
long和double则占据两个slot -
Jvm会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
-
如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可(比如:访问long或double类型变量)
如图:
如果当前帧是由构造方法或者实例方法创建,那么该对象引用this将会放在index为0的slot处
static修饰的方法体里面不能用this,因为this变量不存在于当前方法的局部变量表中。
slot的重复利用:
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
如图:
2.操作数栈(operand stack)
- ①. 每一个独立的栈帧中除了包含局部变量表以外,还包含了一个后进先出的操作数栈,也可以称之为表达式栈
- ②. 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈或出栈
- ③.
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中
- ④.
操作数栈,主要用于保存计算机过程的中间结果,同时作为计算过程中变量临时的存储空间
- ⑤. 操作数栈的具体说明:
代码举例:
步骤:
**1:**寄存器保存的地址为0,局部变量表索引为0的为this,bipush把15压入操作数栈
**2:**寄存器保存的地址为2,istore_1把15从栈中取出来添加到局部变量表索引为1的地方,操作数栈变为空
**3:**寄存器保存的地址为3,bipush把8压入操作数栈
**4:**store_2把8从栈中取出来添加到局部变量表索引为2的地方,操作数栈变为空
5:iload_1获取数组索引为1的值(也就是15)取出来放入操作数栈中(注意,局部变量表可以根据索引来取值,而操作数栈只能出栈和进栈)
**6:**iload_2获取数组索引为2的值取出来放入操作数栈中
**7:**iadd将15和8出栈并且相加后再入栈
**8:**iload_3将23出栈放入索引为3的位置,此时操作数栈变为空
3.动态链接(Dynamic Linking)
- 每一个栈帧内部都包含一个指向运行时常量池Constant pool或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令
- 在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class字节码文件(javap反编译查看)的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么
动态链接的作用就是为了将这些符号引用(#)最终转换为调用方法的直接引用
。
这些就是符号引用:
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
方法的调用(重点)
-
①. 静态链接(早期绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接**(通俗来讲就是在编译器就确定了具体该调用哪个方法)**
-
②. 动态链接(晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
体现了多态
,多态就是这种,只有在传参的时候才知道具体调用哪个方法! -
③. 非虚方法: 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
(静态方法、私有方法、final方法、实例构造器(实例已经确定,this()表示本类的构造器)、父类方法(super调用)都是非虚方法)
-
④. 其他所有体现多态特性的方法称为虚方法
-
⑤. 如下指令要重点掌握
普通调用指令: 1.invokestatic:调用静态方法,解析阶段确定唯一方法版本; 2.invokespecial:调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本; 3.invokevirtual:调用所有虚方法; 4.invokeinterface:调用接口方法; 5.invokedynamic:动态调用指令(Java7新增),动态解析出需要调用的方法,然后执行; 前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。 其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其中invokevirtual(final修饰的除外,因为JVM会把final方法调用也归为invokevirtual指令,但要注意final方法调用不是虚方法)、invokeinterface指令调用的方法称称为虚方法。
关于前四个指令的代码说明: /** * 解析调用中非虚方法、虚方法的测试 */ class Father { public Father(){ System.out.println("Father默认构造器"); } public static void showStatic(String s){ System.out.println("Father show static"+s); } public final void showFinal(){ System.out.println("Father show final"); } public void showCommon(){ System.out.println("Father show common"); } } public class Son extends Father{ public Son(){ super(); } public Son(int age){ this(); } public static void main(String[] args) { Son son = new Son(); son.show(); } //不是重写的父类方法,因为静态方法不能被重写 public static void showStatic(String s){ System.out.println("Son show static"+s); } private void showPrivate(String s){ System.out.println("Son show private"+s); } public void show(){ //invokestatic showStatic(" 大头儿子"); //invokestatic super.showStatic(" 大头儿子"); //invokespecial showPrivate(" hello!"); //invokespecial super.showCommon(); //invokevirtual 因为此方法声明有final 不能被子类重写,所以也认为该方法是非虚方法 showFinal(); //虚方法如下 //invokevirtual showCommon();//没有显式加super,被认为是虚方法,因为子类可能重写showCommon info(); MethodInterface in = null; //invokeinterface 不确定接口实现类是哪一个 需要重写 in.methodA(); } public void info(){ }