一 什么是JVM
JDK是Java程序员常用的开发包、目的就是用来编译和调试Java程序的。
JRE是指Java运行环境,也就是我们的写好的程序必须在JRE才能够运行。
JVM是Java虚拟机的缩写,是指负责将字节码解释成为特定的机器码进行运行。
(Java源程序需要通过编译器编译为.class文件,否则JVM不认识)
JVM是Java Virtual Machine(Java虚拟机)的缩写
Java虚拟机(JVM)包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域。
使Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改地运行。
JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。
二 JVM的编译器
在 JVM 中有三个非常重要的编译器,它们分别是:前端编译器、JIT 编译器、AOT 编译器。
前端编译器,最常见的就是我们的 javac 编译器,其将 Java 源代码编译为 Java 字节码文件。JIT 即时编译器,最常见的是 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler,其将 Java 字节码编译为本地机器代码。而 AOT 编译器则能将源代码直接编译为本地机器码。这三种编译器的编译速度和编译质量如下:
- 编译速度上,解释执行 > AOT 编译器 > JIT 编译器。
- 编译质量上,JIT 编译器 > AOT 编译器 > 解释执行。
而在 JVM 中,通过这几种不同方式的配合,使得 JVM 的编译质量和运行速度达到最优的状态。
JVM要运行字节码文件,在这个过程中,JVM会加载字节码文件,将其存入 JVM的内存空间中,之后进行一系列的初始化动作,最后运行程序得出结果。
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域。
JVM 的运行时数据存储区主要包括:堆、栈、方法区、程序计数器等。
而JVM 的优化问题主要在线程共享的数据区中:堆、方法区。
具体划分为如下5个内存空间:
- 栈(虚拟机栈,java栈,JavaStack):存放局部变量。
- 堆:存放所有new出来的东西(所有的对象和它们相应的实例变量/成员变量)和数组。
- 方法区:用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码等信息。
静态变量+常量+类信息(版本、方法、字段等)+运行时常量池存在方法区中
- 程序计数器(和系统相关):记录这个线程运行到哪里了。
- 本地方法栈:与虚拟机栈类似,只是是执行本地方法时使用的。
- java中的字符串在字符串常量区
虚拟机栈:虚拟机栈表示Java方法执行的内存模型,每调用一个方法,就会生成一个栈帧(Stack Frame)用于存储方法的本地变量表、操作栈、方法出口等信息,当这个方法执行完后,就会弹出相应的栈帧。
方法区:
静态常量池 存在于class文件中,比如经常使用的javap -verbose中,常量池总是在最前面。
运行时常量池:就是在class文件被加载进了内存之后,常量池保存在了方法区中,通常说的常量池 指的是运行时常量池。所以呢,讨论的都是运行时常量池。
堆(Heap):用于存放类的实例对象与数组实例的地方,垃圾回收的主要区域就是这里(还可能有方法区)。
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )
新生代又可能分为Eden区,From Survivor区和To Survivor区,主要是为了垃圾回收。所有的线程共享Java堆
,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
新生代(Young Generation):主要是用来存放新生的对象。
老生代(Old Generation):主要存放应用程序中生命周期长的内存对象。在新生代中经过多次垃圾回收仍然存活的对象,会被存放到老生代中。老生代通过标记/整理算法来清理无用内存。
四 jvm 类加载机制
https://www.cnblogs.com/chanshuyi/p/jvm_serial_07_jvm_class_loader_mechanism.html
JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。
加载:加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。就是把数据加载到内存中。
验证:当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后(当代码数据被加载到内存中后),虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。
准备(重点):当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
- 内存分配的对象:Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
- 初始化的类型:在准备阶段,JVM 会为「类变量」分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。(final 关键字在 Java 中代表不可改变的意思)
解析:将其在常量池中的符号引用替换成直接其在内存中的直接引用(略)
初始化(重点):在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
使用:当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
卸载:当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。