Java面试题之JVM虚拟机
冯诺依曼计算机体系结构:控制器、运算器、存储器、输入设备、输出设备五部分组成(不包括寄存器)Java内存区域(运行时数据区)共享的:方法区:类的共有属性。JVM堆:对象、数组隔离的:本地方法栈:Natitve 方法虚拟机栈:局部变量区和操作数栈注:每一个线程都会生成PC寄存器和虚拟机栈。1.反射机制:第一步:获取Java中的反射类的字节码 ...
蹊源的Java笔记—JVM
前言
作为一个Java
开发,JVM
是我们必须要了解的,我们只有建立在了解它的基本运作原理,才可能设计出一个最合理的代码方案,在此之前我们已经了解了集合中的Map
接口,接下来溪源我将带领大家了解一下JVM
,希望对大家略有帮助。
集合之Map接口可参考我的博客:蹊源的Java笔记—集合之Map接口
线程与线程池可参考我的博客:蹊源的Java笔记-线程与线程池
正文
JVM
JVM为了达到给所有硬件提供一致的虚拟平台的目的,牺牲了一些与硬件相关的特性。
Java
源文件可以通过编译器转化成字节码文件(.class
文件),这些字节码文件又可以被JVM
转化成机器码。JVM
是运行在操作系统之上的,它与硬件没有直接交互。Java
的线程和原生操作系统线程有映射关系,Java
可以通过对应的操作系统线程来获取计算机资源。
线程共享的数据区:
- 方法区:存储程序运行时长期存活的对象,比如类的元数据( 元数据生成相应的
java
文件)、方法、属性等 (常量在JDK1.8移至JVM
堆中) - JVM堆:存放对象、数组、常量等,垃圾收集器就是收集这些对象,然后根据
GC
算法回收。
知识点:
- 在JDK1.8中废弃了永久代区域,方法区被放在了元空间,这种设计可以避免永久代
OOM
(内存溢出)导致触发GC
。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。默认在20M左右,放在元空间的永久代满了即,达到MetaspaceSize
的阈值,同样也会触发FullGC
. - 常量在JDK1.8由方法区移至
JVM
堆中。 - 类的元数据即类的描述数据,虚拟机通过元数据可以生成对应的对象。
线程隔离的数据区:
- 本地方法栈:
Natitve
方法 - 虚拟机栈(JVM方法栈):局部变量区、操作数栈、动态连接(方法调用过程的动态连接)、方法返回地址(可以理解为一个类方法的运行区域)。
- 程序计数寄存器(PC寄存器): 用于记录正在执行的虚拟机字节序列的行指示器。
知识点:
- 每一个线程都会生成
PC
寄存器和虚拟机栈。 - 栈的空间由系统自动分配,而堆,需要程序员自己申请并指明大小
JVM堆的组成
1/3的新生代:(由 Minor GC进行清理,采用复制算法)
- GC开始前,对象只会存在于
Eden
区和名为“From”
的Survivor
区,Survivor
区“To”
是空的。 - GC中,
Eden
区中所有存活的对象都会被复制到“To”
,而在“From”
区中,仍存活的对象会根据他们的年龄值来决定去向。在这里插入代码片
- GC结束后,
Eden
区和From
区已经被清空,这个“To”
和“From”
互换角色,此时Survivor
区“To”
是空的,而“From”
保留上次GC
存活对象
整个流程可以概括 复制-清空-互换。
2/3的老年代:( 由Major GC进行清理,采用标记清除算法 )
- 用于存放新生代中经过多次垃圾回收仍然存活的对象
- 新生代分配不了内存的大对象会直接进入老年代
知识点:
- 新生代满了,会放至到空闲的
Survivor
区,只有所有的Survivor
区满了才会放到老年代。 Survivor
区的作用是减少被送到老年代的对象,进而减少Full GC
的发生,Survivor
的预筛选保证,只有经历16次Minor GC
还能在新生代中存活的对象,才会被送到老年代。
JVM回收机制
JVM确认垃圾回收对象的方式
- 引用计数法:当引用数为0时,对象死亡
- 根搜索算法:根对象(
GC ROOTS
)到某对象不可达时,对象死亡。
GC ROOTS的对象包括:
- 虚拟机栈中的引用对象
- 本地方法栈的引用对象
- 方法区中静态属性引用的对象
- 方法区中静态常量池中引用的对象
JVM垃圾回收算法
- 标记-清除算法:效率偏低
- 复制算法:效率高,但是占用2倍内存 (预留一块内存 将还存活的对象放到该内存)
- 标记-整理算法:效率偏低(是对标记-清除算法的改进,让存活的对象向一段移动)
- 分代收集算法:把
Java
堆分为新生代和老年代,根据年代将特征选择上述算法。新生代通常采用复制算法,老年代采用标记-清除算法或者标记-整理算法。
常见的GC方式
- Minor GC:是清理新生代
- MajorGC:是指清理老年代
- Full GC:清理新生代和老年代
GC
,通常来时Full GC
比Minor GC
至少慢10倍。
触发Full GC的情况 :
- 老年代满了: 由于内存分配担保策略,当晋升到老年代的对象大于了老年代的剩余空间时,就会触发
FGC
。 - 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发
FGC
。 Metaspace
(元空间)在空间不足时会进行扩容,当扩容到了-XX:MetaspaceSize
参数的指定值时,也会触发FGC
。System.gc()
或者Runtime.gc()
被显式调用时,触发FGC
。- 采用
CMS
收集器发生"Concurrent Mode Failure”
异常时,触发FGC
。
知识点:
- 立即回收还是延迟回收是取决于
JVM
的,所以即使有GC
机制还是可能存在无用但可达的对象没有即时被回收而导致内存泄漏。
垃圾收集器
垃圾收集器,又称为垃圾回收器,是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法)的具体实现,不同版本的JVM
所提供的垃圾收集器可能会有很在差别,本文主要介绍HotSpot
虚拟机中的垃圾收集器。
选择垃圾回收器考虑的因素:
- 应用程序的场景
- 硬件的制约
- 吞吐量的需求
选择垃圾回收器的标准:
- 发生
gc
的停顿时间 - 产生空间碎片的大小,会间接影响并发量
串行、并行和并发的区别:
- 串行: 只会使用一个
CPU
或一条收集线程去完成垃圾收集工作 ,并且在进行垃圾收集时,必须暂停其他所有的工作线程。 - 并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
- 并发:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个
CPU
上;
常见的收集器(7种)
- 新生代收集器:
Serial
、ParNew
、Parallel Scavenge
(复制算法) - 老年代收集器:
Serial Old
、Parallel Old
、CMS
; (1、2 采用标记整理算法 3采用标记清除算法) - 整堆收集器:
G1
;(标记整理,分区)
ParNew收集器
ParNew的特点:
- 用于新生代收集器
- 采用复制算法
- 并行,采用多线程收集,垃圾手机时会造成
“Stop The World”
应用场景:在多核的情况下和CMS
搭配使用,以满足用户交互频繁实现低延迟的场景(最常见就是游戏)
Parallel Scavenge收集器
Parallel Scavenge的特点:
- 用于新生代收集器
- 采用复制算法
- 并行,采用多线程收集,垃圾收集时会造成
“Stop The World”
Parallel Scavenge
没有采用传统的GC
代码框架,它相对于ParNew
的特点在于:JVM
会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC
自适应的调节策略
应用的场景:在多核的情况下和Parallel Old
搭配使用,以满足高并发的场景(默认的搭配,最常就是web应用)
Parallel Old收集器
Parallel Old的特点:
- 用于老年代收集器
- 采用”标记—整理"算法
- 并行,采用多线程收集,垃圾收集时会造成
“Stop The World”
应用的场景:在多核的情况下和Parallel Scavenge
搭配使用,以满足高并发的场景(默认的搭配,最常就是web
应用)
CMS收集器
CMS的特点:
- 基于"标记-清除”算法,不进行压缩,以产生内存碎片,换取更短回收停顿时间
- 并发收集、低停顿
- 需要更多内存
应用的场景:在多核的情况下和Parallel Scavenge
搭配使用,以满足用户交互频繁实现低延迟的场景(最常见就是游戏)
CMS运作的过程:
- 初始标记:仅标记
GC Roots
能直接关联到的对象,速度很快,但会造成“Stop The World”
- 并发标记:应用程序运行的同时,对初始标记的对象中存活的对象进行标记,并不能保证可以标记出所有的存活对象;
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,会造成
“Stop The World”
,停顿时间比初始标记稍长,但远比并发标记短; - 并发清除:应用程序运行的同时,回收所有的垃圾对象
CMS的缺陷:
- 对
CPU
资源非常敏感:当CPU
核数低于4时,性能会比较差 - 在并发清除时无法处理应用程序新产生的垃圾对象(即浮动垃圾),所以需要此时需要预留一定的内存空间,当预留的空间也无法填满时会出现
"Concurrent Mode Failure”
失败,JVM
会临时启用Serail Old
收集器,而导致另一次Full GC
的产生; - 由于采用"标记-清除”算法,会产生大量的内存碎片。
G1收集器
G1
的特点:
- 并行与并发:既可以并行来缩短
"Stop The World”
停顿时间,也可以并发让垃圾收集与用户程序同时进行,减少停顿时间; - 分代收集:将整个堆划分为多个大小相等的独立区域 (
Region
),能够采用不同方式处理不同时期的对象; - 结合多种垃圾收集算法,空间整合,不产生碎片: 从整体看,是基于标记-整理算法;从局部(两个
Region
间)看,是基于复制算法; - 可预测的停顿:低停顿的同时实现高吞吐量
应用场景:具有比较大的内存空间、对象相对比较大的场景。
G1
采用三色标记法,将对象分为三种类型:
- 黑色: 根对象,或者该对象与它的子对象都被扫描
- 灰色: 对象本身被扫描,但还没扫描完该对象中的子对象
- 白色: 未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
G1的运作流程:
- 初始标记:仅标记
GC Roots
能直接关联到的对象,并且修改TAMS
(Next Top at Mark Start
),让下一阶段并发运行时,用户程序能在正确可用的Region
中创建新对象,速度很快,但会造成“Stop The World”
。 - 并发标记:应用程序运行的同时,对初始标记的对象中存活的对象进行标记,同时对象的变化记录在线程的
Remembered Set Log
,并不能保证可以标记出所有的存活对象; - 最终标记 :为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,这里把
Remembered Set Log
合并到Remembered Set
中; 会造成“Stop The World”
,停顿时间比初始标记稍长,但远比并发标记短; - 筛选回收:首先排序各个
Region
的回收价值和成本,然后根据用户期望的GC
停顿时间来制定回收计划;,最后按计划回收一些价值高的Region
中垃圾对象;采用复制算法和并行的方式,降低停顿时间、并增加并发量。
Java四种引用类型
- 强引用:
A a=new A()
只要引用a存在,垃圾回收器不会回收。 - 软引用:
SoftReference
类似于缓存的方式,不影响垃圾回收,可以提升速度,节省内存。若对象被回收,此时可以重新new
,主要是用来缓存服务器中间计算结果以及不需要实时保存的用户行为。通常放在用在对缓存比较敏感的应用中。 - 弱引用:
WeakReference
用于监控对象是否被垃圾回收器回收。 - 虚引用:
PhantomReference
,每次垃圾回收的时候都会被回收。主要用于判断对象是否已经从内存中删除。
Java IO/NIO
BIO的阻塞问题
当用户线程发出 IO
请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU
。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block
状态。如果数据没有就绪,就会一直阻塞在read
方法。
无论是客户端还是服务器,同一时间只能发送或者接受一个信息。处于等待回复的间隔,便是"阻塞"问题。即使使用多线程的方式 服务器端的accept()
、read()
方法依旧会被阻塞。
NIO同步非阻塞
- NIO本身采用的是多路复用
IO
模型,通过一个线程不断去轮询多个socket
的状态,只有当socket
真正有读写事件时,才真正调用实际的IO
读写操作。并且这种轮询是采用内核的方式,效率比直接使用用户线程高很多。 - 传统
IO
基于字节流和字 符流进行操作,而NIO
基于Channel
和Buffer
(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
NIO的非阻塞性
- 非阻塞读:使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
- 非阻塞写:一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
线程通常将非阻塞IO
的空闲时间用于在其它通道上执行IO
操作,所以一个单独的线程现在可以管理多个输入和输出通道。
Channel通道
Channel通道,是用于应用程序与操作系统进行交互的渠道。
NIO的Channel
的是主要实现类:
- FileChannel:用于传输文件
IO
的通道 - DatagramChannel:用于传输
UDP
数据的通道 - SocketChannel:客户端使用
SocketChannel
来与服务器建立连接 - ServerSocketChannel: 服务器
ServerSocketChannel
来等待客户端的连接
通道类似于流,但是有区别:
- 通道既可以读数据,也可以写数据。但是流的读写操作是单向的。
- 通道可以异步读取
- 通道的数据总是要经过缓冲区
Buffer缓冲区
缓冲区,实际上是一个容器,是一个连续数组。Channel
提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
。
Selector
Selector 能够检测多个注册的通道上是否有事件发生,如果有事 件发生,便获取事件然后针对每个事件进行相应的响应处理。
用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
AIO异步非阻塞
异步的体现:
- 同步的
IO
流 服务器端不会"主动"联系客户端 ,但是AIO
在数据准备完成后,会主动通知”客户端“ - 异步的
IO
是交给OS
处理,而异步应用自己会处理。
AIO与NIO的区别:
AIO
与NIO
都是通过管道的方式进行I/O
操作,但是AIO
每一个处理器与通道都是独立的、一一对映,NIO
是通过选择器进行匹配。
类加载机制
类加载器的任务就是.class
文件加载到到JVM
转换成 java.lang.class
类
常见的类加载器
- 根类加载器:用来加载
Java
的核心类; - 扩展类加载器:用来加载
jre
的扩展目录; - 系统加载器:它负责在
JVM
启动时加载来自Java
命令的-classpath
选项、java.class.path
系统属性,或者CLASSPATH
换将变量所指定的JAR
包和类路径。
双亲委托模型
双亲委托模型,确保了加载的唯一性,当类收到加载请求时,它首先不会尝试加载这个类,而是把请求委托给父类加载器执行,每个类都是如此(如果还有父类继续上交),只有父类加载完或者父类不存在,子类才会进行加载。
双亲委派模型的好处 :
- 确保了加载的唯一性, 保证了
Java
程序的稳定运行 - 保证了
Java
的核心API
不被篡改。
类加载过程
装载:获取类的二进制字节流,将其静态存储结构转化为方法区的运行时数据结构;
链接(分配静态域与默认值),可以细分为:
- 校验:获取类的二进制字节流,将其静态存储结构转化为方法区的运行时数据结构;
- 准备:在方法区中对类的
static
变量分配内存并设置类变量数据类型默认的初始值,不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java
堆中; - 解析:将常量池内的符号引用替换为直接引用的过程;
初始化(执行静态代码块和初始化静态域):为类的静态变量赋予正确的初始值,使得Java
代码中被显式地赋予的值。
当我们要对基础类进行修改时,打破双亲委托模型的方式:
- 自定义类加载器:继承
ClassLoader
类重写loadClass
方法。 - SPI机制:
JDK
内置的一种服务提供发现机制:通过加载ClassPath
下META_INF/services
,自动加载文件里所定义的类,通过ServiceLoader.load/Service.providers
方法通过反射拿到实现类的实例。
更多推荐
所有评论(0)