前言
- JVM包括字节码指令,寄存器、栈、垃圾回收和存储方法域
- 运行于操作系统之上,与硬件没有直接相联交互
- JVM 允许一个应用并发执行多个线程,Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系
- 具体的编译执行过程:
Java 源文件—->编译器—->字节码文件
字节码文件—->JVM—->机器码
在hotspot的具体线程有
系统线程 | 功能 |
---|---|
虚拟机线程(VM thread) | 等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the-world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除 |
周期性任务线程 | 定时器事件(中断)用来调度周期性操作的执行 |
GC 线程 | JVM 中不同的垃圾回收 |
编译器线程 | 将将字节码动态编译成为本地平台相关的机器码 |
信号分发线程 | 接收发送到 JVM 的信号并调用适当的 JVM 方法处理 |
1. JVM内存区域
JVM的线程分为线程私有(程序计数器、虚拟机栈、本地方法区)、线程共享(【JAVA 堆、方法区)和直接内存
- 线程私有数据区域生命周期与线程相同
- 线程共享区域随虚拟机生命周期
- 直接内存并不是 JVM 运行时数据区的一部分
程序计数器:当前线程所执行的字节码的行号指示器,记录的是虚拟机字节码指令的地址(当前指令的地址),如果为native方法,则为空
每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响
通过计数器取指实现代码的流程控制,以及在多线程中记录其当前执行的位置
唯一一个不会出现oom,随着线程的生命周期生灭
虚拟机栈:
- 每个方法在执行的同时都会创建一个栈帧用于存储
局部变量表、操作数栈、动态链接、方法出口等信息。
- 用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、 方法返回值和异常分派
- 栈帧随方法生命周期
本地方法区:虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务(区分下虚拟机栈和本地方法栈的不同。)
堆:是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域
方法区:即永久区用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
是各个线程共享的内存区域
2. JVM运行时内存
堆从垃圾回收角度分为新生代和老年代
新生代:存放新生对象。一般占据堆的1/3 。频繁创建对象,新生代触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo三个区
- Eden 区:如果新建对象占寸大,分配到老年代。当eden内存不足,触发垃圾回收
- ServivorFrom区:上次垃圾回收的幸存者,此次作为扫描
- ServivorTo区:保留上次垃圾回收幸存者
MinorGC 的过程:复制算法
- 将Eden 区、ServivorFrom复制到ServivorTo区
- 清空Eden 区、ServivorFrom
- ServivorTo与ServivorFrom交换
老年代:生命周期长的内存对象,垃圾回收不会频繁执行
MajorGC的过程:标记清除算法
- 扫描所有,标记存活,没收无标记
- 耗时长,产生内存碎片
- 装不下的时候会产生OOM(Out of Memory)异常
永久代:内存的永久保存区域。存放 Class 和 Meta(元数据)的信息,但随着Class的增多而胀满,会抛出OOM异常
3. 垃圾回收与算法
3.1 如何确定垃圾
引用计数法:通过引用计数来判断一个对象是否可以回收。如果一个对象没有引用,可能不太可能用到
可达性分析:通过一系列的“GC roots”对象作为起点搜索,在“GC roots”和一个对象之间没有可达路径,但不可达对象不代表为垃圾可回收。不可达对象要变为可回收起码两次可能面临回收
3.2 标记清除算法
标记回收的对象,清除该对象变为未使用的内存空间
但会造成内存碎片化严重,后续大对象利用的空间小
3.3 复制算法
为了解决内存碎片化严重
按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
但缺点是内存被压缩挤压,都堆积在一起,虽然没有了碎片化,但是对象如果增多的话,复制的效率会减少
3.4 标记整理算法
结合以上两种算法
标记后将存活对象移向内存的一端,清除端边界外的对象
3.5 分代收集算法
目前JVM常用算法
根据对象存活的不同生命周期将内存划分为不同的域。老年代和新生代,老年代回收少,新生代大量回收,根据不同的区域确定不同算法
新生代与复制算法:Copying 算法
一般将新生代划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另一块 Survivor 空间中
老年代与标记复制算法:Mark-Compact 算法
对象存活率高, 不必进行内存复制, 且直接腾出空闲内存
- 对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代
- 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理
- To Space 无法足够存储某个对象,则将这个对象存储到老生代
- 进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环
- 当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代中
3.6 分区收集算法
将整个堆空间划分为连续的不同小区间, 每个小区间独立使用,独立回收。根据停顿时间,每次合理回收几个区间
4. 引用类型
强引用:把一个对象赋给一个引用变量,这个引用变量就是一个强引用。该对象以后永远都不会被用到 JVM 也不会回收
软引用:当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收
弱引用:它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存
虚引用:跟踪对象被垃圾回收的状态。不能单独使用,必须和引用队列联合使用
5. 垃圾回收器
5.1 Serial 垃圾收集器
- 使用复制算法、单线程(只会使用一个 CPU 或一条线程)
- 简单高效、没有线程交互的开销
- 运行在 Client 模式下默认的新生代垃圾收集器
5.2 ParNew 垃圾收集器
- Serial 收集器的多线程版本,除了使用多线程进行垃圾收集之外,其他都一样
- 机运行在 Server 模式下新生代的默认垃圾收集器
5.3 Parallel Scavenge 收集器
- 新生代垃圾收集器,多线程,复制算法
- 可控制吞吐量(CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
新生代 Parallel Scavenge 收集器与 ParNew 收集器工作原理类似,都是多线程的收集器,都使用的是复制算法,在垃圾收集过程中都需要暂停所有的工作线程
5.4 Serial Old 收集器
- 老年代垃圾收集器,单线程,标记-整理算法
- Client 默认的 java 虚拟机默认的年老代垃圾收集器
在 Server 模式下:老年代中使用 CMS 收集器的后备垃圾收集方案
5.5 Parallel Old收集器
- 多线程、标记整理算法
- 新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略
5.6 CMS 收集器
- 多线程、标记整理算法,用于老年代垃圾回收
- 获取最短垃圾回收停顿时间
CMS的工作机制:
初始标记-并发标记-重新标记-并发清除
- 标记对象(GC Roots 能直接关联的),暂停所有的工作线程
- 跟踪过程,不用暂停所有线程
- 修正并发标记,因用户程序继续运行而导致标记产生变动的那一部分对象的标记,记录需要暂停所有线程
- 清除对象,不用暂停所有线程
在不用暂停线程来看,CMS 收集器的内存回收和用户线程是一起并发地执行
5.7 G1 收集器
改进CMS收集器
优点:
- 标记整理算法,不产生碎片
- 不牺牲吞吐量,精确控制停顿时间
G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率
6. JAVA IO/NIO
阻塞 IO 模型:读写数据过程中会发生阻塞现象
data = socket.read();
//如果数据没有就绪,就会一直阻塞在 read 方法
非阻塞 IO 模型:在非阻塞 IO 模型中,用户线程需要不断地询问( CPU 占用率非常高)内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。轮询实在用户线程下进行,效率低
while(true){
data = socket.read();
if(data!= error){
//处理数据
break;
}
}
多路复用 IO 模型:
- Java NIO 实际上就是多路复用 IO。会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作
- 在 Java NIO 中,是通过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞
- 多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况
- 轮询是在内核进行,效率高
信号驱动 IO 模型:
在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作
异步 IO 模型:只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了
区别:在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作
NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector
- 传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
- IO 是面向流的,NIO 是面向缓冲区的
- IO没有存放缓冲区的地方,不能前后移动,如果要前后移动,还需要存放一个缓冲区。而NIO有缓冲区,可前后移动,增加了灵活性
- IO的堵塞是不能做任何事情。而NIO的非堵塞模型中,即使没有数据也不会保持堵塞模式,不需要等待完全读入或者完全写入可以去做其他事情
- 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道
Channel:
io中的流是单向的,而通道是双向的(可读可写)
实现方式有:
- FileChannel对应IO
- DatagramChannel对应UDP
- SocketChannel对应TCP的client
- ServerSocketChannel对应TCP的server
Buffer:
缓冲区,也是连续数组
在 NIO 中,Buffer 是一个顶层父类,是一个抽象类
子类有:ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、DoubleBuffer、FloatBuffer、ShortBuffer
Selector:
Selector 类是 NIO 的核心类
Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销
7. JVM 类加载机制
过程:加载,验证,准备,解析,初始化
加载:生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口(可从Class 文件、jar包中的war包、运行计算生成、JSP转换的class文件等获取均可)
验证:确保class文件中的字节流符合条件
准备:在方法区中分配变量所使用的内存空间。正式为类变量分配内存并设置类变量的初始值阶段
//实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是程序被编译后,存放于类构造器<client>方法之中
public static int v = 8080;
//在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v赋值为 8080
public static final int v = 8080;
解析:将常量池中的符号引用替换为直接引用的过程,class文件CONSTANT_Class_info、 CONSTANT_Field_info、CONSTANT_Method_info等信息
—补充—
符号引用:引用的目标并不一定要已经加载到内存中
直接引用:可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄,有该引用,在内存中一定有
类构造器<client>
初始化阶段是执行类构造器方法的过程
方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法
类加载器:启动类、扩展类和应用程序类
- 启动类加载器:负责加载 JAVA_HOME\lib 目录或通过-Xbootclasspath 参数指定路径
- 扩展类加载器:负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库
- 应用程序类加载器:负责加载用户路径(classpath)上的类库