关于Java虚拟机的总结和理解

当前主要总结了虚拟机的作用和组成,类的加载机制,方法的加载机制,对异常的处理,垃圾回收等内容

首先介绍一下jdk、jvm、javac三者的关系,jdk包含着jvm和javac,jdk里的jre/bin相当于是jvm而jre下的lib就是jvm运行所需要的类库。javac是编译器,它将Java源码编译成字节码交给jvm,jvm最终将字节码翻译成机器码交给硬件运行。Java源码也能直接翻译成机器码,这里使用javac翻译成字节码是因为jvm只能运行字节码文件。

虚拟机对内存的划分为栈、堆、方法区,栈又包含本地方法栈,Java方法栈,pc寄存器三部分。栈中存放的是基本数据类型对象,自定义对象的应用;堆中存放的是自定义对象本身;方法区用于存放加载后的方法类。栈中的数据私有其他栈不能访问而堆和方法区中的数据为线程共享。此处有个概念问题:栈中的数据私有,栈中存放自定义对象的引用,如果这个自定义对象是全局变量那么全局变量为什么能被其他方法使用?(栈,堆,对象引用,对象本身在线程中的角色是怎么样的)个人的理解是jvm的概念不能等同于Java源码的概念

再说一下类的加载,从class文件到内存中的类需要经过加载、链接、初始化三步;加载是将字节码加载成为类,链接是将类合并到jvm中,初始化是给静态常量赋值并执行方法这个方法后边会介绍。类的加载,过程就是将字节码文件翻译成类,重点介绍一下类的加载器,加载器分为启动类加载器、扩展类加载器、应用类加载器,按照顺序前一个是后一个的父类,启动类加载器加载最基础最重要的类像jre中lib里的类,扩展类加载器加载次重要、通用的类像jre/lib/ext中的类,应用类加载器加载项目中的类。同时还可以自定义类加载器,例如对class文件加密,再利用自定义类加载器对其进行解密;链接,链接分为验证阶段,准备阶段,解析阶段。验证是验证类是否符合jvm的规范要求,准备阶段是为静态变量分配内存,生成当前类的方法表;解析:在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个符号引用(符号引用包括类的名字,目标方法的名字,目标方法的描述)符号引用储存在class文件的常量池中,而解析的目的就是将这些符号引用转换成实际引用,如果符号引用指向一个未被加载的类那么解析会触发这个类的加载。初始化,为标记成常量的字段赋值并执行方法。

方法的概念:如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >

针对内存区间的划分,在这里讲一下经常提到的方法区和常量池,方法区是和堆栈平级的内存区域,存放class加载后的数据(类的版本,接口,方法,字段和常量池)。常量池在本质上也是jvm开辟的一块内存空间,存在于方法区中,常量池分为静态常量池和动态常量池,静态常量池存在于方法区中class文件信息里,主要存放文本字符串、基本数据类型、final常量和符号引用,动态常量池直接存在于方法区里,类加载后jvm将静态常量池里的内容转移到动态常量池里将部分符号引用(静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本)转换为直接引用,其他方法在第一次被调用时转换为直接引用。同时动态常量池里的内容也可以动态添加。还有一个特殊的常量池,字符串常量池 存在于堆中用于存放字符串实例。

jvm对方法的调用,这个过程中主要的矛盾点在于当存在重写、重载时如何选定目标方法,同时是对动态绑定、静态绑定两个概念的理解。对于重载是根据自动装箱,可变参数两种方式来选取目标方法,重写是根据调用对象的类型来确定目标方法。重载属于静态绑定,重写属于动态绑定。静态绑定是指在解析时就能直接识别目标方法的情况,动态绑定是指在运行过程中根据调用者的动态类型来确定目标方法的情况。这里重点介绍一下动态绑定,动态绑定需要知道调用对象的类型和方法表,介绍一下方法表,方法表是在类加载机制里链接部分生成的,方法表的实质是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。然后再说动态绑定的执行过程,执行过程中Java虚拟机获取调用者的实际类型,再在该实际类型的方法表中根据索引值获取目标方法。讲到这而不不得不提一下内联缓存,内联缓存是一项加快动态绑定的优化技术,它能够缓存调用者类型与目标方法的对应关系,下次遇到直接使用缓存中的关系不需要去查看方法表。内联缓存也分单态内联缓存,多态内联缓存,超多态内联缓存

此处有一个问题:方法表,符号引用,实际引用,方法表索引三者的关系,对象调用方法它是怎么拿到索引的(根据类名方法名对应某种关系)

jvm对异常的处理异常分为检查异常和非检查异常,非检查异常像RunTimeException、error,其他的都属于检查异常像io异常,检查异常需要显式的捕获或者通过throws抛出。

在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引,用以定位字节码。当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。对于finally在编译的过程中会生成三个代码块,前两个分别在try和catch的出口,另外一个做为异常处理器,来捕捉try中生成的未被catch捕获的异常和catch中的异常,而且当catch中有异常后,异常处理器会处理catch中的异常try中的异常会被忽略掉,但是Java 7 引入了 Supressed 异常、try-with-resources,以及多异常捕获,弥补了这个不足。

下面说说jvm的垃圾回收机制,jvm里的垃圾回收是指回收死亡对象所占据的堆空间。如何判别对象是否死亡,第一种方式是采用引用计数器,对一个对象的引用数量进行计数当引用数量为零时则判断为对象可以被回收;这种方式不适用循环引用存在缺陷;第二种方式是可达性分析算法,它将一系列GC ROOTS作为初始化的存活对象集合,然后从该集合出发探索所有被该集合引用到的对象,未被探索到的对象即为死亡的可以被回收;可达性算法也有一个缺陷,在多线程情况下,其他线程可能会更新已经访问过的对象中的引用,这样容易造成误报漏报;jvm通过stop-the-World和安全点来弥补这个缺陷;stop-the-World是停止其他非垃圾回收线程直到完成垃圾回收。而安全点是指垃圾回收的合适时机,安全点的实质是一个稳定的可执行状态,在这个状态下Java的堆栈不会发生变化;在这个地方要说明一个点,就是jvm一直是在处理字节码文件,它最终处理的结果是将字节码转化为机器码,因此对内存的各种操作是在执行字节码的过程中完成的,而Java源码是javac编译完成直接交给jvm的对于jvm来说一个方法请求进来也就是进入了字节码文件的方法,然后进行方方面面的处理生成机器码去执行;jvm的解释执行和即时编译都存在安全点的检测,就是在字节码到机器码的过程中进行的垃圾回收。解释执行时逐条插入安全点检测,而即时编辑是HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的回边插入安全点检测;

单独介绍一下垃圾回收的三种方式:清除,把死亡对象占据的内存标记为空闲,并记录在空闲列表中,再创建新对象时,内存管理模块会在空闲列表中寻找空闲内存划分给新对象。这种方式有两个缺点:容易造成内存碎片,堆中对象是连续分布的,如果回收的这段内存过小,新对象就无法使用,再一个缺点是效率低,原因很简单如果是一块连续的内存可通过指针法直接分配内存,而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来为新对象找到合适的内存; 压缩,把存活的对象聚集到内存区域的起始位置,留下一段连续的内存空间,缺点是压缩算法的内存开销;复制,将内存分为两块区域,一直将活的对象复制到另外一个区域(所有存活的对象都分配到新区域后老的区域就空了,然后再次循环)。

你可能感兴趣的:(关于Java虚拟机的总结和理解)