JVM 主要由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区,内存分区),Execution Engine(执行引擎),Native Interface(本地库接口)
下图可以大致描述 JVM 的结构
JVM 是执行 Java 程序的虚拟计算机系统,执行过程:
首先需要准备好编译好的 Java 字节码文件(即class文件),计算机要运行程序需要先通过一定方式(类加载器)将 class 文件加载到内存中(运行时数据区),但是字节码文件是JVM定义的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解释器(执行引擎)将字节码翻译成特定的操作系统指令集交给 CPU 去执行,这个过程中会需要调用到一些不同语言为 Java 提供的接口(例如驱动、地图制作等),这就用到了本地 Native 接口(本地库接口)。
ClassLoader:负责加载字节码文件即 class 文件,class 文件在文件开头有特定的文件标示,并且 ClassLoader 只负责class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
Runtime Data Area:是存放数据的,分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)。几乎所有的关于 Java 内存方面的问题,都是集中在这块。
Execution Engine:执行引擎,也叫 Interpreter。Class 文件被加载后,会把指令和数据信息放入内存中,Execution Engine 则负责把这些命令解释给操作系统,即将 JVM 指令集翻译为操作系统指令集。
Native Interface:负责调用本地接口的。他的作用是调用不同语言的接口给 JAVA 用,他会在 Native Method Stack 中记录对应的本地方法,然后调用该方法时就通过 Execution Engine 加载对应的本地 lib。原本多用于一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。
1.jvm的装入和配置
2.装载jvm
3.初始化JVM,获得本地调用接口
4.运行Java程序
写好的 Java 源代码文件经过 Java 编译器编译成字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到 Java 虚拟机中解释执行,最后通过操作系统操作 CPU 执行获取结果
JRE(Java Runtime Environment)是Java应用程序的运行环境,它是Java开发工具包(JDK)中的一个组成部分。JRE包含了Java虚拟机(JVM),以及用于执行Java程序所必需的类库和其他支持文件。
与JDK不同,JRE不包含Java开发工具,它只提供了运行Java程序所需的组件。因此,如果只是想运行Java程序而不是进行Java开发,那么只需要安装JRE就可以了。
Stack(虚拟机栈):每个线程在Java虚拟机中都有一个私有的栈,用于存储方法调用的局部变量、操作数栈、返回值和异常处理信息。每当一个方法被调用时,一个新的栈帧会被压入该线程的虚拟机栈中,栈帧包含了该方法的局部变量、操作数栈、返回地址等信息。当该方法执行完毕时,对应的栈帧会被弹出栈,控制权返回到调用该方法的方法中。
Heap(堆):堆是Java虚拟机中所有线程共享的内存区域,用于存储动态分配的对象和数组。当程序需要创建一个对象时,Java虚拟机会在堆中分配一块足够大的空间来存储该对象,对象在堆中的位置由Java虚拟机自动管理。
Method Area(方法区):方法区也是所有线程共享的内存区域,用于存储类的信息、静态变量、常量池等数据。在方法区中,每个类都有一个运行时常量池,用于存储字面量和符号引用,包括类、方法、字段等。方法区还包括Java虚拟机加载的所有类的代码、类的静态变量、类的字节码等信息。
PC Register(程序计数器):程序计数器是一块较小的内存区域,用于记录线程正在执行的Java虚拟机字节码指令的地址。每个线程都有自己的程序计数器,并且在任何时候只有一个方法正在被执行,程序计数器就指向该方法的下一条指令。
Native Method Stack(本地方法栈):本地方法栈与虚拟机栈类似,但是它存储的是Java虚拟机调用本地方法(使用非Java语言编写的方法)时的相关信息。和虚拟机栈一样,每当一个本地方法被调用时,一个新的栈帧就会被压入本地方法栈中,当该方法执行完毕时,对应的栈帧也会被弹出栈。
没有程序计数器,Java程序中的流程控制将无法得到正确的控制,多线程也无法正确的轮换。
在Java虚拟机中,类的字节码以及类的相关信息存放在方法区或永久代(Permanent Generation,JDK7及之前版本)中。
局部变量存放在Java虚拟机栈中,每个线程在Java虚拟机栈中都有一个私有的栈帧(Stack Frame),用于存储方法调用的局部变量、操作数栈、返回值和异常处理信息。
准备过程:初始化插入式注解处理器。
解析与填充符号表:
词法、语法分析,将源代码的字符流转变为标记集合,构造出抽象语法树。
填充符号表,产生符号地址和符号信息。
插入式注解处理器的注解处理:
在Javac源码中,插入式注解处理器的初始化过程是在initPorcessAnnotations()方法中完成的,而它的执行过程则是在processAnnotations()方法中完成。这个方法会判断是否还有新的注解处理器需要执行,如果有的话,通过JavacProcessing-Environment类的doProcessing()方法来生成一个新的JavaCompiler对象,对编译的后续步骤进行处理。
分析与字节码生成:
标注检查,对语法的静态信息进行检查。
数据流及控制流分析,对程序动态运行过程进行检查。
解语法糖,将简化代码编写的语法糖还原为原有的形式。
字节码生成,将前面各个步骤所生成的信息转化成字节码。
注意:注解处理中又可能会产生新的符号,如果有新的符号产生,就必须转回到之前的解析、填充符号表的过程中重新处理这些新符号
加载-验证-准备-解析-初始化-使用-卸载
加载类:Java虚拟机会首先加载该类的字节码,并在方法区中创建一个运行时数据结构,用于存储该类的所有信息,包括类的名称、方法信息、字段信息等。
分配内存:Java虚拟机在堆(Heap)中为该类的对象分配内存空间。堆是Java虚拟机中所有线程共享的内存区域,用于存储对象实例。
初始化零值:Java虚拟机会对新分配的内存空间进行零值初始化。基本类型变量会被初始化为0,而引用类型变量会被初始化为null。
执行构造函数:Java虚拟机调用该类的构造函数,初始化该对象的所有属性。构造函数是一个特殊的方法,用于初始化对象实例。
返回对象引用:对象初始化完成后,Java虚拟机返回该对象的引用,该引用可以被赋值给一个变量,或作为参数传递给方法等。
在栈外,元空间占用的是本地内存。
Java虚拟机(JVM)的类加载器(ClassLoader)负责将Java类文件加载到JVM中,使得这些类能够被JVM使用。在Java中,类加载器主要分为三种类型:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)
启动类加载器负责加载JVM自身需要的基础类库,如java.lang、java.util等。扩展类加载器负责加载JVM扩展的类库,如JDBC驱动程序等。应用程序类加载器负责加载应用程序的类和资源文件。除了这三种标准类加载器外,开发者还可以自定义类加载器来满足特定需求,如热部署等。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
其工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
这种委派模型的好处是可以避免重复加载类,使得Java应用程序的类加载更加高效和安全。
在Java内存运行时区域的各个部分中,堆和方法区这两个区域则有着很显著的不确定性:一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理
1.引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。
2.可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
分代收集理论:
它建立在两个分代假说之上:
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储
1.标记清除算法:
最早出现也是最基础的垃圾收集算法是“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
但是如果可回收对象很多的话会产生大量时间开销。
2.标记复制算法
为了解决标记-清除算法面对大量可回收对象时执行效率低的问题,将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉
3.标记整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存
ParNew 垃圾回收器,它按照 8:1:1 将新生代分成 Eden 区,以及两个 Survivor 区。某一时刻,我们创建的对象将 Eden 区全部挤满,这个对象就是挤满新生代的最后一个对象。此时,Minor GC 就触发了。
在正式 Minor GC 前,JVM 会先检查新生代中对象,是比老年代中剩余空间大还是小。
老年代剩余空间大于新生代中的对象大小,那就直接Minor GC,GC完survivor不够放,老年代也绝对够放;
老年代剩余空间小于新生代中的对象大小,这个时候就要查看是否启用了“老年代空间分配担保规则”。
老年代中剩余空间大小,大于历次Minor GC之后剩余对象的大小,进行 Minor GC;
老年代中剩余空间大小,小于历次Minor GC之后剩余对象的大小,进行Full GC,把老年代空出来再检查。
开启老年代空间分配担保规则只能说是大概率上来说,Minor GC 剩余后的对象够放到老年代,所以当然也会有万一,Minor GC 后会有这样三种情况:
Minor GC 之后的对象足够放到 Survivor 区,皆大欢喜,GC 结束;
Minor GC 之后的对象不够放到 Survivor 区,接着进入到老年代,老年代能放下,那也可以,GC 结束;
Minor GC 之后的对象不够放到 Survivor 区,老年代也放不下,那就只能 Full GC。
Full GC会“Stop The World”,即在GC期间全程暂停用户的应用程序。