知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制

基本概念

JVM是可运行Java代码的假想计算机,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法区,JVM运行在操作系统之上,与硬件没有直接交互
知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第1张图片

运行过程

1、Java源文件 ——> 编译器 ——> 字节码文件(.class 二进制文件)
2、字节码文件 ——> JVM(类加载器) ——> 机器码
这也是为什么Java可以跨平台,一个程序启动,虚拟机就开始实例化,多个系统启动则有多个虚拟机实例。程序退出,虚拟机消亡,多个虚拟机实例间数据不能共享
知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第2张图片

1、线程

JVM中的Java线程与原生操作系统线程有直接的映射关系,当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好后,就会创建一个操作系统原生线程。操作系统负责调度所有线程,并分配到任何可用CPU上,原生线程初始化完毕后,就会调用Java线程的run()方法,线程结束时释放原生线程和Java线程的所有资源。JVM后台运行的系统线程主要有以下几个:
知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第3张图片

2、内存区域

知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第4张图片

  • JVM内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区域【堆、方法区】,即常说的五大分区
  • 线程私有区域生命周期与线程相同,依赖用户线程的启动/结束 而 创建/销毁 线程共享区域随虚拟机的启动/关闭 而 创建/销毁
  • 除了程序计数器,其余四个区域均规定了两种异常状况:
    • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出【StackOverflowError】异常
    • 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出【OutOfMemoryError(OOM)】异常
      知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第5张图片

2.1 程序计数器(线程私有)

  • 一块较小的内存区域,是当前线程所执行的字节码的行号指示器,通过改变这个计数器的值来选取下一条需要执行的字节码属性。Java虚拟机的多线程通过线程轮流切换、分配处理器执行时间的方式实现,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器。
  • 如果正在执行java方法,计数器记录的是虚拟机字节码地址,如果执行的是本地(Native)方法,则为空
    该内存区域是唯一一个虚拟机中没有规定任何OOM情况的区域

2.2 虚拟机栈(线程私有)

  • 是描述java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,对应着一个栈帧再虚拟机栈中入栈到出战的过程
  • 栈帧是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。
    栈帧随方法调用而创建,随方法结束而销毁——无论方法正常结束还是异常完成
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第6张图片

2.3 本地方法栈(线程私有)

本地方法栈与虚拟机栈类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务

2.4 堆(线程共享)——运行时数据区

  • 被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
  • 现代VM采用分代收集算法,因此Java堆从GC的角度还可以细分为新生代(Eden区、From Survivor区和To Survivor区)和老年代

2.5 方法区/永生代(线程共享)

  • 即常说的永生代,用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • 运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

3、JVM运行时内存

Java堆从GC的角度可以细分为新生代(Eden区、From Survivor区和To Survivor区)和老年代
知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第7张图片

3.1 新生代

用来存放新生的对象,一般占据堆的1/3空间,由于频繁创建对象,新生代会频繁触发MinorGC进行垃圾回收,又分为Eden区、From Survivor区和To Survivor区,只限于新生代的GC称为MinorGC

3.1.1 Eden区

Java新对象的出生地(如果新创建的对象占用内存很大,直接分配到老年代),当Eden区内存不够时就会触发MinorGC,对新生代区进行一次GC

3.1.2 ServivorFrom

上一次GC的幸存者,作为这次GC的被扫描者

3.1.3 ServivorTo

保留了一次MinorGC过程中的幸存者

3.1.4 MinorGC的过程(复制-清空-互换)

MinorGC采用复制算法:
①eden、from复制到to,年龄+1
②清空eden、from
③to和from互换

3.2 老生代

  • 主要存放应用程序中生命周期长的内存对象,对老生代的GC称为MajorGC
  • 老年代的对象比较稳定,所以MajorGC不会频繁执行,一般之前会进行一次MajorGC,使得新生代的对象晋身入老生代,导致空间不够才触发,或者无法找到足够大的连续空间分配给新创建的较大内存对象时也会提前触发一次MajorGC
  • MajorGC采用标记清除算法:首先扫描一次老年代,标记出存活对象,然后回收没有标记的对象。
  • Major耗时较长,因为需要扫描后回收,并且会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。
  • 当老年代也满了装不下时,会抛出OOM异常

4、垃圾回收与算法

知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第8张图片

4.1 如何确定垃圾

4.1.1 引用计数法

  • Java中,引用和对象之间是有关联的,如果要操作对象则必须用引用进行,因此很显然的一个简单办法是通过引用计数来判断一个对象是否可以回收
  • 即在对象中添加一个引用计数器,每当一个地方引用则加1,引用失效后减一,任何时刻计数器为0的对象都是不可能再被使用的
  • 但是目前的java虚拟机不是用这种方法管理内存,因为当两个对象互相引用时,该方法就失效了,并不会被回收

4.1.2 可达性分析

  • 为了解决引用计数法的循环引用问题,Java采用了可达性分析的方法
  • 即通过一系列“GC roots”对象作为起点搜索,如果在“GC roots” 和一个对象之间没有可达路径,则称该对象不可达,则该对象不可能被使用
  • GC roots即一些活跃的对象,Java虚拟机中可以当作GC roots的对象包括:
    • 虚拟机栈中引用的对象
    • 方法区中类静态属性引用的对象,如Java类的引用类型静态变量
    • 方法区中常量引用的对象,如字符串常量池里的引用
    • 本地方法栈中Native方法引用的对象
    • 被同步锁持有的对象

4.2 标记-清除算法

  • 最基础的垃圾回收算法,分为两个阶段:标记和清除,清除阶段回收被标记对象占用的空间
  • 该方法最大的问题是内存碎片化严重,后续可能发生大对象找不到可利用空间的问题
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第9张图片

4.2 标记-复制算法

  • 为了解决标记-清除算法内存碎片化严重的缺陷而提出的算法。
  • 按照内存容量将内存划分为等大小的两块,每次只是用其中一块,当这一块内存满后将尚存活的对象复制到另一块上,把已使用的内存清理
  • 【这种算法虽然实现简单、内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半,且存活的对象增多的话,效率大大降低】
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第10张图片

4.3 标记-整理算法

  • 结合以上两个算法,标记阶段和标记-清除算法相同
  • 标记后不是清理对象,而是将存活对象移向内存的一段,然后清除边界外的对象
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第11张图片

4.4 分代收集算法

  • 分代收集是目前大部分JVM使用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般将GC堆划分为【老生代】和【新生代】
  • 老生代的特点是每次垃圾回收只有少量对象需要被回收,新生代的特点是每次垃圾回收都有大量垃圾需要被回收,因此可以根据不同区域选择不同算法

4.4.1 新生代与标记-复制算法

  • 目前新生代均采用标记-复制算法,因为新生代中每次垃圾回收都要回收大部分对象,因此需要复制的操作比较少
  • 但通常新生代不是按照1:1的比来来划分,一般将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(From,To),比例为8:1:1,每次使用Eden和其中一块Survivor空间,当进行回收时,将该两块空间中还存活的对象复制到另一块Survivor中
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第12张图片

4.4.2 老年代与标记-整理算法

【老年代每次只回收少量对象,因此采用标记-整理算法】

  • JVM中位于方法区的永生代,用来存储class类,常量,方法描述等,对永生代的回收主要包括废弃常量和无用的类
  • 对象的内存分配主要在新生代的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和From Space,如此反复
  • 当对象在Survivor区躲过一次GC后,年龄+1,默认年龄到达15的对象会被移动到老年代

5、JAVA四种引用类型

5.1 强引用

  • Java中最常见的是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。
  • 当一个对象被强引用变量引用时,它处于可达状态,不能被GC,即使该对象以后永远都不会被用到,也不会被GC
  • 因此强引用是Java内存泄漏的主要原因之一(OOM)

5.2 软引用

  • 需要SoftReference类实现,对于只有软引用的对象来说,当系统内存足够时不会被GC,不足时会被GC
  • 软引用通常用于对内存敏感的程序中

5.3 弱引用

  • 需要WeakReference类实现,比软引用的生存期更短
  • 只要GC机制一运行,不管JVM内存是否足够,弱引用就会被回收

5.4 虚引用

  • 需要PhantomReference实现,不能单独使用,必须和引用队列联合使用
  • 虚引用的主要作用是跟踪对象被垃圾回收的状态

6、GC垃圾收集器

Java堆内存被划分为新生代和老年代两部分,新生代主要使用标记-清除和标记-复制垃圾回收算法,老年代主要使用标记-整理垃圾回收算法,因此JVM中针对新生代和老年代分别提供多种不同的垃圾收集器,HotSpot的垃圾收集器如下:
知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第13张图片

6.1 Serial收集器(新生代、单线程、标记-复制)、Serial Old收集器(老年代、单线程、标记-整理)

  • Serial和Serial Old分别用于新生代和老年代的垃圾回收,均是单线程的收集器。【单线程】不仅意味着它只会使用一个处理器或者一条收集线程来完成垃圾手机工作,更重要的是它进行垃圾回收时,【必须暂停其他所有工作线程】,直到收集结束
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第14张图片

  • 虽然Serial收集器需要暂停其他工作线程,但仍是HotSpot虚拟机运行在【客户端模式】下的默认新生代处理器,因为它简单而高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率

6.2 ParNew收集器(新生代、Serial+多线程)

  • ParNew收集器是Serial收集器的多线程版本,也是用标记-复制方法,除了多线程进行垃圾回收外,其余的行为与Serial完全一样,ParNew收集器垃圾收集过程中同样也要暂停其他工作线程
  • 是很多Java虚拟机运行在Server模式下的新生代默认垃圾收集器,通常与CMS收集器配合工作
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第15张图片

6.3 Parallel Scavenge收集器(新生代、多线程、标记-复制、高效)

  • Parallel Scavenge收集器的关注点是【程序达到一个可控制的吞吐量】(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率利用CPU,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
  • Parallel Scavenge收集器具有动态调整参数以提供最大的吞吐量的功能,这种自适应调节策略也是区别于ParNew收集器的重要特性

6.4 Parallel Old收集器(老年代、多线程、标记-整理)

  • JDK1.6之前新生代使用Parallel Scavenge只能搭配老年代的Serial Old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parellel Old正是为了在老年代同样提供吞吐量优先的垃圾收集器
  • 如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和老年代Parallel Old的搭配策略:
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第16张图片

6.5 CMS收集器(老年代、多线程、标记-清除)

  • CMS是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,通常较为关注服务的响应速度,CMS就非常适合
  • CMS的工作流程包括:
    • 1)【初始标记】:只是标记以下GC Roots能直接关联的对象,速度很快,仍然需要暂停所有的工作流程
    • 2)【并发标记】:进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程
    • 3)【重新标记】:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作流程
    • 4)【并发清除】:清除GC Roots不可达对象,和用户线程仪器工作,不需要暂停工作线程。
  • 由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户一起并发工作,所以总体上来看CMS收集器的内存回收是和用户线程一起并发执行的
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第17张图片

6.6 Garbage First (G1)收集器

  • G1收集器是垃圾收集器技术发展历史上的里程碑式成果,开创了收集器面向局部收集的设计思路和基于Region的内存布局形式
  • 在之前的垃圾收集器的目标范围要么是新生代(Minor GC),要么是老年代(Major GC),要么是整个Java堆(Full GC),而G1跳出了这个樊笼,它可以面向堆内存的任何部分来组成回收集(Collection Set,CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是Mixed GC模式。
  • G1开创的基于Region的堆内存布局是它能实现这个目标的关键,虽然G1也是基于分代收集理论设计的,但其对内存的布局与其他收集器具有明显差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为大小相等的独立区域(Region),每一个区域都可以根据需要,扮演新生代的Eden、Survivor空间,或者老年代空间,收集器对不同角色的Region采用不同的策略来处理
  • Region中有一类用于存放大对象的Humongous Region,一般被看作老年代。
  • G1收集器之所以能够建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,这样可以有计划的避免在整个Java堆中进行全区域的垃圾收集
  • 更具体的处理思路是让G1收集器跟踪每个Region里的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需要时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的Region。

7、类文件结构

  • Java的“一次编写,到处运行”特点正是基于虚拟机和字节码存储格式实现的
  • Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与【Class文件】这种特定的二进制文件格式所关联
  • Java程序经javac编译器得到字节码文件(*.class),而后虚拟机加载字节码文件进行后续工作
  • 【class类文件结构】

8、JVM类加载机制

  • 【Class文件中描述的各类信息,最终需要被加载到虚拟机中才能被运行和使用,虚拟机的类加载机制就是解释虚拟机如何加载这些class文件,以及加载后怎么使用和运行】
  • JVM类加载机制分为五个部分:加载、验证、准备、解析、初始化。
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第18张图片

8.1 类加载的时机

  • 上图中,加载、验证、准备、初始化、卸载的顺序是确定的,类型的加载过程必须按照这些顺序按部就班开始
  • 虚拟机规定了【有且只有】六种情况必须立即对类进行【初始化】:(加载、验证、准备自然需要在此之前开始)
    • 1)遇到new、getstatic、putstatic、invokestatic四条字节码指令时,能够生成这四条字节码指令的Java场景有:
      • 使用new关键字实例化对象时
      • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
      • 调用一个类型的静态方法时
    • 2)使用java.lang.reflect包的方法对类型进行反射调用时
    • 3)当初始化类时,如果父类没有进行初始化,需要先触发父类初始化
    • 4)虚拟机启动时,用户需要指定一个要执行的主类(main方法类),虚拟机会先初始化这个主类
    • 5)使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSorcial等四种类型的方法句柄,并且这个方法句柄对应的类没有进行初始化,需要先触发其初始化
    • 6)当一个接口中定义了JDK8新加入的默认方法(default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口需要在其之前进行初始化
  • 这六种场景中的行为称为对一个类型进行主动引用,除此之外,所有引用类型的方式都不会触发初始化,称为被动引用,下面举三个例子来说明何为被动引用:
  • 例一:【通过子类引用父类的静态字段,不会导致子类初始化】
class SuperClass{
     
    static{
     
        System.out.println("Super init!");
    }

    public static int value=123;
}

class SubClass extends SuperClass{
     
    static{
     
        System.out.println("Sub init!");
    }
}

public class NotInitialization{
     
    public static void main(String[] args) {
     
        System.out.println(SubClass.value);
    }
}

在这里插入图片描述

  • 因此,对于静态字段,【只有直接定义这个字段的类才会被初始化】
  • 例二:【通过数组定义来引用类,不会触发此类的初始化】
public class NotInitialization{
     
    public static void main(String[] args) {
     
        SuperClass[] superClasses = new SuperClass[10];
    }
}

在这里插入图片描述

  • 例三:【常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类初始化】
class ConstClass{
     
    static {
     
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWORD="hello world!";
}
public class NotInitialization{
     
    public static void main(String[] args) {
     
        System.out.println(ConstClass.HELLOWORD);
    }
}

知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第19张图片

  • 在编译阶段,常量HELLOWORD的值"helloword"已经被存储在NotInnitialization类的常量池中,之后NotInnitialization对HELLOWORD的引用被转化为对自身常量池的引用。也就是说NotInnitialization的class文件中并没有ConstClass类的符号引用入口,这两个类在编译成class文件后就不存在任何联系

8.2 类加载的过程

8.2.1 加载

【加载阶段,Java虚拟机需要完成三件事情:】

  • 1)通过一个类的全限定名来获取此类的二进制字节流(class文件)
  • 2)将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构
  • 3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

【关于加载过程中的类加载器在后文详细说明】

8.2.2 验证

  • 验证是连接阶段的第一步,确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,从代码量和耗时的角度,验证阶段的工作量在类加载过程中有相当大的比重。
  • 验证阶段大致完成四个阶段的检验工作:
    • 1)文件格式验证
      • 验证字节流是否符合Class文件规范,只有通过这个阶段的验证,字节流才会被允许进入Java虚拟机内存的方法区中进行存储
      • 所以后面三个阶段的验证全部是基于方法区的存储结构进行的,不会直接读取、操作字节流
    • 2)元数据验证
      • 对字节码描述的信息进行语义分析(除了Code外的所有字节流),判断当前类是否有父类,类中方法是否与父类有矛盾等
    • 3)字节码验证
      • 对类的方法体(Class文件中的Code属性)进行校验分析
    • 4)符号引用验证
      - 发生在虚拟机将符号引用转化为直接引用的时候,这个转化在解析阶段发生。
      - 符号引用验证可以看作是对类自身以外的各类信息进行匹配性校验,即该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段资源等

8.2.3 准备

【准备阶段是正式为类中定义的变量(静态变量,被static修饰的变量)分配内存设置变量初始值的过程,即在方法区中分配这些变量所使用的内存空间】

  • 1)准备阶段中内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
  • 2)初始值“通常情况”下是数据类型的零值,假设一个类变量被定义为
public static int value=123;
  • 那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法中,所以赋值123的动作要到类的初始化阶段才会被执行
  • 如果类字段的字段属性表中存在ConstantValue属性,那么在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,如:
public static final int value=123;
  • 编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置,将value赋值为123

8.2.4 解析

【解析阶段是Java虚拟机将常量池内符号引用替换为直接引用的过程】,符号引用就是Class文件中的CONSTANT_Class_info、CONSTANT_Fieldref_info等类型的常量

  • 【符号引用】
    符号引用与虚拟机实现的内存布局无关,引用的内容并不一定是已经加载到虚拟机内存中的内容。各种虚拟机实现的内存布局各不相同,但它们能接收的符号引用必须一致,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中
  • 【直接引用】
    直接引用是可以指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不同,如果有了直接引用,那么引用的目标必定存在在内存中

8.2.5 初始化

【初始化阶段开始,虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序】

  • 准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源
  • 初始化是执行类构造器()方法的过程,下面对该方法的产生过程,以及影响因素进行说明:
  • 1) 类构造器()是由编译器自动收集类中所有【类变量的赋值动作】和【动态语句块(static{})中的语句】合并产生,编译器收集顺序是由语句的先后顺序决定,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如:
public class test2 {
     
    static{
     
        i=0;//编译通过
        System.out.println(i);//编译不能通过  报错:Illegal forward reference
    }
    static int i=1;
}
  • 在准备阶段中,静态变量i已经被分配了内存,并且初始化了0值,因此在test2编译阶段,static静态代码块中,能对i赋值,但是不能访问,因为其创建的i实例在访问语句之后
  • 2)()方法与类的构造函数【即虚拟机视角中的实例构造器()】不同,它不需要显式的调用父类构造器,Java虚拟机保证子类的()执行前,父类的()已经被执行完毕。因此Java虚拟机中第一个被执行()的类型肯定是java.lang.Object
  • 3)由于父类的()优先执行,也就意味着【父类中定义的静态语句块】要优于【子类的变量赋值操作】,如下例中,输出结果为2,而不是1:
public class test2 {
     
    public static void main(String[] args) {
     
        System.out.println(Sub.B);
    }
}

class Parent{
     
    public static int A=1;
    static {
     
        A=2;
    }
}

class Sub extends Parent{
     
    public static int B=A;
}
  • 4)()方法对于类或接口来收并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法

8.3 类加载器

【Java虚拟机团队将加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类】

8.3.1 类加载器种类

  • JVM提供了三种类加载器,用于在加载这个步骤中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个工作:
  • 1)【启动类加载器】
  • 负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类
  • 2)【扩展类加载器】
  • 负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库
  • 3)【应用程序类加载器】
  • 负责加载用户路径(classpath)上的类库。 JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第20张图片

8.3.2 双亲委派机制

  • 【类加载器的工作过程】
  • 1)当一个类加载器收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到最顶层的启动类加载器中
  • 2)只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载
  • 采用双亲委派的一个好处是Java中的类随着他的类加载器一起具备了一种带有优先级的层次关系。比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象
  • 如果没有双亲委派机制,都由各个类加载器自行去加载,假如用户也编写一个名为java.lang.Object的类,并放在程序的ClassPath,那么系统就会采用这个类加载器去得到Object类,系统中就会出现多个Object类
    知识梳理(1)JVM—内存区域、运行时内存、垃圾回收与算法、JAVA的四种引用类型、GC垃圾收集器、类文件结构、JVM类加载机制_第21张图片

你可能感兴趣的:(jvm)