JVM全称是Java虚拟机(Java Virtual Machine),是Java跨平台运行的中间层,用以运行Java字节码。JVM运行在操作系统之上,不与硬件设备直接交互。
常见的Java虚拟机包括Sun classic、 HotSpot 以及 Android Dalvik VM。
# 运行机制
Java源文件在通过编译器之后被编译成相应的.Class文件(字节码文件),.Class文件又被JVM中的解释器编译成机器码在不同的操作系统(Windows、Linux、Mac)上运行。
每种操作系统的解释器都是不同的,但基于解释器实现的虚拟机是相同的,这也是Java能够跨平台的原因。
在一个Java进程开始运行后,虚拟机就开始实例化了,有多个进程启动就会实例化多个虚拟机实例。进程退出或者关闭,则虚拟机实例消亡,在多个虚拟机实例之间不能共享数据。
Java程序的具体运行过程如下:
- Java源文件被编译器编译成字节码文件。
- JVM将字节码文件编译成相应操作系统的机器码。
- 机器码调用相应操作系统的本地方法库执行相应的方法。
# 子系统
Java 虚 拟 机 包 括 一 个 类 加 载 器 子 系 统 ( Class Loader SubSystem)、运行时数据区(Runtime Data Area)、执行引擎和本地接口库(Native Interface Library)。本地接口库通过调用本地方法库(Native Method Library)与操作系统交互。
# 多线程
并发不一定要依赖多线程(如PHP中很常见的多进程并发),但是在Java里面谈论并发,基本上都与线程脱不开关系。
线程是比进程更轻量级的调度执行单位,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。
# 内核线程
JVM中的线程与操作系统中的线程是相互对应的,使用内核线程实现的方式也被称为1:1实现。内核线程(Kernel-Level Thread,KLT)就是直接由 操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换。
操作系统负责调度所有线程,并为其分配CPU时间片,在原生线程初始化完毕时,就会调用Java线程的run()执行该线程;在线程结束时,会释放原生线程和Java线程所对应的资源。
# 后台线程
在JVM后台运行的线程主要有以下几个:
- GC线程:GC线程支持JVM中不同的垃圾回收活动
- 编译器线程:编译器线程在运行时将字节码动态编译成本地平台机器码,是JVM跨平台的具体实现
# 内存模型
# 运行时数据区
# 程序计数器
(线程私有,无内存溢出问题)
程序计数器是一块很小的内存空间,用于存储当前运行的线程所 执行的字节码的行号指示器。每个运行中的线程都有一个独立的程序 计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟 机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空(Undefined)。
程序计数器属于“线程私有”的内存区域,它是唯一没有Out Of Memory(内存溢出)的区域。
# 虚拟机栈
(线程私有,描述Java方法的执行过程)
虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧 (Stack Frame)中存储了局部变量表、操作数栈、动态链接、方法出 口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理 动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)。
栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创 建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机栈中的 入栈和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法 内未被捕获的异常),都视为方法运行结束。
# 本地方法区
(线程私有)
本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。
# 堆
(线程共享)
在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是 被线程共享的内存区域,也是垃圾收集器进行垃圾回收的最主要的内 存区域。由于现代JVM采用分代收集算法,因此Java堆从GC(Garbage Collection,垃圾回收)的角度还可以细分为:新生代、老年代和永 久代。
# 方法区
(线程共享)
方法区也被称为永久代,用于存储类信息、常量、静态变量、即时编译器编译后的机器码、运行时常量池等数据。
# 垃圾回收
# 分代收集
# 垃圾回收算法
Java采用引用计数法和可达性分析来确定对象是否应该被回收, 其中,引用计数法容易产生循环引用的问题,可达性分析通过根搜索 算法(GC Roots Tracing)来实现。
- 复制算法
复制算法是为了解决标记清除算法内存碎片化的问题而设计的。 复制算法首先将内存划分为两块大小相等的内存区域,即区域 1和区 域 2,新生成的对象都被存放在区域 1中,在区域 1内的对象存储满 后会对区域 1进行一次标记,并将标记后仍然存活的对象全部复制到 区域 2中,这时区域 1将不存在任何存活的对象,直接清理整个区域 1的内存即可
复制算法的内存清理效率高且易于实现,但由于同一时刻只有一 个内存区域可用,即可用的内存空间被压缩到原来的一半,因此存在 大量的内存浪费。同时,如果大对象过多的时候,来回复制则效率过低。
- 标记整理算法
标记整理算法结合了标记清除算法和复制算法的优点,其标记阶 段和标记清除算法的标记阶段相同,在标记完成后将存活的对象移到 内存的另一端,然后清除该端的对象并释放内存
- 分代收集算法
无论是标记清除算法、复制算法还是标记整理算法,都无法对所 有类型(长生命周期、短生命周期、大对象、小对象)的对象都进行 垃圾回收。因此,针对不同的对象类型,JVM采用了不同的垃圾回收算法,该算法被称为分代收集算法。
分代收集算法根据对象的不同类型将内存划分为不同的区域,JVM将堆划分为新生代和老年代。
新生代主要存放新生成的对象,其特点 是对象数量多但是生命周期短,在每次进行垃圾回收时都有大量的对 象被回收;
老年代主要存放大对象和生命周期长的对象,因此可回收 的对象相对较少。
# 分代收集与分区收集
# 垃圾收集器
# 类加载系统
# 类加载器
# 双亲委派(向上委托、向下委派)
为了保障类的唯一性和安全性。JVM通过双亲委派机制对类进行加载。
双亲委派机制指一个类在收 到类加载请求后不会尝试自己加载这个类,而是把该类加载请求向上 委派给其父类去完成,其父类在接收到该类加载请求后又会将其委派 给自己的父类,以此类推,
这样所有的类加载请求都被向上委派到启 动类加载器中。
若父类加载器在接收到类加载请求后发现自己也无法 加载该类(通常原因是该类的Class文件在父类的类加载路径中不存 在),则父类会将该信息反馈给子类并向下委派子类加载器加载该 类,
直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFound异常。