今天来总结一下这个星期所学到的关于JVM的知识。
这星期我看了JVM的内部的概述,先用一张图来进行展示说明:
下面我会逐个进行概述(只是大概了解每个模块的功能,后面会进行后面的总结会进行详细的了解)。
好,在概述这些模块之前,首先需要了解的是Java虚拟机的生命周期? Java虚拟机内部有两种线程,一种是守护线程,另一种是非守护线程。守护线程时Java虚拟机内部自己使用的,比如垃圾回收线程。但是Java程序也可以把任何线程标记为守护线程。非守护线程main()方法的那个线程,只要还有任何非守护线程还在运行,那这个Java程序也在继续运行。当改程序中的所有非守护线程都终止了,Java虚拟机实例也将自动退出。 接下来需要了解一下Java数据类型是什么? Java数据类型分为两种:基本类型和引用类型。基本类型的变量持有原始值,是真正的原始数据,而引用类型的变量持有是对某个对象的引用,而不是对象本身。 看下面这张图: 还有一个类型只在内部使用:returnAddress,是用来实现Java程序中的finally子句的,程序员无法调用这个基本类型。 Java有三种引用类型统称为引用类型:类类型、接口类型以及数组类型,它们都是对动态创建对象的引用。 Java虚拟机还有一个字的概念。在Java虚拟机中最基本的数据单元是字,它的大小是由每个虚拟机实现的设计者来决定的。字长一般需要能够持有byte,short等基本类型,也就是它们至少选择32位作为字长。(这一块我就大致了解有这个一个概念)
下面就真正开始概述虚拟机下各个模块。 首先先来讲类装载子系统。Java虚拟机有两种类装载器:启动类装载器和用户自定义装载器。 装载的顺序: 1)装载——查找并装载类型的二进制数据 2)连接——执行验证、准备以及解析(可选) 1.验证——确保被导入类型的正确性 2.准备——为类变量分配内存,并将其初始化为默认值 3.解析——把类型中的符号引用转化为直接引用 3)初始化——把类变量初始化为正确的初始值
接下来讲方法区。被Java虚拟机装载的class文件类型信息存储在方法区中,也就是说方法区存储关于类的信息,也包括class的static静态信息。 由于所有线程共享方法区,所以他们对方法区数据的访问必须是线程安全的,也就是说假设两个线程访问同一个类,只有一个线程能够去加载它,另一个线程只能等待。方法区也可以被垃圾回收。 虚拟机都会在方法区中存储以下信息: 1.这个类型的全限定名 2.这个类型的直接超类的全限定名(除非这个类是java.lang.Object这个类没有超类) 3.这个类型是类类型还是接口类型 4.这个类型的访问修饰符 5.任何直接超接口的全限定名的有序列表 除了上面列出的基本信息,还有 1.该类型的常量池 2.字段信息 3.方法信息 4.除了常量以外的所有类静态变量 5.一个到类ClassLoader的引用 6.一个到类Class类的引用
常量池:虚拟机必须为每个被装载的类型维护一个常量池,是该类型所用常量的一个有序集合,包括直接常量(String,Integer等常量),还有对其他类型,字段和方法的引用,是通过数组一样用索引进行访问的。
字段信息:对于类型中声明的每一个字段的字段名、字段的类型、字段的修饰符。
方法信息:对于类型中声明的每一个方法的方法名,方法的返回类型、方法参数的数量和类型(按声明的顺序)、方法的修饰符。如果这个方法不是抽象或本地的,还需要记录方法的字节码、操作数栈和该方法的栈帧中的局部变量区的大小、异常表(这些名词后面都会解释)
类静态变量:这些变量只与类有关,跟类的实例无关
指向ClassLoader的引用:虚拟机必须跟踪类是由启动类装载器还是由用户自定义装载器装载的,这是作为方法表中类型数据的一部分保存的。
指向Class类的引用:对于一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个java.lang.Class类的实例,而且虚拟机必须以某种方式把这个实例和存储在方法区中的类型数据关联起来。
方发表:为了提高访问效率的一种数据结构,虚拟机对每个装载的非抽象类都生成一个方法表,它是一个数组,它的元素都是它的实例可以被调用的实例方法的直接引用,包括从那些超类继承过来的实例方法,运行时可以通过方法表快速搜寻在对象中调用的实例方法。
下一个介绍的是堆。Java程序在运行时创建的所有类的实例或数组都放在同一个堆中。一个Java虚拟机只存在一个堆空间,所有线程都共享这个堆,又由于一个Java程序独占一个Java虚拟机实例,因而每个Java程序都有自己的堆空间——它们不会彼此干扰。但是同一个Java程序的多个线程共享一个堆空间,所以要注意同步问题。 多态的实现:当程序运行时需要转化为对某个对象引用为另一种类型时,虚拟机必须检查这种转化是否被允许,被引用的对象会通过方法区查看类信息,确定是否被转化的类型或者是超类型,但是在调用某个实例方法时,虚拟机会进行动态绑定,换句话说也就是它不能按照引用的类型来决定将要调用的方法,而必须根据对象的实际类型。所以,虚拟机必须再次通过对象的引用去访问方法区中的类数据。 堆和方法表的实现:
下一个是程序计数器(这块我感觉无需太了解)。对于一个运行中的Java程序而言,每一个线程都有一个自己的PC(程序计数器)寄存器,它是在该线程启动时创建的,当线程执行某个Java方法时,PC寄存器的内容总是下一条将要被执行指令的“地址”。
下面介绍Java栈。每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈,Java栈以帧为单位保存在线程的运行状态,虚拟机只会对Java栈进行两种操作:以帧为单位的压栈和出栈。 某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈称为当前栈…… 每当线程调用一个一个Java方法时,虚拟机都会在该线程中压入一个新帧,而这个新帧自然也就是当前帧,在执行这个方法时,它用这个帧来存储参数、局部变量、中间运算结果等数据。 Java方法有两种方式完成,一个是return,一个是抛异常,都会将当前帧释放掉,上一个帧就变成了当前帧。 Java栈上的所有数据是私有的,任何线程不能访问另一个线程的栈数据。因此不需要考虑多线程情况下的同步问题,当一个线程调用一个方法时,方法的局部变量保存在调用线程的Java栈中,只有调用方法的线程能够访问那些局部变量。
下一个是重点:Java栈帧。栈帧有三部分组成:局部变量区、操作数栈、帧数据区。 当虚拟机调用一个Java方法时,它从对应的类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入到Java栈中。 局部变量区以字长为单位,从0开始计数的数组,类型为int、float、referece和returnAdress的值在数组中只占一项,而类型为byte、short、char的值在存入数组前都将被转化为int,所以也只占据一项,而long、double的值占据连续的两项。 局部变量区包含对应方法的参数和局部变量,编译器首先按声明的顺序把这些参数放入局部变量数组,看下面两个例子: public static int runClassMethod(int i,long l, float f,double d ,Object o ,byte b){ return 0; } public int runInstanceMethod(char c,double d, short s,double d ,boolean b){ return 0; } 注意在runInstanceMethod()中,局部变量第一个参数为reference,尽管在源代码中没有显式声明这个参数,但是这个参数this对应任何一个实例方法都是隐含加入的,它用来标识调用方法的对象本身。而char等被转化成了int的参数。在存回堆或方法区时才会转回原来的类型。除参数外,Java方法内的局部变量可以按照任意顺序,当出现作用域不同时,比如for循环定义的i和j可以共用一个索引,比如
操作数栈也是一个组织成一个以字长为单位的数组,但是它是通过标准的栈操作——压栈和出栈进行访问的。基本类型的存储方式跟之前的相同。 Java虚拟机没有寄存器(这块我也不懂,不是之前说PC计数器是寄存器么),Java虚拟机的指令主要是从操作数栈中而不是从寄存器中取得操作数的,因此它的运行是基于栈的而不是基于寄存器的。 虚拟机把操作数栈作为它的工作区,比如执行加法就需要:
帧数据区:Java栈帧还需要一些数据来支持常量池的解析、正常方法的返回以及异常派发机制,这些信息都保存在Java栈帧的帧数据区中。 每当虚拟机要执行某个需要用到常量池的指令时,它都会通过帧数据区中指向常量池的zh指针来访问它。常量池中堆类型、字段和方法的引用在开始时都是符号,当虚拟机在常量池搜索时,如果遇到指向类、接口、字段或者方法的入口,假若它们仍然是符号,才会进行解析。 除了常量池的解析,帧数据区还要帮助虚拟机处理Java方法的正常结束和异常终止,若是return无返回值正常结束,则虚拟机必须回复发起调用方法的栈帧,包括设置PC寄存器指向发起调用的方法中的指令+即紧跟着调用了完成方法指令的下一个指令。加入有返回值,虚拟机必须将它压入到发起调用的方法的栈帧中的操作数栈。 当处理异常情况时,帧数据还必须保存一个对此方法异常表的引用,定义了在这个方法的字节码中收catch子句保护的范围,异常表中的每一项都有一个被catch子句保护的代码的起始和结束位置(即try子句内部的代码),当某个方法抛出异常时,虚拟机根据帧数据区对应的异常表来决定如何处理。如果在异常表中找到了匹配的catch子句,就会把控制权转交给catch子句中的代码,如果没有发现,方法会立即异常中止,然后虚拟机使用帧数据区的信息恢复发起调用的方法的帧,然后在发起调用的方法的上下文重新抛出同样的异常。
接下来讲本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈,当它调用的是本地方法时,虚拟机会保持Java栈不变,不在线程的Java栈中压入新的帧,只是简单的动态连接并直接调用指定的本地方法。 该线程首先调用了两个Java方法,第二个Java方法调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。假设这是一个C语言栈,其中有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个函数又调用了第二个C函数,之后第二个C函数又通过本地方法接口回调了一个Java方法,最终这个Java方法又调用了一个Java方法。
之后讲执行引擎。运行中Java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。Java虚拟机的实现可能用一些用户程序不可见的线程,比如垃圾收集器,这样的线程不需要是实现的执行引擎的实例,所有属于用户运行程序的线程,都是在实际工作的执行引擎。
关于本地方法接口,个人暂不学习,所以不做总结。
今天的分享到此结束。致谢正在向光奔跑的自己。