JVM整体架构
用记事本写第一个java程序HelloWorld
第一步:写java源代码程序:HelloWorld.java
第二步:编译java源代码程序,生成HelloWorld.class文件,字节码文件
第三步:运行程序.(在JVM里运行的)
关于JVM,其实有很多大厂开发了不同版本的JVM,比较知名的有:Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、 Azul VM、 Apache Harmony、 Google Dalvik VM、 Microsoft JVM等等。
现在使用的比较多的JDK8版本就是Sun HotSpot VM与BEA JRockit VM合并之后开发出的JDK版本。
JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE,JRE又包含了JVM。
对于Java程序员来说,JAVA虚拟机底层的指令,不是我们核心关注的内容,我们关注的是我们写的java程序在jvm中运行时需要的内存,内存是由JVM自动管理的,由于程序或硬件的原因可能会出现内存泄漏或溢出的问题导致运行的程序崩溃,不了解JVM的内存结构和各个内存区域的工作职责,将对解决这类问题带来很大的麻烦,所以学习java到一定程度的时候,是必须要学习jvm里的知识点的,我们先粗略地了解一下整个JVM的构架:
如上图所示,JVM分为三个主要子系统:
- 类加载器子系统(Class Loader Subsystem)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
类加载器子系统
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
Java的动态类加载功能由类加载器子系统处理。它在运行时(而不是编译时)首次引用类的时候加载、连接、并初始化。
加载
类将由此组件加载。通过引导类加载器,扩展类加载器,应用类加载器这三个类加载器帮助完成加载。
- 引导类加载器(BootStrap ClassLoader) —— 负责从引导类路径中加载类,仅用于rt.jar。给予该加载器最高的优先级。
- 扩展类加载器(Extension Class Loader) —— 负责加载ext文件夹内的类。
- 应用类加载器(Application ClassLoader) —— 负责加载类路径中应用级别的类,path提到的环境变量等。
上面的类加载器在加载类文件时将遵循 双亲委派机制 。
连接
- 验证 —— 字节码验证程序将验证生成的字节码是否正确,如果验证失败,我们将收到验证错误。
- 准备 —— 为所有静态变量分配内存并为其分配默认值。
- 解析 —— 用方法区的原始引用代替所有符号内存引用。
初始化
这是类加载的最后阶段,此处所有静态变量将被分配原始值,并且将执行静态块。
父类和子类的初始化顺序?
父类静态块→子类静态块→父类初始化块(成员变量,非静态代码块等)→父类构造方法→子类初始化块(成员变量,非静态代码块等)→子类构造方法
运行时数据区
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。
运行时数据区分为5个主要组成部分:方法区,堆区,虚拟机栈,PC寄存器,本地方法栈。
方法区
所有类级别的数据(包括静态变量)都将存储在此处。每个JVM只有一个方法区,它是一个共享资源。
堆区
所有对象及其对应的实例变量和数组都将存储在此处。对于每个JVM,堆区域也是一个,由于方法和堆区域共享多个线程的内存,因此所存储的数据不是线程安全的。
虚拟机栈
对于每个线程,将会创建一个单独的运行时栈。每个方法调用,都会在栈内存创建一个条目,称之为栈帧。所有局部变量都会在栈内存中创建。由于它不是共享的资源,所以是线程安全的。
栈又分为三个子实体 :
- 局部变量数组 —— 与方法相关,涉及局部变量,并在此存储相应的值。
- 操作数栈 —— 如果需要执行任何中间操作,操作数栈将充当运行时工作空间来执行操作。
- 帧数据 —— 对应于方法的所有符号存储在此处。在任何异常的情况下,捕获的区块信息将被保持在帧数据中。
PC寄存器(程序计数器)
每个线程都有单独的PC寄存器,用于保存当前执行指令的地址,一旦指令执行,PC寄存器将更新到下一条指令。
本地方法栈
本机方法堆栈保存本机方法信息。对于每个线程,将创建单独的本地方法栈。
JDK1.7和JDK1.8的运行时数据区的区别?
JDK1.8以前的HotSpot JVM有方法区,也叫永久代(permanent generation)。
方法区用于存放已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码。
方法区是一片连续的堆空间,通过-XX:MaxPermSize来设定永久代最大可分配空间,当JVM加载的类信息容量超过了这个值,会报OOM:PermGen错误。
永久代的GC是和老年代(old generation)捆绑在一起的,无论谁满了,都会触发永久代和老年代的垃圾收集。
移除永久代的工作从JDK 1.7就开始了。JDK 1.7中,存储在永久代的部分数据就已经转移到Java Heap或者Native Heap。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)、类的静态变量转移到了Java heap。
JDK1.8 和 JDK1.7的jvm内存最大的区别是:在1.8中方法区是由元空间(元数据区)来实现的,常量池移到堆中。1.8不存在方法区,将方法区的实现给去掉了,而是在本地内存中,新加入元数据区(元空间)。
元空间:存储 .class 信息, 类的信息,方法的定义,静态变量等,而常量池放到堆里存储。
为什么要用Metaspace替代方法区
随着动态类加载的情况越来越多,这块内存变得不太可控,如果设置小了,系统运行过程中就容易出现内存溢出,设置大了又浪费内存。
各个分区总结
名称 | 特征 | 作用 | 配置参数 | 异常 |
程序计数器 | 占用内存小,线程私有 | 字节码行号指示器 | 无 | 无 |
虚拟机栈 | 线程私有,使用连续的内存空间 | java方法存储的内存模型,存储局部变量表、操作数栈、动态链接、方法出口等 | -Xss | OOM,stackOverFlow |
本地方法栈 | 线程私有 | 为虚拟机使用到的本地方法服务,方便与外界环境交互 | 无 | OOM,stackOverFlow |
堆区 | 线程共享 | 保存对象实例,所有对象实例都要在堆上分配 |
-Xmn |
OOM java heap space |
方法区 | 线程共享 | 存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据 |
-XX:PermSize:16M |
1.7 OOM PermGen space |
运行时常量池 | 方法区的一部分 | 字面量及符号引用 |
执行引擎
分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐个执行它。
解释器
读取字节码,对其进行解释并逐一执行。解释器解释字节码的速度较快,但执行速度较慢。解释器的缺点是,当一个方法多次调用时,每次都需要解释。
JIT编译器
JIT编译器消除了解释器的缺点(当一个方法多次调用时,每次都需要解释),执行引擎将使用解释器的帮助进行转换,当发现重复的代码时,它将使用JIT编译器编译整个字节码并将其更改为本地代码。此本地代码将直接用于重复的方法调用,从而提高系统性能。
- 中间代码生成器 —— 生成中间代码
- 代码优化器 —— 代码优化器负责优化上面生成的中间代码
- 目标代码生成器 —— 目标代码生成器负责生成机器代码/本机代码
- 分析器 —— 一个特殊组件,负责查找热点,即该方法是否被多次调用
垃圾收集器
垃圾收集器是执行引擎的一部分,它收集/删除未引用的对象。可以通过调用”System.gc()”来触发垃圾回收,但是不能保证执行。JVM的垃圾收集器仅收集那些由new关键字创建的对象(无论以哪种方式创建对象,最终也是会回到new关键字上,只是不是我们显式地使用关键字new创建而已)。因此,如果您创建了没有new的任何对象,则可以使用finalize方法执行清理。
本地方法接口(JNI)
JNI将与本机方法库进行交互,并提供执行引擎所需的本机库。
本地方法库
它是执行引擎所需的本机库的集合。