一)JVM是如何运行的?
1)在程序运行前先将JAVA代码转化成字节码文件也就是class文件,JVM需要通过类加载器将字节码以一定的方式加载到JVM的内存运行时数据区,将类的信息打包分块填充在运行时数据区;
2)但是字节码文件是JVM的一套指令运行规范,并不能直接交给底层的操作系统来执行,因此需要特殊的命令解析器,也就是JVM的执行引擎会将字节码翻译成底层的操作系统指令也就是0 1的二进制操作系统指令数据交给CPU执行,因为操作系统只认机器码,不认识字节码
3)执行引擎在进行执行的过程中也会调用其它语言的接口,比如说调用本地库接口调用本地方法来实现整个程序的运行;
JVM=类加载器+运行时数据区+执行引擎+本地库接口
1)本地方法接口:简单来说就是一个本地方法就是一个JAVA调用,非JAVA代码的一个接口,方法体不是由JAVA代码写的,设置优先级之类的,简单来说一个本地方法就是一个JAVA调用非JAVA代码的接口,一个本地方法的方法并不是由JAVA方法实现的,比如说C,本地接口的作用就是融合不同的编程语言为JAVA所用。本地方法不是抽象方法,native方法是存在方法体,但是abstract不存在方法体,标识符native可以和其它所有的java标识符连用,但是不可以和abstract连用
2)因为本身JAVA的实现非常简单,但是有些层次的任务使用JAVA实现就非常不容易了,或者对于程序执行的执行效率很在意的时候,问题就出现了
2.1)需要和JAVA外部的环境进行交互:有的时候JAVA应用需要和外部的环境进行交互,这是本地方法存在的主要的原因,JAVA需要和底层的操作系统,交互系统或者是某一些硬件交换信息的时候,本地方法提供了一些交互机制
1)类加载器:加载class字节玛的内容到内存中
2)运行时数据区:负责管理JVM使用到的内存,比如说创建对象和销毁对象
3)方法区:常量,域信息,只有HotSpot虚拟机才有
4)执行引擎:将字节码文件中的内容解析成机器码,同时使用即时编译优化性能
5)本地方法接口:调用本地已经编译的方法,比如说虚拟机中已经提供好的C/C++的方法
6)翻译字节码:针对于字节码的指令进行解释执行
7)JIT编译器:针对于热点代码进行二次编译(将字节码中的字节码指令编译成机器指令)并将其缓存起来缓存在方法区中
字符串常量池的底层实现是依靠C++的map来实现的,C++的hashmap也是需要存放局部变量的,存放C++本地方法的方法调用和局部变量
JMM:一种内存模型,为了提升CPU的读写效率,充分利用CPU资源
二)类加载子系统,类加载的过程=类的生命周期
双亲委派模型
1)沙箱安全机制:自己写的类不会加载,这样便可以防止JAVA的核心API不会被修改
2)避免类的重复加载:当父亲已经加载之后,子类就没有必要再加载一次
类加载子系统的作用:是一种JAVA虚拟机提供给应用程序去实现获取类和接口字节码的技术,类加载器只是参与加载过程中的字节码获取并加载到内存的这一部分
类的生命周期描述了一个类加载使用卸载的过程,加载---链接---初始化----使用----卸载
一)loadding:
a)根据包的全限定包名+类名通过不同的渠道来找到对应的.class文件加载到内存中;
b)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,就是将字节码的信息存放到方法区里面,方法区就是用来存放已被加载的类信息,常量,静态变量
c)在方法区中生成这个类的类对象,作为方法区中这个类的各种数据的访问入口;
二)链接:
2.1)验证:class文件是以特定的文件符开始的,校验内容是否满足JAVA虚拟机规范;
a)文件格式验证:验证文件是否已特定字符开头,就是以特定的二进制文件开头
b)原信息校验:就是对一些基本信息进行校验,比如说类必须有父类
c)验证程序执行的语义:比如说方法中的指令执行中跳转到不正确的位置
d)符号引用验证:例如说有没有类中访问private修饰的方法
e)版本号检测:如果返回值是true,代表验证成功,就是检测JDK的版本
2.2)准备:为静态变量分配内存并设置初始化值为0值,是默认值的初值,就是防止程序员写出脑残代码,比如说给一个a没有赋初值,如果程序员进行后续操作打印a;
final修饰的静态变量,在准备阶段就直接复制初始值了,因为在编译期的时候直接就可以确定值,不会针对于实例变量进行初始化,实例变量会随着对象一起被分配到JAVA的堆里面
下面是准备阶段,下面分别是两个变量在内存中的状态
2.3)解析:解析所作的操作就是将常量池中的符号引用替换成直接引用
符号引用就是在字节码文件中使用编号来访问字符串常量池中的内容,而直接引用不再使用编号,而是使用内存地址来直接访问具体的数据
3)初始化阶段:执行静态代码块中的代码并且会给静态变量赋初值
其实本质上初始化就是在执行字节码部分的中的clinit部分的字节码指令
java虚拟机针对于class文件采用的是按需加载的方式,也就是说当需要使用该类的时候才会将他的class文件加载到内存中生成类对象,而且家在某一个类的class文件的时候,JAVA虚拟机采用的是双亲委派模型,会把请求交给父亲来处理,是一种任务委派模式
在类加载中使用synchronized加锁,向上委托检查,向下加载
1)避免类的重复加载
2)保护程序安全,防止核心API被随意篡改
在JVM中表示两个Class对象是否是同一个需要满足两个条件
1)两个类的完整类名必须完全一致,全限定包名和类名
2)加载这个类的classloader实例对象必须相同
换句话说,在JVM中,即使这两个类对象Class对象来源于同一个Class文件,即使一个虚拟机所加载,但是只要加载他们的ClassLoader实例对象不同,两个类对象也是不相同的
三)字节码文件的组成:
1)基础信息:魔数,字节码文件对应的java版本号,访问表示public final以及父类和接口
2)常量池:保存了字符串常量,类或者是接口名,字段名,主要在接口中使用
3)字段:当前类或者是接口声明的字段信息
方法:当前类或者接口声明的方法信息,字节码指令
属性:指的是类的属性,源码的文件名以及类的列表
字节码文件中常量池的作用:避免相同的内容同时定义节省空间,不仅会使文件变得非常大,况且读取也会非常慢,通过常量池节省字节码文件中的一部分空间避免同样的数据出现多次
可以看到字符串的引用存放的是7号的索引,点击常量池的索引,发现又是一个字面量
最后点击25就可以找到最终的字面量了
iconst:将数字放入到操作数栈中
putstatic:将操作数栈中的数据放到静态变量中
如果将代码进行颠倒,clinit字节码指令执行的顺序和java中编写的顺序是一致的
但是为什么字节码文件再进行设计这一块的时候,先通过字符串的引用找到字符串,再来通过字符串找到字面量呢?能不能直接通过字段来找到字面量呢?因为JAVA里面的字符串解析并加载中,需要将String类型加载到字符串常量池中
从下到上查找是否加载过,再从上向下进行记载
下面来看一下哪几种方式会导致类的初始化:
1)当访问到一个类的静态变量或者是静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化
2)调用Class.forName(String className)获取到这个类的类对象的时候
3)new一个该类的对象的时候
4)执行Main方法的当前类
添加-XX:+TraceClassLoading参数可以打印出加载并且初始化的类
下面程序的输出结果是:
执行main函数,况且类加载只会执行一次,所以静态代码块也只会执行一次,先进行类加载DACBCB
clinit方法在特定的条件下不会出现,如下面几种情况是不会执行初始化指令的,也就不会生成clinit方法,在下面的情况下不会执行初始化操作
1)没有静态代码块况且没有静态变量赋值语句;
2)有静态变量的声明但是没有赋值语句,public static int a在类加载的准备阶段就会赋值成0
3)静态变量的定义使用final关键字况且这份变量会在准备阶段直接进行初始化
1)直接访问父类的静态变量,不会触发到子类的初始化,子类的clinit方法执行前会先执行父类的clinit方法
2)声明一个类以后,内部至少会存在一个这个类的构造器,也就是一定会出现init方法
访问父类的初始化变量只会初始化父类,因为a只是在父类中,此时打印的是a=1
第五步就是为了防止多个线程多次加载同一个类,从下面的代码中而可以看到类加载中的静态代码块只会执行一次,就相当于是一个加锁的过程,一个类在内存中加载一次即可,方法区在JDK1.8使用的是元空间,会使用直接内存缓存起来了,也就是说JAVA虚拟机在执行类加载的时候只会执行一次,只会调用一次clinit()方法;
非法的前向引用:当一个定义的变量出现在静态代码块之后,是可以在静态代码块中赋值的,但是是不可以打印这个静态代码块的
四)如何打破双亲委派模型?
五) JAVA的运行时数据区
1)网络中的数据和硬盘上面的数据要想能够被CPU运算,需要先把数据加载到内存中,CPU直接交互的对象就是内存,内存充当着磁盘和CPU的桥梁,一个JVM实例对应着一个RunTime实例,内存是非常重要的系统资源,是硬盘和CPU的中间仓库以及桥梁,承载着操作系统和应用程序的实时运行,JVM的内存布局规定了JAVA在运行过程中内存申请,分配和管理的策略,保证了JVM的高效稳定运行;
2)JAVA虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些随着虚拟机的启动而创建,随着虚拟机的退出而销毁,另外一些则是和线程是一一对应的,这些线程对应的数据区域会随着县城开始和结束而创建或者销毁
3)线程是一个程序中的运行单元,JVM允许一个应用有多个线程并行的执行
在Hotspot虚拟机JVM里面,每一个线程都和操作系统本地线程直接映射,当一个Java线程准备好执行以后,此时一个操作系统的本地线程也会创建,Java线程终止以后,本地线程一会回收,操作系统负责所有的线程的安排调度到任何一个可用的CPU上,一旦本地线程初始化成功,他就会调用Java线程的run()方法
4)如果使用jconsole或者是任何一个调试工具,都可以看到在后台有很多线程在运行,这些后台线程不包括调用main线程以及所有这个main线程自己所创建的线程
java虚拟机规范白皮书是给JVM开发厂商去看的,指导开发厂商实现JVM,默认的JVM是hotSpot
1)程序计数器:当前线程执行的字节码指令的地址,CPU的个数有限,但是任务很多,CPU会频繁进行线程切换,某一块程序会一直经历执行,暂停,再继续执行,就需要有个东西来记录当前线程执行到哪一步,用于存储当前线程执行的执行的字节码的指令的地址,在多线程环境下,程序计数器用于实现线程切换,保证线程恢复执行的时候能够继续从正确的位置开始执行代码
2)JAVA虚拟机栈:用于存储JAVA方法调用和局部变量(方法内部调用的变量),局部变量的生命周期是和方法的生命周期是一模一样的,当方法执行完成,方法调用出栈,局部变量也会销毁,为什么JAVA虚拟机栈也是线程私有的呢?因为线程执行方法的局部变量只会有线程本身使用,当前线程执行方法的局部变量,其他线程是不会使用到这个变量,使用到这个方法
JAVA虚拟机栈,每一个线程在进行创建的时候会创建一个JAVA虚拟机栈,它的内部保留的是一个一个的栈帧,一个栈帧就对应着一个JAVA方法,对应是一次一次的方法调用就对应着栈帧的入栈和出栈操作,是线程私有的,它本身对应的是一次一次的方法调用,生命周期是和线程保持一致的,它是主管JAVA程序的运行,保存方法中的局部变量(8种基本数据类型和对象的引用地址),部分结果以及参与方法的调用和返回,JAVA虚拟机栈随着线程的创建而创建,随着线程的销毁而销毁;
六)JAVA中的程序计数器:被执行引擎来进行解释执行
1)程序计数器是一块很小的内存空间,几乎可以忽略不记,它也是运行速度最快的内存区域
2)在JVM规范中,每一个线程都有着它自己的程序计数器,是线程私有的,生命周期和线程的生命周期保持一致
3)任何时间一个线程只会有一个方法在执行,也就是说所谓的当前方法程序计数器会存储对应的线程正在执行的JVM指令地址,他是程序控制流的指示器,分支,循环,异常处理线程恢复等基础功能都是需要这个程序计数器来完成,字节码解释器工作的时候就是按照改变这个计数器的值来选取下一条需要执行的字节码指令
4)他也是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemory的区域
5)程序计数器就相当于是行号指示器,相当于是迭代器和游标
1)操作局部变量表和操作数栈
2)将字节码指令翻译成CPU指令
1)使用PC寄存器存储字节码指令地址有什么用呢?
为什么使用PC寄存器来记录当前线程执行的地址呢?
CPU需要不停的进行切换各个线程,这时候切换回来之后,就得知道下一条从哪里开始继续执行,JVM的字节码解释器就需要通过改变PC寄存器的值来确定下一条该执行啥样的字节码
CPU时间片就是CPU分配给各个程序的时间,每一个线程被分配一个时间段,称作是它的时间片,在宏观上可以同时打开多个应用程序,每一个程序并行同时运行,但是在微观上,由于只是有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每一个线程交替执行
串行:用户线程和垃圾回收线程不能同时执行,排队执行,同一个时间点只能有一个线程执行,垃圾回收线程只有一条执行逻辑
并行:线程可以并行同时地去执行
并发:一个CPU核心快速的切换几个线程,让他们串行执行,看着像是并行
并行:垃圾回收线程可以并行同时地去执行,可以有多条,但是执行用户线程的程序用户线程必须是一个停止的状态
并发:垃圾回收线程和用户线程是同时执行,同时不一定是并行,还有可能是交替执行,不会是得用户线程出现Stop The World
七)JAVA中的栈:
栈的特点:栈是一种快速有效的分配内存方式,访问速度仅仅次于程序计数器
JVM对于JAVA虚拟机栈的操作只有两个:每一个方法执行都伴随着进栈,压栈和入栈
执行方法完成之后的出栈操作,栈和程序计数器不存在垃圾回收问题,虽然会溢出,但是不需要GC,资源会自动释放
1)每一个线程都有自己的栈,栈中的数据都是依靠栈帧为基本单位格式进行存储的,在这个线程上每一个执行的方法都是对应着一个栈帧,栈帧是一个内存区块,是一个数据集,维持着方法执行的各种的存放的数据信息,一个方法的执行对应着栈帧的入栈,一个方法的结束对应着一个栈帧的出栈,方法执行完成就出栈,况且一个栈帧不可以调用另一个栈帧
这里面的抛出异常是未处理的异常,会沿着栈向下抛出,最后交给main函数
变量的分类:
按照数据类型划分分为基本数据类型和引用数据类型
按照在类中声明的位置来进行划分:
1)成员变量:在使用之前都默认经历过初始化赋值
静态成员变量也叫做类变量:在链接的准备阶段会给类变量赋初值并分配内存空间,在初始化阶段给类变量进行赋值或者是静态代码块赋值
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值;
2)局部变量:在使用之前必须要显示赋值,否则编译不通过
JAVA虚拟机规范中允许JAVA栈的大小是动态的或者是固定不变的,
1)如果采用固定大小的JAVA虚拟机栈,那么每一个线程的JAVA虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过虚拟机栈允许的最大容量,JAVA虚拟机将会抛出一个StackOverFlower异常;
2)如果JAVA虚拟机栈可以进行动态扩展,并且在尝试扩展的过程中无法申请到足够的内存,或者在进行创建新的线程的时候没有足够的内存去创建对应的虚拟机栈,那么JAVA虚拟机将会抛出一个OutOfMemory异常,是整体虚拟机内存都不够的情况下才执行;
可以使用-Xss选项来进行设置现成的最大栈空间,栈的大小直接决定了函数调用的最大可达深度,是以字节为单位,-Xss256K
1)栈溢出的情况:StackOverFlow,当加栈帧的时候,栈空间不足,会发生StackOverFlow
2)调整栈大小,就能保证栈不溢出吗?
能,当进行有限递归的情况下,可以解决;
不能,对于死循环来说,循环递归只能增加递归的深度,但是最终还是会溢出,只能降低递归最后出现的时间,增加栈的大小,对于有限的递归来说,可能会避免堆溢出;
3)垃圾回收不会涉及到虚拟机栈,程序计数器没有GC也没有error,虚拟机栈没有GC出栈就是垃圾回收,但是存在error,本地方法栈不存在GC,存在error,方法区会存在GC和error,方法区放时间比较长的数据,有error和GC;
4)分配的栈越多越好吗?
避免出现StackOverFlow的概率会降低,整个JVM分配内存空间有限的,可能会挤占其他的空间
堆和栈有什么区别?
1)存放的数据内容不同
2)大小不同
3)访问数据性能不同:因为堆很大,进行查询对象的时候需要进行寻址和内存管理,但是栈存放基本数据类型,内存固定,不需要有动态数据的变化,不需要进行访问,再多大部分情况下,从栈上面寻找数据是很快的,不需要寻址和内存管理;
4)功能侧重点不同:堆是JAVA虚拟机的主要存储单位,JAVA中的对象和数组都是保存在这个区域的,而栈式JAVA虚拟机基本的运行单位,也就是说,堆主要解决的是数据的存储,数据怎么放,放在哪里的问题,但是栈主要解决的是JVM程序方法运行的调用关系,以及运算在哪里进行运算在那里进行存储数据,如何处理数据的;
栈是运行时的基本单位,而堆是存储的单位栈解决的是程序的运行问题,就是程序如何执行,如何处理数据,把相关的一些指令通过栈的局部变量表和操作数栈来进行体现,但是主体的数据都放在堆中,左上是局部变量,左下是字节码指令,右边是堆空间
符号引用:符号引用是一种字面上的引用,它使用符号来描述所引用的对象,比如说类名,方法名和字段名,符号引用在编译时期就已经存在了它是一种无法直接定位到内存地址的引用,直接进行编译,还没有进行类加载,就是一个占位符,标记着内存的一个位置
直接引用定义:直接引用就是直接指向对象内存地址的引用,它包括创建对象的new操作符,获取对象的引用或者是实例变量的操作,直接引用在运行时才存在,他是可以直接定位到内存地址的引用,根据内存地址可以直接拿到对象的引用,直接可以定位到字面量的引用地址
字面量:实际的字符串,判断字符串常量池是否存在该字符串的依据,如果key存在,那么直接就把value值赋值给实际的引用,如果没有就现存
从HotSpot虚拟机来说,字符串常量池是依靠C++的HashMap来实现的,key是字符串常量的字面量,value是字符串对象的引用
编译期常量池:在编译期间可以确定的常量
运行期常量池:在运行期间可以确定的常量,String的intern()方法,将动态生成的东西就是放在运行期常量池的
一)栈帧:
一个栈桢的入栈对应着方法的调用,一个栈桢的出栈,对印着方法的执行的结束,如果发生异常,还会将异常抛给方法调用者,栈帧的大小决定了栈中能存放栈帧的个数,每一个栈帧中存放着局部变量表,操作数栈(或者是表达式栈),动态链接是指向运行时常量池的方法引用,方法返回地址是方法正常退出或者异常退出的定义,还有一些附加信息
二)局部变量表:局部变量表又被称之为是局部变量数组或者是本地变量表
对于int double float本身就是数值,char有对应的ASCILL值,可以看作数值,存储可以转化成int,bool类型,8中基本数据类型和引用类型都可以使用数值类型来表示,不会涉及到线程安全问题,局部变量表大小一旦确定下来是不会更改的,本质上来说就是一个数字数组用于存储方法参数和本地的局部变量,这些数据类型包括各种基本数据类型,对象引用以及各种返回值类型,由于局部变量表是建立在线程的栈上面,是线程的私有数据,因此不存在线程安全问题,还有就是局部变量表的大小是在编译时期确定下来的,在方法运行期间是不会修改局部变量表的大小的
方法返回值类型,访问标识是public static,上面包含着方法声明的所有信息
下面字节码指令行号和源代码行号的对应关系
按照变量声明的位置依次占据着索引位置,根据索引位置来使用变量,Descibler:表示变量类型,length:描述当前变量作用域的范围
起始PC:字节码执行的行号,也是表示变量作用域的起始位置,也就是变量声明完以后
起始PC+length=代码的长度=CodeLength
变量槽:this变量存在与普通方法和构造方法的局部变量表,但是静态方法没有this,序号就是局部变量表的位置,引用数据类型是一个槽位;
index:代表变量所占槽位的起始位置
三)操作数栈:底层是使用数组结构来实现的,
3.1)方法执行过程中就是在执行字节码的指令,操作数栈都是临时存储数据,弹出栈栈中的数据也就没了,但是局部变量表中的数据永远都是存在的
3.2)操作数栈是存放临时数据的地方,两个数相加运算都要放在操作数栈,最终结果都放在操作数栈中,也就是在编译期间确定了大小,只能由入栈出栈操作,不能通过索引调用
一些字节码指令向操作数栈中存放数据,也可以从操作数栈中取出数据
3.3)局部变量表是存放方法中局部变量的位置,在编译期间就确定了数组的长度,是在方法中声明的局部变量,局部变量表,方法形参,方法内部定义的变量,底层是依靠数组来实现的,实际上是依靠定义变量的顺序来声明数组下标的
每一个独立的栈桢中除了包含局部变量表以外,还包含着一个先进先出的操作数栈,也可以称之为是表达式栈,操作数栈在方法执行过程中向栈中写入数据或者是提取数据,就是入栈或者是出栈,比如说操作数栈在执行某一些字节码指令的时候,向栈中写入数据也就是将值压入操作数栈中,其余的字节码指令再将将操作数取出操作数栈中,执行复制交换,求和等操作的时候再将它们写回到操作数栈中;
byte short int boolean都以int型来保存
执行引擎要把字节码指令翻译成机器指令再来进行操作操作数栈
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
操作数栈的指令非常多,缓存一般缓存在物理寄存器中,从而来提升CPU的读写效率,执行速度快,就比如说add操作
三)动态链接:指向常量池的方法引用
重点:每一个栈帧内部包含着一个指向运行时常量池的该栈帧所述方法的引用,包含这个引用的目的就是为了支持当前的方法可以实现动态链接,因为JAVA源文件被编译成字节码文件的时候,所有的变量和方法引用都作为符号引用保存在class文件的常量池里面,比如说一个方法调用了另外的其他方法的时候,就是通过常量池中指向的方法的符号引用来表示的,所以动态链接的作用就是为了将符号引用替换成直接引用
大部分字节码指令执行的时候都是要对常量池中的访问,在桢数据区中,就保存着能够进行访问的常量池的一个指针,方便访问常量池
在编译时时常量池里面,包含着类似于键值对的信息,key是符号引用就是带有#的,value是真实的字面量或者是接口信息等真实结构,返回值类型void,数据类型int,父类信息,System结构的加载,比如说很多方法都是没有返回值类型的,那么这些函数就是都可以引用void的符号引用,方法名字,value后面可能还是包含着符号引用,类加载过程中的使用到的信息都作为一个符号声明出来;
运行时常量池:就是为了提供一些符号和常量,便于指令的识别
五)方法返回地址:记录PC寄存器存储的值作为返回地址
PC寄存器的地址值=方法调用者调用该方法的下一条指令地址