字节码文件的组成:
基础信息:魔数,字节码文件对应的java版本号,访问表示public final以及父类和接口
常量池:保存了字符串常量,类或者是接口名,字段名,主要在接口中使用
字段:当前类或者是接口声明的字段信息
方法:当前类或者接口声明的方法信息,字节码指令
属性:指的是类的属性,源码的文件名以及类的列表
字节码文件中常量池的作用:避免相同的内容同时定义节省空间,不仅会使文件变得非常大,况且读取也会非常慢
可以看到字符串的引用存放的是7号的索引,点击常量池的索引,发现又是一个字面量
最后点击25就可以找到最终的字面量了
通过常量池节省字节码文件中的一部分空间,避免同样的数据出现多次
但是为什么字节码文件再进行设计这一块的时候,先通过字符串的引用找到字符串,再来通过字符串找到字面量呢?能不能直接通过字段来找到字面量呢?因为JAVA里面的字符串解析并加载中,需要将String类型加载到字符串常量池中
操作数栈是存放临时数据的地方,两个数相加运算都要放在操作数栈,最终结果都放在操作数栈中,局部变量表是存放方法中局部变量的位置,是在方法中声明的局部变量
下面就是局部变量表,底层是依靠数组来实现的,实际上是依靠你定义变量的顺序来声明数组下标的
istore_i:将操作数栈中的数据取出来放到局部变量表中的对应位置i,那么到底应该放在哪一个位置呢?应该在istrore后面加上一个数组下标,比如说istore_1就会将操作数栈中的内容放到局部变量表中的1号位置,局部变量表中的数据取出来之后,就没了
iload_i:从局部变量表的i位置复制一份取出数放到操作数栈中,最终操作数栈和局部变量表都是会有这个数据的;
iconst_data:将data数据放入操作数栈中
int i=0会拆解出iconst_0和istore_1这两个指令
i_add:将操作数栈中的顶部的两个数据进行相加,并将结果放入到操作数栈中
iinc 1 by 1:将局部变量表中的1号位置加1
JVM是如何执行的?
类加载器:加载class字节玛的内容到内存中
运行时数据区:负责管理JVM使用到的内存,比如说创建对象和销毁对象
方法区:常量,域信息,只有HotSpot虚拟机才有
执行引擎:将字节码文件中的内容解析成机器码,同时使用即时编译优化性能
本地方法接口:调用本地已经编译的方法,比如说虚拟机中已经提供好的C/C++的方法
翻译字节码:针对于字节码的指令进行解释执行
JIT编译器:针对于热点代码进行二次编译(将字节码中的字节码指令编译成机器指令)并将其缓存起来缓存在方法区中
类加载:
1)类加载器子系统负责从文件系统或者是网络中去加载class文件,class文件在文件开头有着特定的文件标识
2)类加载器只是负责class文件的加载,至于它是否可以运行,那么则有执行引擎来决定
3)加载的类信息存放在一块称之为是方法区的内存空间,除了类的信息以外,方法区中还会存放运行时常量池等信息,可能还会包含字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的部分映射)
java虚拟机针对于class文件采用的是按需加载的方式,也就是说当需要使用该类的时候才会将他的class文件加载到内存中生成类对象,而且家在某一个类的class文件的时候,JAVA虚拟机采用的是双亲委派模型,会把请求交给父亲来处理,是一种任务委派模式
在类加载中使用synchronized加锁,向上委托检查,向下加载
1)避免类的重复加载
2)保护程序安全,防止核心API被随意篡改
在JVM中表示两个Class对象是否是同一个需要满足两个条件
1)两个类的完整类名必须完全一致,全限定包名和类名
2)加载这个类的classloader实例对象必须相同
换句话说,在JVM中,即使这两个类对象Class都西昂来源于同一个Class文件,悲痛一个虚拟几所加载,但是只要加载他们的ClassLoader实例对象不同,两个类对象也是不相同的
类加载的相关知识:
类的生命周期描述了一个类加载使用卸载的过程:
加载---链接---初始化----使用----卸载
1)loadding:
a)根据包的全限定包名+类名通过不同的渠道来找到对应的.class文件加载到内存中;
本地的字节码文件+动态代理生成的+网络
b)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,就是将字节码的信息存放到方法区里面,方法区就是用来存放已被加载的类信息,常量,静态变量
c)在方法区中生成这个类的类对象,作为方法区中这个类的各种数据的访问入口
2)链接:
2.1)验证:class文件是以特定的文件符开始的,校验内容是否满足JAVA虚拟机规范;
a)文件格式验证:验证文件是否已特定字符开头,就是以特定的二进制文件开头
b)原信息校验:就是对一些基本信息进行校验,比如说类必须有父类
c)验证程序执行的语义:比如说方法中的指令执行中跳转到不正确的位置
d)符号引用验证:例如说有没有类中访问private修饰的方法
e)版本号检测:如果返回值是true,代表验证成功
class文件的主版本号要大于一个常量,代表所支持的最低版本号,这个常量默认是45代表JDK1.0,JDK的最高版本是52,况且父版本号是0
2.2)准备:为静态变量分配内存并设置初始化值为0值,是默认值的初值,就是防止程序员写出脑残代码,比如说给一个a没有赋初值,如果程序员进行后续操作打印a;
因为final修饰的静态变量,在准备阶段就直接复制初始值了,因为在编译期的时候直接就可以确定值
这里面不会针对于实例变量进行初始化,实例变量会随着对象一起被分配到JAVA的堆里面
2.3)解析:解析所作的操作就是将常量池中的符号引用替换成直接引用
符号引用就是在字节码文件中使用编号来访问字符串常量池中的内容,而直接引用不再使用编号,而是使用内存地址来直接访问具体的数据
3)初始化阶段:执行静态代码块中的代码并且会给静态变量赋初值
其实本质上初始化就是在执行字节码部分的中的clinit部分的字节码指令
iconst:将数字放入到操作数栈中
putstatic:将操作数栈中的数据放到静态变量中
如果将代码进行颠倒,clinit字节码指令执行的顺序和java中编写的顺序是一致的
下面来看一下哪几种方式会导致类的初始化:
1)当访问到一个类的静态变量或者是静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化
2)调用Class.forName(String className)获取到这个类的类对象的时候
3)new一个该类的对象的时候
4)执行Main方法的当前类
添加-XX:+TraceClassLoading参数可以打印出加载并且初始化的类
下面程序的输出结果是:
执行main函数,况且类加载只会执行一次,所以静态代码块也只会执行一次,先进行类加载DACBCB
clinit方法在特定的条件下不会出现,如下面几种情况是不会执行初始化指令的,也就不会生成clinit方法:
1)没有静态代码块况且没有静态变量赋值语句
2)有静态变量的声明但是没有赋值语句,public static int a;
3)静态变量的定义使用final关键字况且这份变量会在准备阶段直接进行初始化
在上面的情况下不会执行初始化操作
1)直接访问父类的静态变量,不会触发到子类的初始化,子类的clinit方法执行前会先执行父类的clinit方法
2)声明一个类以后,内部至少会存在一个这个类的构造器,也就是一定会出现init方法
访问父类的初始化变量只会初始化父类,因为a只是在父类中,此时打印的是a=1
第五步就是为了防止多个线程多次加载同一个类,从下面的代码中而可以看到类加载中的静态代码块只会执行一次,就相当于是一个加锁的过程,一个类在内存中加载一次即可,方法区在JDK1.8使用的是元空间,会使用直接内存缓存起来了,也就是说JAVA虚拟机在执行类加载的时候只会执行一次,只会调用一次clinit()方法
非法的前向引用:当一个定义的变量出现在静态代码块之后,是可以在静态代码块中赋值的,但是是不可以打印这个静态代码块的
从下到上查找是否加载过,再从上向下进行记载
BootStrap:启动类加载器
rt.jar:包含着java.lang中的String类这样就可以使用BootStrap类加载器来加载自己所扩展的类
1)将自己写的类打包成jar包
2)将自己写的jar包放在任意一个目录:C:/data
3)使用命令来进行扩展,选择添加虚拟机参数
ext目录:通用但是不重要
Appclassloader:在用户自己写的类和第三方依赖jar包的字节码文件
如何打破双亲委派模型?
1)使用PC寄存器存储字节码指令地址有什么用呢?
为什么使用PC寄存器来记录当前线程执行的地址呢?
CPU需要不停的进行切换各个线程,这时候切换回来之后,就得知道下一条从哪里开始继续执行,JVM的字节码解释器就需要通过改变PC寄存器的值来确定下一条该执行啥样的字节码