1、什么是JVM?
JVM是一个可运行Java代码的虚拟计算机,包括一套字节码指令集,一组寄存器,一个栈,一个垃圾回收,堆和一个存储方式栈。JVM 是运行在操作系统之上,并不与操作系统直接交互。
2、运行过程:
我们都知道Java源文件,通过编译器,能够产生相应的.class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码。
如下:Java 源文件 —— 编译器 —— 字节码文件 —— JVM —— 机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这就是 Java 跨平台机制。
- 多个程序启动就会存在多个虚拟机实例,随程序的开始和完成而创建和消亡
- 多个虚拟机实例之间数据不能共享
2.1 线程
这里的线程指程序执行过程中的一个线程实体,在Hotspot JVM中的Java 线程与操作系统线程有直接的映射关系。
① 当线程本地存储、缓冲池分配、同步对象、栈、程序计数器等准备好以后,创建一个操作系统线程,随着Java线程的生命周期而变化,OS负责调度所有线程并分配CPU。
② OS线程完成初始化,调用Java线程的run()
方法
③ Java线程结束时,释放绑定OS线程和线程所有资源。
Hotspot JVM 后台运行的主要系统线程
- 虚拟机线程: 等待JVM到达安全点操作出现,这些操作必须在独立的线程中执行,因为当 堆 修改时需要JVM到达安全点。操作主要有 stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁解除。
- 周期任务线程: 负责定时器任务(即中断),用来调度周期性操作的执行。
- GC线程: 支持JVM的各种GC活动。
- 编译器线程: 支持在运行时将字节码动态编译成本地平台相关的机器码。
- 信号分发线程: 支持接收发送到JVM的信号并调用适当的JVM方法处理。
2.2 JMM 内存模型
- 私有型内存区域: 生命周期与线程相同,依赖用户线程的创建或结束(在Hotspot JVM中,每个线程都与OS线程直接映射)
- 程序计数器: 唯一不会发生OOM的区域
- 一块很小的内存空间,是当前线程所执行的字节码行号指示器,每一个线程都有一个独立的程序计数器。
- 若正在执行Java方法,计数器记录的是字节码指令的地址(当前指令的地址),若为本地方法(Native Method)则设为空。
- 虚拟机栈: 描述Java方法执行的内存模型。每一个方法执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,栈帧对应着方法的开始到结束的过程。
- 栈帧: 是一个存储数据和部分计算结果的数据结构,同时用来处理动态链接、方法返回值和异常分派等信息,无论如何完成方法(正常或异常)都会随着方法结束而销毁。
- 本地方法栈: 为Native方法服务的栈结构。
- 共享型内存区域:
- 方法区(永久代): 存储加载的类信息、常量、静态变量、即时编译后的代码等数据。
- 运行时常量池: 方法区的一部分,主要用于存储在类加载后,编译期产生的字面量和符号引用。
- 类实例区(堆内存): 存储创建的对象和数组的内存区域,是GC最重要的内存区域。
- 直接型内存区域: 不受JVM GC管理
2.3 JVM 运行时内存
JVM 堆从GC的角度可以细分为 新生代 和 老年代。
2.3.1 新生代
新生代 在JVM 堆内存中占有 1/3 的空间,老年代占有 2/3 的空间。
新生代区域 可分为三个部分:Eden区(8/10)、ServivorFrom区(1/10)、ServivorTo区(1/10)
- Eden区: Java新对象的存储区域(若新对象过大,则会直接放入老年代区),当该区域内存不足时,则会触发Minor GC 进行新生代区回收。
- ServivorFrom区: 存储上一次GC未回收的对象
- ServivorTo区: 保留一次Minor GC 未回收对象。
Minor GC(次要垃圾收集)
- 采用算法: 复制算法
- MinorGC的过程: 复制——清空——互换
新生代进行垃圾回收的过程:
- 1、eden、servivorFrom 复制到 servivorTo, 并且所有对象年龄+1
- 若对象年龄符合老年代标准,则复制到老年代
- 若ServivorTo区域内存不足,则直接复制到老年代
- 2、清空edon、servivorFrom区域
- 3、ServivorFrom与ServivorTo指针互换
- 在下一次GC时,现在的ServivorFrom就是需要扫描的ServivorTo
2.3.2 老年代
老年代主要是存放应用程序中生命周期长的内存对象,因为对象的稳定,所以不会频繁发生MajorGC。
- 当有新生代对象进入老年代时,先触发一次MajorGC,再触发MajorGC。
- 当无法找到足够的内存空间分配给新创建的较大对象时,也会触发MajorGC腾出空间。
- 当老年代内存不足时,抛出OOM异常。
Major GC(主要垃圾回收)
- 采用算法: 标记清除算法。
- 老年代触发后的使用过程:
- 1、扫描所有老年代对象,标记存活对象。
- 2、回收无标记的对象。
- 缺点: 每一次回收都会产生内存碎片,通常需要进行合并或标记出来方便下次直接分配。
2.3.3 永久代
内存永久保存的区域,主要存储Class和Meta(元数据)的信息。
- Class在被加载时就被放入永久代区域中,GC不会在主程序运行时对永久区进行回收,所以会导致OOM异常。
元数据空间: Java 8 引入代替永久代的内存空间,最大区别在于元数据空间是映射在实际内存上的,其大小也是限制在实际可用内存上的;而永久代则是映射在JVM的内存上,受JVM的限制。
2.4 垃圾回收算法
2.4.1 如何判断是否为垃圾?
- 引用计数法: 对象引用一次,引用计数器+1,当引用数为0或引用数是所有对象中最低的,则被GC回收。
- 可达性分析: 通过对一系列的“GC roots” 对象为起点进行搜索,当一个“GC roots” 和 一个对象之间没有可达路径,则该对象是不可达对象并进行标记,若第二次搜索标记仍然是不可达对象,则被GC回收。
2.4.2 有哪些GC算法?
- 标记清除算法(Mark-Sweep): 一种最基本的GC算法,分为两个阶段:标注和清除。其缺点是内存碎片化。
- 标注: 通过一次扫描进行标记可回收对象。
- 清除: 回收带有标记的对象。
- 复制算法(copying): 将内存分为等大的两份(主区域、备份区域),首先使用主区域,当主区域内存满后或不足以分配足够的空间时,将主区域进行一次标记清除回收,再将存活的对象复制到备份区域,并清理主区域。其缺点是可用内存减半,且在存活对象过多,复制算法效率下降。
- 标记整理算法(Mark-Compact): 分为三个阶段:标注、移动和清除。
- 标注: 标记可回收对象。
- 移动: 移动存活对象到内存的的一端。
- 清除: 清除存活对象存储区域外的所有区域。
- 分代收集算法: 主要是针对新生代、老年代和永久代的收集算法。
- 新生代: 采用复制算法
- 由于新生代的清除操作多于复制操作,所以一般划分一个较大的Edon区域和两个较小的Servivor空间,当进行GC时,将Eden区和其中一个Servivor空间存活的对象复制到另一个Servivor空间中。
- 老年代: 采用标记复制算法
- 永久代: 采用标记复制算法
2.5 Java的四种引用类型
- 强引用: 最常见于对象赋值变量,这个引用就是强引用,它不会被JVM回收,是造成OOM的主要原因。
- 软引用: 通过 SoftReference 类实现,对于软引用对象来说,只有在JVM内存不足时才会被回收,常见用于内存敏感的程序中。
- 弱引用: 通过 WeakRefrence 类实现,只要GC机制启动,该类引用就会被回收,常用于类初始化(例如类代码块)。
- 虚引用: 通过 PhantomRefrence 类实现,必须与引用队列联合使用,主要用于跟踪对象被回收的状态。
2.6 分代收集算法 和 分区收集算法的区别
2.6.1 分代收集算法
- 新生代-复制算法: 频繁发生GC且仅有少量存活对象时,选用复制算法,其付出成本较少。
- 老年代-标记整理算法: 对象存活率高,选用标记整理算法,效率更高
2.6.2 分区收集算法
- 分区算法是将一块内存空间分成连续不同的内存空间,每个小区间都可以独立使用,独立回收,可以控制一次性回收多个空间,根据目标停顿时间的不同,每次合理回收若干个小区间,从而减少一次GC所产生的停顿。
2.7 GC 垃圾收集器
Java 对新生代和老年代分别提供了不同的垃圾收集器。
- 新生代
- Serial(单线程复制算法): 一种最基本的垃圾收集器,是JDK 1.3.1 之前唯一的新生代GC器,执行时只使用一条线程或一个CPU并阻塞所有线程直到垃圾收集完成。因为其高效的特性,在限定单个CPU环境下,没有线程交互的开销,因此Serial依然是JVM在Client模式下的默认新生代收集器。
- ParNew(多线程Serial): 默认开启和CPU数量相同的线程数,可以通过
-XX:ParallelGCThreads
参数进行限制垃圾收集器的线程数。
- Parallel Scavenge(多线程自适应Serial): 高吞吐量环境下高效利用CPU时间进行垃圾回收,主要适用于后台运算且不需要过多交互的任务,自适应调节策略是其最大的特点。
- 老年代
- CMS(Concurrent mark sweep): 通过获取最短垃圾回收停顿时间,利用多线程的标记-清除算法进行垃圾回收。工作机制有四个阶段:
- 初始标记: 标记与GC roots直接关联的对象,速度极快,需要阻塞工作线程。
- 并发标记: 与用户线程并发执行跟踪GC roots操作
- 重新标记: 修正因用户程序运行而改变的标记,需要阻塞工作线程
- 并发清除: 当第一次标记与第二次标记都为可回收,则CMS清除当前可回收对象
- Serial Old(单线程标记整理算法): Serial 收集器的老年代版本,主要运行在 Client 默认的JVM老年代垃圾收集器。
- Parallel Old(多线程自适应Serial Old): Parallel Scavenge 收集器的老年代版本,在高吞吐环境下搭配 Parallel Scavenge 进行新/老年代的垃圾回收调节策略。
- G1收集器: 改进了CMS,基于标记整理算法并且精确控制停顿时间,在保证高吞吐量的同时实现低停顿GC,具有有效时间内的最高垃圾回收效率。
- 1、将堆内存划分为大小固定的几个内存区域,并跟踪这几个区域的GC进度
- 2、维护一个优先级列表,根据允许的收集时间,优先回收垃圾最频繁产生的区域。
2.8 IO和NIO
2.8.1 阻塞IO模型
一种最传统的IO模型,即在读写过程中会发生阻塞现象
- 概念: 用户线程发出IO请求,内核查看数据就绪状态,若数据未就绪,则用户线程处于阻塞状态,用户线程交出CPU,当数据就绪时,内核拷贝数据到用户线程并返回结果到用户线程,用户线程解除阻塞状态。
- 例子:
data = socket.read
,若数据未就绪,就会一直阻塞在read方法。
2.8.2 非阻塞IO模型
- 概念: 当用户线程发出read操作后,内核检查数据就绪状态,若数据就绪则内核拷贝数据,若数据未就绪,则直接返回error结果,用户线程不断询问内核数据就绪状态,所以NIO模型会一直占用CPU。
- 例子:
while(true){ data = socket.read(); if( data != error ) break; }
- 缺点: CPU占用率高
2.8.3 多路复用IO模型
目前使用较多的IO模型,Java NIO 就是一种多路复用IO。
- 概念: 在多路复用IO模型中,会有一个内核线程不断轮询每个socket的状态,当socket 发出IO请求时,才调用 IO 操作。在Java NIO 中通过
selector.select()
轮询socket,若没有请求则阻塞在轮询状态。
- 使用场景: 适用于连接数较多的场景。
- 缺点: 若响应体过大,会阻塞其它IO请求处理,并且影响sokect轮询进度。
2.8.4 信号驱动IO模型
- 概念: 当用户线程发起一个IO请求,给对应的socket注册一个信号函数,然后用户线程继续执行,当内核数据就绪会发送一个信号给用户线程,当用户线程接收到信号后,利用信号函数的IO操作接口来实现读写操作。
2.8.5 异步IO模型
一种理想的IO模型。
- 概念: 当用户线程发起read请求后,无需等待数据返回,并发去做其它任务。而在内核角度,内核受到
asynchronous read
后,会立刻返回,说明 read 请求发起成功,因此不会发生阻塞状况,内核等待数据就绪完成后,将数据拷贝到用户线程并发送一个信号给用户线程。
- 使用: Java 7 中,提供了 AIO(Asynchronous IO)类。
- 优点: 用户线程在IO请求阶段和IO使用阶段都不需要阻塞线程,而是接受到内核发送的信号后可以直接使用数据。
2.9 Java IO包
主要是常用的字节流和字符流分支。
2.10 Java NIO包
NIO 主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。
NIO基于Channel与Buffer进行IO操作,数据总是从通道读取到缓冲区中或从缓冲区写入通道;而Selector监听多个Channel事件(例如连接、数据就绪),因此单个线程可以监听多个通道。
NIO 和 IO 之间最大区别是 【IO面向流,NIO面向缓冲区】
传统IO是从流中读取单个或多个字节,直至读取完所有字节,所以不能预处理流中的数据。
NIO缓冲区的设立是为了可以在缓冲区中灵活地预处理读取到的数据,当数据从通道读取到缓冲区后,可以在缓冲区中处理读取数据再执行获取数据操作,但处理数据时有两个问题需要处理:1、判断数据是否需要处理;2、读入新数据不覆盖未处理的数据。
NIO的非阻塞是指在线程调用read或write操作时,会立刻在通道中获取当前可用数据或写入部分数据,若无数据可用,则返回空数据,无需等待有值数据,所以NIO可以用单独的线程管理多个IO通道。
2.10.1 Channel
Channel 和 IO中的Stream是同一级别的,只是Channel是双向的,Stream是单向的。
NIO中的Channel主要实现有:
- 1、FileChannel(文件IO)
- 2、DatagramChannel(UDP通道)
- 3、SocketChannel(TCP用户端通道)
- 4、ServerSocketChannel(TCP服务端通道)
2.10.2 Buffer
Buffer实际上是一个连续数组,任何读入或写入的数据都需要通过Buffer进行传输,但通道与通道之间不需要Buffer进行缓冲。
NIO中的Buffer主要实现有:
- 1、ByteBuffer
- 2、ShortBuffer
- 3、IntBuffer
- 4、LongBuffer
- 5、DoubleBuffer
- 6、FloatBuffer
- 7、CharBuffer
2.10.3 Selector
Selector能够检测多个注册的通道上是否发出IO请求,当有IO请求时,便获取请求事件并调用请求时间对应的响应方法。
2.11 JVM类加载机制
JVM类加载机制分为五个部分:
- 加载(Loading): 这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为读取这个类各种数据的方法区入口。(这里的Class对象可以是从压缩包中读取,也可以是动态代理类,也可以是其它文件生成)
- 验证(Verification): 这个阶段是为了确保Class文件的字节流包含的信息是符合当前JVM的要求。
- 准备(Preparation): 这个阶段是为了给类变量分配内存并初始化。(这里的初始化是JVM已经分配好的类型默认值,例如int类型的初始化值为 0)
- 解析(Resolution): 这个阶段是为了将常量池的符号引用替换为直接引用的过程。(就是将使用变量的位置都换成常量)
- 符号引用: 符号引用与虚拟机实现的布局无关,但符号引用的字面量形式必须符合JVM规范的Class文件格式。
- **直接引用:**引用目标在虚拟机中必须存在,直接引用的目标可以是是指向目标的指针,相对偏移量或是间接定位目标的句柄。
- 初始化(Initialization): 这个阶段是真正执行类中定义的Java程序代码,主要是执行类构造器
方法的过程。
方法是编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的,JVM会保证子类构造器执行前,父类构造器完成执行,如果一个类中没有静态变量赋值也没有静态代码块,那么编译器不会为其生成
方法。
- 不执行类初始化的情况
- 1、子类引用父类的静态字段,只会触发父类的初始化
- 2、定义了对象数组
- 3、未直接引用定义常量的类
- 4、通过类名获取Class对象
- 5、调用Class.forName()加载指定类时,指定参数
initialize=false
- 6、调用了ClassLoader 默认的loadClass方法
JVM类的卸载机制分为两个部分:
2.11.1 类加载器
JVM提供了三种类加载器:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。
- 启动类加载器(Bootstrap ClassLoader): 负责加载
JAVA_HOME\lib
目录中的,或通过-Xbootclasspath
指定路径中的,且被JVM认可(按文件识别,例如 rt.jar)的类。
- 扩展类加载器(Extension ClassLoader): 负责加载
JAVA_HOME\lib
目录中的,或通过java.ext.dirs
系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader): 负责加载用户路径(classpath)上的类库
- 自定义类加载器: JVM通过双亲委派模型进行类的加载,可以通过继承
java.lang.ClassLoader
实现自定义类加载器。
2.11.2 双亲委派模型
- 概念: 当一个类收到类加载请求时,首先将请求委派到父类加载器,每一层类加载器都会委派到父类加载器,所以最终所有的类加载请求都会到达启动类加载器,当父类加载器响应无法处理请求(自身加载路径下未找到所需加载的Class)时,子类加载器才尝试自行加载。
- 优点: 保证了不同的类加载器最终获得同一个Object对象。
2.11.3 OSGI(动态模型系统)
OSGI 提供了在多种网络设备中无需重启的动态改变构造的功能,可以实现模块级的热插拔功能,在程序升级更新时,可以只停用、重新安装、启动程序部分模块,但在提供模块化功能的同时,也会额外引入高复杂度,因为不遵循双亲委派模型。