Java虚拟机(JVM)小结

Java内存模型JMM

概念

JMM即Java内存模型(Java Memory Model),其是虚拟机用来定义一个一致的跨平台内存模型,以让Java程序达到“一次编译,到处运行”的能力。
Java内存模型描述了一个程序的各个变量之间的关系,以及在实际计算值系统中变量存储到内存和从内存中取出变量的底层细节。

JMM规定了所有变量都存储在主内存中,每个线程拥有自己的工作内存,其工作内存中保存着该线程使用的变量在主内存中的一个副本拷贝,线程对变量的操作(读、写)只能在自己的工作内存中,不同的线程之间无法操作对方工作内存中的变量,线程之间的值传递都需要经过主内存来完成

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

以上是JDK1.7和1.8中的Java内存模型,其最大的改变就将JDK1.7中的方法区移到了本地内存中,并且命名为元数据区

各个内存区域分析

线程私有的内存区域

  • 程序计数器:每个线程独有的一小块内存空间,可以看做是当前线程所执行的字节码的行号指示器
  • JVM虚拟机栈:线程中每个方法执行时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机中的入栈和出栈过程
  • 本地方法栈:本地方法栈和JVM虚拟机栈作用完全一样,只是本地方法栈为虚拟机使用的Native方法服务,而JVM虚拟机栈为JVM执行的Java方法服务

线程共享的内存区域

  • 堆:在JVM启动时创建,所有的对象实例以及数组都要在堆上分配,如果堆上没有足够的空完成实例分配且堆无法拓展,就会抛出OOM。
  • 方法区/元数据区:用于存储已经被加载过的类信息、常量、静态变量、即时编译器编译后的代码。
  • 运行时常量池:存在于元数据区,编译器或运行期间产生的常量被放在运行时常量池中,这里的常量包括:基本类型、包装类(包装类不管理浮点型,整形只会管理-128到127)和String。
  • 字符串常量池:存在堆中,用于存储字符串对象,或着字符串对象的引用。
  • 直接内存:在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

从上图可以看出,多个线程之间有共享的数据区,堆区、元数据区
每个线程也有自己独立的数据区,程序计数器、JVM虚拟机栈、本地方法栈

Java程序运行流程:
在这里插入图片描述

  1. 利用工具编写Java源代码,后缀名为.java
  2. 通过编译器将源代码转化成字节码,后缀名变为.class
  3. 通过类加载器将字节码文件加载到JVM中有系统执行

类加载机制

如果当Java程序主动使用一个还未被加载到内存中的类时,此时JVM就会对该类通过加载、连接、初始化三个步骤来进行初始化,把这三个阶段统称为类加载阶段

虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java类加载机制

类的生命周期

一个类的生命周期从加载开始,然后经过连接、初始化、使用、最后直到卸载结束
在这里插入图片描述

类加载的具体五个步骤

类加载阶段一共分为五个步骤:加载、验证、准备、解析、初始化

加载:通过类的权限名获取此类的二进制字节流,将这个二进制字节流的静态存储结构转换成方法区的运行时数据结构。在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

验证:该阶段主要为了保护Class字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

准备:正式为类变量分配内存空间,并且设置变量的初始值

解析:虚拟机将常量池中的符号引用转换为直接引用的过程 (符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。而直接引用就是直接指向内存地址)

初始化:在准备阶段,变量已经被赋过一次系统要求的初始值,而初始化阶段,则是要根据使用者的主观计划去赋值变量和其他资源

类加载的时机

  1. 创建一个新的对象实例
  2. 访问某个类的静态变量,或对其赋值
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个类的子类(其首先会创建子类的父类)
  6. JVM启动时标明的启动类,即类名与文件名相同的那个类

类加载器

类加载器:实现通过类的权限名获取该类的二进制字节流代码块叫做类加载器(其本身也是一个类),其为所有被载入内存中的类生成一个java.lang.Class实例对象

类加载器可分为启动类加载器、扩展类加载器、应用程序类加载器和自定义类加载器

双亲委派机制

工作过程:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是先把请求委派给父类加载器去完成,每一层类加载器都是如此,一直向上委派直到最顶层。如果父类加载器反馈自己无法完成加载请求(他的搜索范围内没有找到所需的类),了加载器才会尝试自己加载。在这里插入图片描述
双亲委派模型的优点:其好处在于使java类和类加载器共同具备了带有优先级的层次关系,这种层次关系可以避免类的重复加载,如当父类加载器以及加载过一个类,而此时子类加载器收到该类的加载请求,直接通过本级类加载器加载便会重复加载,更加程度上保证了java代码的安全性,但由于每次加载都要从下至上一次请求加载,没有足够灵活。

垃圾回收机制

Java进程在启动后会穿件创建一个垃圾回收线程,来对内存中无用的对象进行自动回收。

发生垃圾回收的时机

1.由JVM垃圾回收机制决定

  • 创建对象分配内存空间,空间不足时,自动触发GC
  • 其他回收机制
    Object类中有一finalize()方法,当JVM检测该对象不在被任何引用指向时,垃圾回收期则会调用该对象的finalize()方法

2.显示的手动调用System.gc()
调用该方法建议JVM进行FGC,一般不会使用这种方法,而是交给虚拟机自行管理

垃圾回收的判断策略

当虚拟机进行垃圾回收时,如何判断一个对象是否应该被回收?一般有两种方法

  1. 引用计数法:
    给对象添加一个引用计数器,每当有一个引用指向该对象,计数器+1,引用断开计数器-1,当计数器为0时,表示该对象即可被回收。这种方法比较简单,但很难解决对象之间的互相引用问题
  2. 可达性分析算法:
    通过一系列“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索走过的路径称为“GC Root 引用链”,当一个对象到GC Roots没有任何引用链连接即通过该对象不可达“GC Roots”时,这个对象即可被回收,java中用的正是这种策略

在这里插入图片描述
如上图,当Object3 与 Object2的连接断开后,Object3 与 Object4 都无法找到 “GC Root” ,所以此时这两个对象可回收.

需要进行垃圾回收的内存空间

1.方法区/元数据区

  • JDK1.7的方法区在GC中一般称为永久代
  • JDK1.8的元空间存在于本地内存中,GC也是即对元空间垃圾回收
  • 永久代或元空间的垃圾收集主要有两部分,废弃常量和没有被引用的类
    2.堆
  • 堆是Java垃圾回收管理的主要区域,也被称为“GC堆”
  • 从回收的角度,基于垃圾回收的分代算法,堆还可以被细分:
    1、新生代:其中又分为Eden区、From Survivor区和To Survivor区
    因为java对象大多都具备朝生夕死的特点,而绝大多数类的空间分配都在 新生代,所以新生代垃圾回收(Minor GC)非常频繁,回收速度也很快
    2、老年代:老年代的垃圾回收 (Major GC) 经常会伴有至少一次的 Minor GC,因为老年代的垃圾回收没有新生代那么频繁,因此速度会比Minor GC慢10倍以上
    3、Full GC:在不同的语义条件下,对Full GC的定义也不同,有时候指老年代的垃圾回收,有时候指全堆(新生代+老年代)的垃圾回收,还可能指有用户线程暂停(Stop-The-World)的垃圾回收(如GC日志中)
    在这里插入图片描述

垃圾回收的算法

1、标记清除法

  • 最基础回收算法,用于老年代
  • 算法分为标记和清除两个阶段,对老年代空间整体查找一次,标记需要回收的空间,最后统一回收标记的空间
  • 标记清除法是最基础的回收算法,其他算法都是在其基础上改进得到的
  • 不足:标记和清除效率都不高,并且标记清除后会留下许多不连续的小空间,这可能会导致下次分配一个较大对象时,无法存入,导致空间利用率下降
    在这里插入图片描述

2、标记整理法

  • 老年代收集算法
  • 标记过程仍与标记清除算法一样,但第二阶段不是简单清除,而是先将所有存活的对象移到一边,然后直接清理掉边界以外的空间
    在这里插入图片描述

3、复制算法

  • 新生代的收集算法
  • 将可用内存划分为两部分,每次分配内存时总是只用其中的一部分,当这部分内存块用完时,将所有存活对象直接全部复制到另一块未使用的内存上,然后把用掉的那部分全部清理掉
  • 优点:操作简单执行效率高,缺点:空间利用只有一半,代价太高
    在这里插入图片描述

分代收集算法

  • 当前的JVM都是使用分代收集算法,这种算法没有什么新的思路,只是以上几个方法的优化,根据对象的存活周期把内存空间分为几个部分
  • 先将java堆分为新生代和老年代
  • 由于新生代中98%的对象都具备朝生夕死的特点,所以不需要按复制算法的1:1分配空间,而是将新生代分为很大的一块Eden区和两块很小的Survivor区。其默认的比例为8:1:1。所以实际上新生代的内存空间利用率为90%
  • 在新生代中,由于需要频繁的进行垃圾回收,并且垃圾回收时会经常会产生大量可回收空间,所以使用复制算法,而进入老年代的对象存活率高,不易被收回,并且没有额外空间可复制,所以采用标记整理清除算法

垃圾回收流程

  1. Eden空间不足时,触发Minor GC,然后将Eden区和一个Survivor区(From)的还存活的数据复制到另一个Survivor区(To)中,最后清理掉Eden区 和 Survivor区(From)的对象在这里插入图片描述
  2. 空间清理结束,在此线程创建新的对象再次放入Eden区,直到Eden区空间不足,再次重复以上操作进行Minor GC
  3. 当经过多次清理,Eden区的某个对象总是存活,则将其放入老年代
  4. Survivor区空间不足,无法存放复制的存活对象时,对象全体进入老年代
  5. 老年代空间不足时,触发Magor GC

内存分配与回收策略

  1. 大多数创建的对象优先会进入Ende区,当Ende区内存不足时,触发Minor GC
  2. 大对象会直接进入老年代,这里所谓的大对象时需要占用大量连续内存空间存储的java对象,最典型的如很长的字符串、很大的数组
  3. 新生代长期存活的对象进入老年代,因为Ende区的对象大多具备朝生夕死的特点,这也是我们期望的,但如果在新生大长期存在对象总是存活,则会导致每次进行Minor GC总是要对该对象搬移如此降低了效率。因此为了避免这种情况,虚拟机给每个对象定义了一个年龄计数器,初始为1,如果每进行一次Minor GC 该对象还存活,则年龄+1,当年龄达到一定阈值(默认为15),该对象就将进入老年代
  4. 动态的进行年龄判断,对于新生代的对象,不一定只要年龄达到阈值才会进入老年代,当Survivor空间中相同年龄的对象占空间大小大于总空间一半时,这些对象也会进入老年代
  5. 空间分配担保机制,当发生Minor GC时,我们不能保证剩余存活的对象占空间总是小于Survivor区(To),而当剩余存活的对象占空间总是大于Survivor区(To)时,则会触发空间分配担保机制,尝试将这些存活的对象直接进入老年代
    在发生Minor GC 前,虚拟机会检查老年代的最大连续可用内存空间是否大于新生代所有对象的总空间;
    如果大于,说明Minor GC 安全,可进行空间分配担保,如果小于,虚拟机就会查看设置的HandlePromotionFailure设置值是否允许担保失败(是否为true);
    如果为true,那么进行检查老年代的最大连续空间十分大于每次晋升老年代对象的平均大小,如果大于,继续尝试一次Minor GC,但此时依然有风险,如果小于或HandlePromotionFailure == false,则需要进行一次Full GC

垃圾回收时造成的影响

  1. 用户线程暂停
    垃圾回收工作是再垃圾回收线程中执行的,在很多情况下,执行垃圾回收工作或者垃圾回收线程中的某一个步骤的时候,需要暂停用户线程,也就是Stop-The-World,这个过程会造成用户看起卡顿的现象

垃圾收集器中,并发和并行的概念有所不同
并行:指多条垃圾回收线程同时执行,用户线程处于等待状态
并发:值用户线程和垃圾收集线程同时执行,用户线程继续执行,而垃圾收集线程在另外一个CPU上

  1. 评判垃圾回收器的指标:吞吐量和用户体验
    吞吐量:CPU用于运行用户代码的时间和CPU消耗时间的比值,即
    吞吐量 = 运行用户代码的时间/(运行用户代码的时间+垃圾回收的时间)
    停顿时间:GC造成用户线程单次停顿的时间和总的停顿时间
    吞吐量优先:用户线程总的停顿时间短,即使单次停顿时间长一点也可以接受
    用户体验优先:用户线程单次停顿时间短,即使总的停顿时间长一点也可以接受。

垃圾收集器

在这里插入图片描述

Serial收集器(新生代收集器)

  • 单线程
  • 复制算法
  • Stop The World(STW)
  • 应用场景:Client模式下的默认新生代收集器
  • 优势:对于单个CPU来说,Serial收集器没有线程交互的开销,可以专心做垃圾收集,可以获得最高的单线程收集效率

ParNew(新生代收集器,并行GC)

  • 多线程
  • 复制算法
  • Stop The World
  • 应用场景:搭配CMS收集器在用户体验优先的程序中使用
  • 优势:随着可以使用CPU数量的增加,对于CPU资源的利用有很大好处

Parallel Scavenge(新生代收集器,并行GC)
serial收集器的多线程版本

  • 多线程
  • 复制算法
  • 可控制吞吐量
  • 自适应的调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量
  • 应用场景:“吞吐量优先”收集器,适用于吞吐量需求高的任务型程序

Serial Old收集器(老年代,串行GC)

  • 单线程
  • 标记-整理算法
  • 应用场景:与Parallel Scavenge收集器搭配使用,作为CMS的备用方案,在并发收集发生Concurrent Mode Failure时使用

Parallel Old收集器(老年代收集器,并发GC)

  • 多线程
  • 标记-整理算法
  • 应用场景:吞吐量优先

CMS收集器(老年代收集器,并发GC)

  • 并发收集、低停顿
  • 用户体验优先
  • 整个过程分四个步骤:
    初始标记:初始标记一下GC Roots能关联到的对象、速度很快,需要STW
    并发标记:并发标记就是进行GC Roots Tracing的过程,从GC Roots关联到的对象开始向下搜索标记没有被引用链连接起来的对象
    重新标记:重新标记阶段是为了修正并发标记阶段,因为用户线程也在并发执行,所以导致的原本没有被GC Roots关联到的对象又被关联起来,标记产生变动需要重新标记。这个阶段需要STW,停顿的时间比初始标记长一点,但远比并发标记短
    并发清除:并发清除阶段会并发的清除标记的对象
    由于整个阶段耗时最长的并发标记和并发清除两个阶段都是可以和用户线程一起并发的执行,不会STW,所以从整体上看,CMS收集器的内存回收是与用户线程一起并发执行的
    缺陷:CMS会抢占CPU资源,CMS无法处理浮动垃圾,CMS使用“标记-清除”算法,会导致大量空间碎片的产生

G1收集器(全区域的垃圾收集器)

  • 用在heap memory很大的情况下,把heap划分为很多的region块,然后并行对其进行垃圾回收
  • G1垃圾回收的时候基本不会STW,而是基于most garbage优先回收(整体上看是“标记-整理”算法,从局部上看是两个region的复制算法)的策略对region进行垃圾回收
  • 用户体验优先
  • 一个region有可能属于Eden、Survivor或者Tenured内存区域
  • G1收集器在清理掉垃圾所占的空间后,还会做内存压缩

结果如下图,图中的E表示该region属于Eden内存区域,S表示属于Survivor内存区域,T表示属于Tenured内存区域。图中空白的表示未使用的内存空间。G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。这种内存区域主要用于存储大对象-即大小超过一个region大小的50%的对象:
在这里插入图片描述

年轻代垃圾收集
在G1垃圾收集器中,年轻代的垃圾回收过程使用复制算法,把Eden区和Survivor区的对象刷新到新的Survivor区
在这里插入图片描述
老年代垃圾收集
对于老年代的垃圾收集,G1一共分四个阶段,基本根CMS垃圾收集器一样

  • 初始标记阶段:同CMS相同的是,他会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象,但不同的是G1没有使用STW,而是与minor GC一起发生
  • 并发标记阶段:这个阶G1与CMS相同,同时G1还进行了发现哪些Tenured region中对象的存活率很小或者基本没有对象存活,那么G1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段,这个阶段G1会计算每个region的对象存活率,方便后续清除
  • 最终标记阶段:重新更新标记所有可达对象
  • 筛选回收阶段:此阶段G1会挑选出那些对象存活率低的region进行回收,与Minor GC同时发生
    在这里插入图片描述

垃圾回收触发的时机

Minor GC:
在Eden区创建对象,Eden区空间不足时
Migor GC:
当有对象要放入老年代,而老年代空间不足时

  • 新生代晋升为老年代 (对象年龄达到一定阈值,或年龄相同的对象占空间总和已大于Survivor区的一半时)
  • 大对象(需要占用大量连续空间)直接进入老年代
  • Minor GC的空间分配担保机制,在发生Minor GC前,如果老年代最大可用的连续空间小于新生代所有对象空间时,一下情况将会触发Migor GC
    1、不允许空间担保 HandlePromotionFailure == false
    2、允许空间担保,但老年代最大可用的连续空间小于历次晋升到老年代的对象的平均大小
    3、允许空间担保,但执行完Major GC后老年代空间仍不足

以上便是对Java虚拟机(JVM)的知识点小结,随着后续学习的深入还会同步的对内容进行补充和修改,如能帮助到各位博友将不胜荣幸,敬请斧正

猜你喜欢

转载自blog.csdn.net/m0_46233999/article/details/118216648
今日推荐