0
点赞
收藏
分享

微信扫一扫

jvm运行机制


蹊源的Java笔记—JVM

前言

作为一个​​Java​​​开发,​​JVM​​​是我们必须要了解的,我们只有建立在了解它的基本运作原理,才可能设计出一个最合理的代码方案,在此之前我们已经了解了集合中的​​Map​​​接口,接下来溪源我将带领大家了解一下​​JVM​​,希望对大家略有帮助。

集合之Map接口可参考我的博客:​​蹊源的Java笔记—集合之Map接口​​

线程与线程池可参考我的博客:​​蹊源的Java笔记-线程与线程池​​

正文

JVM

JVM为了达到给所有硬件提供一致的虚拟平台的目的,牺牲了一些与硬件相关的特性。

  • ​Java​​​源文件可以通过编译器转化成字节码文件(​​.class​​​文件),这些字节码文件又可以被​​JVM​​转化成机器码。
  • ​JVM​​​是运行在操作系统之上的,它与硬件没有直接交互。​​Java​​​的线程和原生操作系统线程有映射关系,​​Java​​可以通过对应的操作系统线程来获取计算机资源。

jvm运行机制_Java

线程共享的数据区:

  • 方法区:存储程序运行时长期存活的对象,比如类的元数据( 元数据生成相应的​​java​​​文件)、方法、属性等 (常量在JDK1.8移至​​JVM​​堆中)
  • JVM堆:存放对象、数组、常量等,垃圾收集器就是收集这些对象,然后根据​​GC​​算法回收。

知识点:

  1. 在JDK1.8中废弃了永久代区域,方法区被放在了元空间,这种设计可以避免永久代​​OOM​​​(内存溢出)导致触发​​GC​​​。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。默认在20M左右,放在元空间的永久代满了即,达到​​MetaspaceSize​​​的阈值,同样也会触发​​FullGC​​.
  2. 常量在JDK1.8由方法区移至​​JVM​​堆中。
  3. 类的元数据即类的描述数据,虚拟机通过元数据可以生成对应的对象。

线程隔离的数据区:

  • 本地方法栈:​​Natitve​​ 方法
  • 虚拟机栈(JVM方法栈):局部变量区、操作数栈、动态连接(方法调用过程的动态连接)、方法返回地址(可以理解为一个类方法的运行区域)。
  • 程序计数寄存器(PC寄存器): 用于记录正在执行的虚拟机字节序列的行指示器。

知识点:

  1. 每一个线程都会生成​​PC​​寄存器和虚拟机栈。
  2. 栈的空间由系统自动分配,而堆,需要程序员自己申请并指明大小

JVM堆的组成

jvm运行机制_蹊源_02

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进行清理,采用标记清除算法 )

  • 用于存放新生代中经过多次垃圾回收仍然存活的对象
  • 新生代分配不了内存的大对象会直接进入老年代

知识点:

  1. 新生代满了,会放至到空闲的​​Survivor​​​区,只有所有的​​Survivor​​区满了才会放到老年代。
  2. ​Survivor​​​区的作用是减少被送到老年代的对象,进而减少​​Full GC​​​的发生,​​Survivor​​​的预筛选保证,只有经历16次​​Minor GC​​还能在新生代中存活的对象,才会被送到老年代。

JVM回收机制

JVM确认垃圾回收对象的方式

  • 引用计数法:当引用数为0时,对象死亡
  • 根搜索算法:根对象(​​GC ROOTS​​)到某对象不可达时,对象死亡。

GC ROOTS的对象包括:

  1. 虚拟机栈中的引用对象
  2. 本地方法栈的引用对象
  3. 方法区中静态属性引用的对象
  4. 方法区中静态常量池中引用的对象

JVM垃圾回收算法

  • 标记-清除算法:效率偏低
  • 复制算法:效率高,但是占用2倍内存 (预留一块内存 将还存活的对象放到该内存)
  • 标记-整理算法:效率偏低(是对标记-清除算法的改进,让存活的对象向一段移动)
  • 分代收集算法:把​​Java​​堆分为新生代和老年代,根据年代将特征选择上述算法。新生代通常采用复制算法,老年代采用标记-清除算法或者标记-整理算法。

常见的GC方式

  • Minor GC:是清理新生代
  • MajorGC:是指清理老年代
  • Full GC:清理新生代和老年代​​GC​​​,通常来时​​Full GC​​​比​​Minor GC​​至少慢10倍。

触发Full GC的情况 :

  1. 老年代满了: 由于内存分配担保策略,当晋升到老年代的对象大于了老年代的剩余空间时,就会触发​​FGC​​。
  2. 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发​​FGC​​。
  3. ​Metaspace​​​(元空间)在空间不足时会进行扩容,当扩容到了​​-XX:MetaspaceSize​​​ 参数的指定值时,也会触发​​FGC​​。
  4. ​System.gc()​​​或者​​Runtime.gc()​​​被显式调用时,触发​​FGC​​。
  5. 采用​​CMS​​​收集器发生​​"Concurrent Mode Failure”​​​异常时,触发​​FGC​​。

知识点:

  1. 立即回收还是延迟回收是取决于​​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运作的过程:

  1. 初始标记:仅标记​​GC Roots​​​能直接关联到的对象,速度很快,但会造成 ​​“Stop The World”​
  2. 并发标记:应用程序运行的同时,对初始标记的对象中存活的对象进行标记,并不能保证可以标记出所有的存活对象;
  3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,会造成 ​​“Stop The World”​​,停顿时间比初始标记稍长,但远比并发标记短;
  4. 并发清除:应用程序运行的同时,回收所有的垃圾对象

CMS的缺陷:

  • 对​​CPU​​​资源非常敏感:当​​CPU​​核数低于4时,性能会比较差
  • 在并发清除时无法处理应用程序新产生的垃圾对象(即浮动垃圾),所以需要此时需要预留一定的内存空间,当预留的空间也无法填满时会出现​​"Concurrent Mode Failure”​​​失败,​​JVM​​​会临时启用​​Serail Old​​​收集器,而导致另一次​​Full GC​​的产生;
  • 由于采用"标记-清除”算法,会产生大量的内存碎片。

G1收集器

G1的特点:

  • 并行与并发:既可以并行来缩短​​"Stop The World”​​停顿时间,也可以并发让垃圾收集与用户程序同时进行,减少停顿时间;
  • 分代收集:将整个堆划分为多个大小相等的独立区域 (​​Region​​),能够采用不同方式处理不同时期的对象;
  • 结合多种垃圾收集算法,空间整合,不产生碎片: 从整体看,是基于标记-整理算法;从局部(两个​​Region​​间)看,是基于复制算法;
  • 可预测的停顿:低停顿的同时实现高吞吐量

应用场景:具有比较大的内存空间、对象相对比较大的场景。

​G1​​采用三色标记法,将对象分为三种类型:

  • 黑色: 根对象,或者该对象与它的子对象都被扫描
  • 灰色: 对象本身被扫描,但还没扫描完该对象中的子对象
  • 白色: 未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

G1的运作流程:

  1. 初始标记:仅标记​​GC Roots​​​能直接关联到的对象,并且修改​​TAMS​​​(​​Next Top at Mark Start​​​),让下一阶段并发运行时,用户程序能在正确可用的​​Region​​​中创建新对象,速度很快,但会造成 ​​“Stop The World”​​。
  2. 并发标记:应用程序运行的同时,对初始标记的对象中存活的对象进行标记,同时对象的变化记录在线程的​​Remembered Set Log​​,并不能保证可以标记出所有的存活对象;
  3. 最终标记 :为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录,这里把​​Remembered Set Log​​​合并到​​Remembered Set​​​中; 会造成 ​​“Stop The World”​​,停顿时间比初始标记稍长,但远比并发标记短;
  4. 筛选回收:首先排序各个​​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​​操作,所以一个单独的线程现在可以管理多个输入和输出通道。

jvm运行机制_JVM_03

Channel通道

Channel通道,是用于应用程序与操作系统进行交互的渠道。

​NIO的Channel​​的是主要实现类:

  • FileChannel:用于传输文件​​IO​​的通道
  • DatagramChannel:用于传输​​UDP​​数据的通道
  • SocketChannel:客户端使用​​SocketChannel​​来与服务器建立连接
  • ServerSocketChannel: 服务器​​ServerSocketChannel​​ 来等待客户端的连接

通道类似于流,但是有区别:

  1. 通道既可以读数据,也可以写数据。但是流的读写操作是单向的。
  2. 通道可以异步读取
  3. 通道的数据总是要经过缓冲区

Buffer缓冲区

缓冲区,实际上是一个容器,是一个连续数组。​​Channel​​​ 提供从文件、 网络读取数据的渠道,但是读取或写入的数据都必须经由 ​​Buffer​​。

jvm运行机制_JVM运行机制_04

Selector

Selector 能够检测多个注册的通道上是否有事件发生,如果有事 件发生,便获取事件然后针对每个事件进行相应的响应处理。

用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。

AIO异步非阻塞

异步的体现:

  • 同步的​​IO​​​流 服务器端不会"主动"联系客户端 ,但是​​AIO​​在数据准备完成后,会主动通知”客户端“
  • 异步的​​IO​​​是交给​​OS​​处理,而异步应用自己会处理。

AIO与NIO的区别:

​AIO​​​与​​NIO​​​都是通过管道的方式进行​​I/O​​​操作,但是​​AIO​​​每一个处理器与通道都是独立的、一一对映,​​NIO​​是通过选择器进行匹配。

类加载机制

类加载器的任务就是​​.class​​​文件加载到到​​JVM​​​转换成 ​​java.lang.class​​类

jvm运行机制_Java_05

常见的类加载器

  • 根类加载器:用来加载​​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​​方法通过反射拿到实现类的实例。

jvm运行机制_JVM基本结构_06


举报

相关推荐

0 条评论