前言

JVM是Java的重要组成部分,对于我这个Cpper转Javaer也需要认真学习才对。

一、JVM内存结构

JVM内存空间
程序计数器
Java虚拟机栈
本地方法栈
方法区

在这里插入图片描述
JDK 1.8 同 JDK 1.7 比,最大的差别就是:元数据区取代了永久代。元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。

1、程序计数器(PC寄存器)

程序计数器:一块较小的内存空间,是当前线程正在执行的那条字节码指令的地址。若当前线程正在执行的是一个本地方法,那么此时程序计数器为Undefined

程序计数器的作用:

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
  • 在多线程情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。

程序计数器的特点:

  • 是一块较小的内存空间。
  • 线程私有,每条线程都有自己的程序计数器。
  • 生命周期:随着线程的创建而创建,随着线程的结束而销毁。
  • 是唯一一个不会出现 OutOfMemoryError 的内存区域。

2、Java虚拟机栈

Java虚拟机栈:Java方法运行过程中的内存模型。
Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,用于存放该方法运行过程中的一些信息,如:

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址

栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
虚拟机栈可能出现的两种错误:

  • StackOverFlowError
  • OutOfMemoryError

3、本地方法栈(C栈)

本地方法栈则为虚拟机使用native方法服务。 在HotSpot虚拟机中和Java虚拟机合二为一。

栈帧变化过程:
本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量、操作数栈、动态链接、方法出口信息等。
方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出StackOverFlowError和OutOfMemoryError异常。

如果Java虚拟机本身不支持Natvie方法,或是本身不依赖于传统栈,那么也不提供本地方法栈。如果支持本地方法栈,那么这个栈一般会在线程创建的时候按线程分配。

4、堆(认真复习,这一块好复杂)

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
在这里插入图片描述

堆的特点
  • 线程共享,整个Java虚拟机只有一个堆,所有的线程都访问同一个堆。
  • 在虚拟机启动时创建。
  • 是垃圾回收的主要场所。
  • 堆可分为新生代(Eden区:From SurviorTo Survior)、老年代。
  • Java虚拟机规范规定,堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的。
  • 关于Surviror s0,s1区:复制之后有交换,谁空谁是to。

不同的区域存放不同的生命周期的对象,这样可以根据不同的区域使用不同的垃圾回收算法,更具有针对性。
堆的大小既可以固定地也可以扩展,但对于主流的虚拟机,堆的大小是可扩展的,因为当线程请求分配内存,单堆已满,且内存已无法再扩展时,就抛出OutOfMemoryError异常。

新生代与老年代
  • 老年代比新生代生命周期长。、
  • 新生代与老年代空间默认比例1:2:JVM调参数,XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3。
  • HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是8:1:1
  • 几乎所有的Java对象都是在Eden区被new出来的,Eden放不下的大对象,就直接进入老年代了。
对象分配过程
  • new的对象先放在Eden区,大小有限制;
  • 如果创建新对象时,Eden空间填满了,就会触发Minor GC,将Eden不再被其他对象引用的对象进行销毁,再加载新的对象放到Eden区,特别注意的是Survivor区满了是不是触发Minor GC的,而是Eden空间填满了,Minor GC才顺便清理Survivor区,将Eden中剩余的对象移到Survivor0区
  • 再次触发垃圾回收,此时上次Survivor下来的,放在Survivor0区的,如果没有回收,就会放到Survivor1区
  • 再次经历垃圾回收,又会将幸存者重新放回Survivor0区,依次类推
  • 默认是15次的循环,超过15次,则会将幸存者区幸存下来的转去老年区,jvm参数设置次数:-XX:MaxTenuringThreshold=N进行设置
  • 频繁在新生区收集,很少在老年区收集,几乎不在永久区/元空间收集
Full GC/Major GC触发条件

目前已出现的三种GC方式:Major GC、Minor GC、Full GC

  • 显示调用System.gc(),老年代的空间不够,方法区的空间不够等都会触发Full GC,同时对新生代和老年代回收,Full GC的STW的时间最长,应该要避免
  • 在出现Major GC之前,回显触发Minor GC,如果老年代的空间还是不够就会触发Major GC,STW的时间长度Minor GC
逃逸分析
标量替换
  • 标量不可在分解的量,java 的基本数据类型就是标量,标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在 JAVA 中对象就是可以被进一步分解的聚合量
  • 替换过程,通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM 不会创建该对象,而会将该对象成员变量分解若干个被这个方法使用的成员变量所代替。这些代替的成员变量在栈帧或寄存器上分配空间。
  • 使用逃逸分析,编译器可以对代码做如下优化:
    • 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
    • 将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
    • 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。
TLAB

TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存去,是属于Eden区的,这是一个线程专用的内存分配区域,线程私有,默认开启的(当然也不是绝对的,也要看哪种类型的虚拟机)
当然并不是所有的对象都可以在 TLAB 中分配内存成功,如果失败了就会使用加锁的机制来保持操作的原子性
-XX:+UseTLAB使用 TLAB,-XX:+TLABSize 设置 TLAB 大小

5、方法区

Java 虚拟机规范中定义方法区是堆的一个逻辑部分。

方法区
已经被虚拟机加载的类信息
常量
运行时常量池
静态变量
即时编译器编译后的代码
方法区的特点
  • 线程共享。方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
  • 永久代。方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,把方法区称为"永久代"。
  • 内存回收效率低。方法区中的信息一般需要长期存在,回收一遍之后可能只有少量信息无效。主要回收目标是:对常量池的回收;对类型的卸载。
  • Java虚拟机规范对方法区的要求比较宽松。和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收。
运行时常量池

常量就存放在运行时常量池内。
当类被Java虚拟机加载后,.class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如String类的intern()方法就能在运行期间向常量池中添加字符串常量。
在这里插入图片描述

6、直接内存(堆外内存)

操作直接内存

在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在堆中的DirectByteBuffer对象直接操作该内存,而无须先将外部内存中的数据复制到堆中再进行操作,从而提高了数据操作的效率。
直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OutOfMemoryError异常。

直接内存与堆内存比较
  • 直接内存申请控件耗费更多的性能
  • 直接内存读取IO的性能要优化普通的堆内存
  • 直接内存作用链:本地IO -> 直接内存 -> 本地IO
  • 堆内存作用链: 本地IO -> 直接内存 -> 非直接内存 -> 直接内存 -> 本地IO

在这里插入图片描述

二、HotSpot 虚拟机对象探秘

对象的内存布局

内存布局
对象头
哈希码
GC分代年龄
锁状态标志
线程持有的锁
偏向线程ID
偏向时间戳
实例数据
对齐填充

在这里插入图片描述
对象头可能包含类型指针,通过该指针能确定对象属于哪个类。如果对象是一个数组,那么对象头还会包括数组长度。
实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。
对齐填充用于确保对象的总长度为 8 字节的整数倍。

对象的创建过程

在这里插入图片描述
类加载检查:
虚拟机在解析.class文件时,若遇到一条 new 指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。

为新生对象分配内存,分配堆中内存有两种方式:

  • 指针碰撞
  • 空闲列表

初始化:
分配完内存后,为对象中的成员变量赋上初始值,设置对象头信息,

对象的访问方式

所有对象的存储控件都是在堆中分配的,但是这个对象的引用却是在堆栈中分配的。
也就是说在建立一个对象时需要两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存知识一个指向这个堆对象的指针(引用)而已。

对象的访问方式
句柄访问方式
直接指针访问方式,HotSpot采用此类型

句柄访问方式:
在这里插入图片描述
直接指针访问方式:
在这里插入图片描述

三、垃圾收集策略与算法

垃圾收集主要是针对方法区进行;程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。

判断对象是否存活

若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。这个地方的实现类似于C++的指针指针,感兴趣的同学可以去对比下。

对象头维护一个counter计数器
优点
缺点
GC Roots不包括堆中对象所引用的对象来解决循环引用问题
判断对象是否存活
引用计数法
实现简单,判定效率高
难以解决对象循环引用问题,多线程场景需要执行同步操作
可达性分析法

可达性分析法
所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
GC Roots 是指:

  • Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中常量引用的对象
  • 方法区中类静态属性引用的对象

GC Roots 并不包括堆中对象所引用的对象,这样就不会有循环引用的问题。

引用的种类

引用的种类
强引用
常见new申请的对象,只要存在GC不会回收被引用的对象
软引用
JVM认为内存不足时,会尝试去回收软引用指向的对象
弱引用
无论内存是否充足,JVM进行垃圾回收时都会回收只被引用关联的对象
虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,在任何时候都可能被回收

回收堆中无效对象

对于可达分析中不可达的对象,也并不是没有存活的可能。

判定finalize()是否有必要执行

在这里插入图片描述
JVM会判断此对象是否有必要执行finalize()方法,如果对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么视为"没有必要执行"。那么对象基本上就针对被回收了。

如果对象被判定为有必要执行finalize()方法,那么对象会被放入一个F-Queue队列中,虚拟机会以较低的优先级执行这些finalize()方法,但不会确保所有的finalize()方法都会执行结束,如果finalize()方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。

对象重生或死亡

如果在执行finalize()方法时,将this赋给了某一个引用,那么该对象就重生了。如果没有,那么就会被垃圾收集器清除。

任何一个对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,想继续在finalize()中自救就失效了。

回收方法区内存

方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。
方法区中主要清除两种垃圾:

  • 废弃常量:只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。
  • 无用的类:
    • 该类的所有对象都已经被清除
    • 加载该类的ClassLoader已经被回收
    • 该类的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法

    一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除。

垃圾收集算法

学习了如何判定无效对象、无用类、废弃常量之后,剩余工作就是回收这些垃圾。
常见的垃圾算法有以下几个:

垃圾回收算法
标记-清除算法
复制算法
标记-整理算法
分代收集算法
标记-清除算法

在这里插入图片描述
标记的过程是:遍历所有的 GC Roots,然后将所有 GC Roots 可达的对象标记为存活的对象
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。与此同时,清除哪些被标记过对象的标记,以便下次的垃圾回收。

缺点:

  • 效率问题:标记和清除两个过程的效率都不高;
  • 空间问题:标记清除之后会产生大量不连续的内存碎片,碎片太多可能导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(新生代)

在这里插入图片描述
为了解决效率问题,“复制”收集算法出现了。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完,需要进行垃圾收集时,就将存活者的对象复制到另一块上面,然后将第一块内存全部清除。这种算法有优有劣:

  • 优点:不会有内存碎片问题
  • 缺点:内存缩小为原来的一半,浪费空间

为了解决空间利用率问题,可以将内存分为三块:Eden、From Survivor、To Survivor,比例是8:1:1,每次使用Eden和其中一块Survivor。回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用的Survivor空间。这样只有10%的内存被浪费。
但是我们无法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够,需要依赖其他内存(指老年代)进行分配担保。

分配担保

为对象分配内存空间时,如果Eden+Survivor中空闲区域无法装下该对象,会触发MinorGC进行垃圾收集。但如果Minor GC过后依然有超过10%的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再讲新对象存入Eden区。

标记-整理算法(老年代)

在这里插入图片描述
标记:它的第一个阶段与标记-清除算法是一模一样的,均是GC Roots,然后将存活的对象标记。
整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才成为整理阶段。

这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,因此每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制存活的对象,效率很低。

分代收集算法

根据对象存活周期的不同,将内存划分为几块。一般是把Java堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。

  • 新生代:复制算法
  • 老年代:标记-清除算法、标记-整理算法

四、HotSpot垃圾收集器

HotSpot 虚拟机提供了多种垃圾收集器,每种收集器都有各自的特点:

单线程
多线程
多线程
单线程
多线程
多线程
垃圾收集器
新生代垃圾收集器
Serial垃圾收集器
ParNew垃圾收集器
Parallel Scavenge垃圾收集器
老年代垃圾收集器
Serial Old垃圾收集器
Parallel Old 垃圾收集器
CMS垃圾收集器
G1通用垃圾收集器

新生代垃圾收集器

Serial 垃圾收集器(单线程)

只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程,即 Stop The World。Serial 垃圾收集器适合客户端使用。

ParNew 垃圾收集器(多线程)

ParNew 是 Serial 的多线程版本。由多条 GC 线程并行地进行垃圾清理。但清理过程依然需要 Stop The World。
ParNew 追求“低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升;但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial。

Parallel Scavenge 垃圾收集器(多线程)

Parallel Scavenge 和 ParNew 一样,都是多线程、新生代垃圾收集器。但是两者有巨大的不同点:

  • Parallel Scavenge:追求CPU吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
  • ParNew:追求降低用户停顿时间,适合交互式应用。
    吞吐量 = 运行用户代码时间 +(运行用户代码时间 + 垃圾收集时间)

追求高吞吐量,可通过减少GC执行实际过程的时间,然而,仅仅偶尔运行GC意味着每当GC运行时将有许多工作要做,因为在此期间积累了堆中的对象数量很高。,单个GC需要花更多的时间来完成,从而导致更高的暂停时间。而考虑到低暂停时间,最好频繁运行GC以便更快速完成,反过来有导致吞吐量下降。

  • 通过参数 -XX:GCTimeRadio 设置垃圾回收时间占总 CPU 时间的百分比。
  • 通道参数 -XX:MaxGCPauseMills设置垃圾处理过程最久停顿时间。
  • 通过命令 -XX:+UseAdaptiveSizePolicy 开启自适应策略。我们只要设置好堆的大小和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、Eden和Survivor的比例、对象进入老年代和年龄,以最大程度上接近我们设置的MaxGCPauseMills或GCTimeRadio。

老年代垃圾收集器

Serial Old 垃圾收集器(单线程)

Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用。它们唯一的区别就是:Serial Old 工作在老年代,使用“标记-整理”算法;Serial 工作在新生代,使用“复制”算法。

Parallel Old 垃圾收集器(多线程)

Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。

CMS垃圾收集器

CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和GC线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

  • 初始标记:Stop The World,仅使用一条初始标记线程对所有与GC Roots直接关联的对象进行标记。
  • 并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象,速度很慢。
  • 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
  • 并发清除:只使用一条GC线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。

并发标记与并发清除过程耗时最长,且可以与用户一起工作,因此,总体来说,CMC收集器的内存回收过程是与用户线程一起并发执行的。
在这里插入图片描述
CMS的缺点:

  • 吞吐量低
  • 无法处理浮动垃圾
  • 使用“标记-清除”算法产生碎片空间,导致频繁Full GC

对于产生碎片空间问题,可以通过开启 -XX:+UseCMSCompactAtFullCollection,在每次 Full GC 完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块。
设置参数-XX:CMSFullGCsBeforeCompaction 告诉 CMS,经过了 N 次 Full GC 之后再进行一次内存整理。

G1通用垃圾收集器

G1是一款面向服务端应用的垃圾收集器,它没有新生代和老年代的概念,而是将堆划分为一块块独立的Region。当要进行垃圾收集时,首先估计每个Region中垃圾的数量,每次都从垃圾回收价值最大的Region开始回收,因此可以获得更大的回收效率。

从整体上看,G1 是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。

如果不计算维护Remembered Set的操作,G1收集器的工作过程分为以下几个步骤:

  • 初始标记:Stop The World,仅使用一条初始标记线程对所以后与GC Roots直接关联的对象进行标记。
  • 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
  • 最终标记:Stop The World,使用多条标记线程并发执行。
  • 筛选回收:回收废弃对象,此时也要Stop The World,并使用多条筛选回收线程并发执行。

五、内存分配与回收策略

对象的内存分配,就是堆上分配(也可能经过JIT编译后备拆散为标量类型并间接在栈上分配),对象主要分配在新生代的Eden区上,少数情况下可能直接分配在老年代,分配规则不固定,取决于当前使用的垃圾收集器组合以及相关的参数配置。
以下为普遍的内存分配规则:

  • 对象优先在Eden分配
  • 大对象直接进入老年代:-XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配
  • 长期存活的对象将进入老年代: -XXMaxTenuringThreshold 设置新生代的最大年龄
  • 动态对象年龄判定
  • 空间分配担保

可能会触发JVM进行Full GC的情况

  • System.gc()方法的调用:此方法的调用时建议JVM进行Full GC,注意这只是建议而非一定,但在很多情况下它会触发Full GC,从而增加Full GC的频率。通常情况下我们只需要让虚拟机自己去管理内存即可,我们可以通过 -XX:+ DisableExplicitGC 来禁止调用System.gc()
  • 老年代控件不足:老年代控件不足会触发Full GC操作,若进行该操作后空间依然不足,则会抛出如下处理java.lang.OutOfMemoryError: Java heap space
  • 永久代空间不足:JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中已也称为永久代,永久代可能会被占满,会触发 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space
  • CMS GC时出现promotion failed concurrent mode failure promotion failed,就是上文所说的担保失败,而 concurrent mode failure 是在执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。
  • 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。

六、JVM性能调优(后期还得看)

在高性能硬件上部署程序,目前主要有两种方式:

  • 通过64位JDK来使用大内存;
  • 使用若干个32为虚拟机建立逻辑集群来利用硬件资源。

使用 64 位 JDK 管理大内存

++++

使用 32 位 JVM 建立逻辑集群

++++

调优案例分析与实战

++++

七、类文件结构

JVM的“无关性”

谈论JVM的无关性,主要有以下两个:

  • 平台无关性:任何操作系统都能运行Java代码
  • 语言无关性:JVM能运行除Java以外的其他代码

Class文件结构

Class文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全都是连续的0/1。
Class文件中的所有内容被分为两种类型:无符号数、表。

  • 无符号数:无符号数表示Class文件中的值,这些值没有任何类型,但有不同的长度。u1、u2、u4、u8分别代表1/2/4/8字节的无符号数。
  • 表由多个符号数或者其他表作为数据项构成的符合数据类型。
Class文件结构
魔数
版本信息
常量池
访问标志
类索引/父类索引/接口索引集合
字符表集合
方法表集合
属性表集合

魔数

Class 文件的头 4 个字节称为魔数,用来表示这个 Class 文件的类型。Class 文件的魔数是用 16 进制表示的“CAFE BABE”。

版本信息

紧接着魔数的 4 个字节是版本信息,5-6 字节表示次版本号,7-8 字节表示主版本号,它们表示当前 Class 文件中使用的是哪个版本的 JDK。
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。

常量池

版本信息之后就是常量池,常量池中存放两种类型的常量:

  • 字面值常量:定义的字符串、被final修饰的值
  • 符号引用:类和接口的全限定名、字段的名字和描述符、方法的名字和描述符
常量池的特点
  • 常量池中常量数量不固定,因此常量池开头放置一个u2类型的无符号数,用于存储当前常量池的容量。
  • 常量池的每一项常量都是一个表,表开头的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种常量类型。
访问标志

在常量池结束之后,紧接着的两个字节代表访问标志, 这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否被abstract/final修饰。

类索引、父类索引、接口索引集合

类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。

字段表集合

字段表集合存储本类涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。

方法表集合

方法表结构与属性表类似。
volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有ACC_VOLATILE和ACC_TRANSIENT标志。
方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译后的字节码指令。

属性表集合

每个属性对应一张属性表,属性表的结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

八、类加载的时机

类的生命周期

类从被加载到虚拟机内存开始,到卸载出内存位置,它的整个生命周期包括以下7个阶段:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

验证、准备、解析3个阶段统称为连接
在这里插入图片描述
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始),而解析阶段则不一定:它在某些情况下可以在初始化后再开始,这是为了支持Java语言的运行时绑定。

类加载过程中“初始化”开始的时机

Java虚拟机规范没有强制约束类加载过程的第一阶段(即:加载)什么时候开始,但对于“初始化”阶段,有着严格的规定。有且仅有5种情况必须立即对类进行“初始化”:

  • 在遇到new、putstaitc、getstatic、invokestatic字节码指令时,如果类尚未初始化,则需要先触发其初始化。
  • 对类进行反射调用时,如果类还没有初始化,则需要先触发其初始化。
  • 初始化一个类时,如果其父类还没有初始化,则需要先初始化父类。
  • 虚拟机启动时,用于需要指定一个包含main()方法的主类,虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发其初始化。

这5种场景中的行为称为对一个类的进行主动引用,除此以外,其它所有引用类的方式都不会触发初始化,称为被动引用

接口的加载过程

接口加载过程与类加载过程稍有不同。

当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。

九、类加载的过程

类加载过程包括5个阶段:加载、验证、准备、解析和初始化。

加载

在加载阶段,虚拟机需要完成3件事:

  • 通过类的全限定名获取该类的二进制字符流。
  • 将二进制字节流所代表的静态结构转换为方法区的运行时数据结构。
  • 在内存中创建一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
获取二进制字符流

对于Class文件,虚拟机没有指明要从哪里获取、怎样获取,除了直接从编译的.class文件中读取,还有以下几种方式:

  • 从 zip 包中读取,如 jar、war 等;
  • 从网络中获取,如 Applet;
  • 通过动态代理技术生成代理类的二进制字符流;
  • 由JSP文件生成对应的Class类;
  • 从数据库中读取,如有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
"非数组类"与"数组类"加载比较
  • 非数组类加载阶段可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器控制字节流的获取方式(如重写一个类加载器的loadClass()方法)。
  • 数组类本身不通过类加载器创建,它是有Java虚拟机直接创建的,再由类加载器创建数组中的元素类。
注意事项
  • 虚拟机规范未规定 Class 对象的存储位置,对于 HotSpot 虚拟机而言,Class 对象比较特殊,它虽然是对象,但存放在方法区中。
  • 加载阶段与连接阶段的部分内容交叉进行,加载阶段尚未完成,连接阶段可能已经开始了。但这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证阶段确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备

准备阶段是正式为类变量(或称“静态成员变量”)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化

类初始化阶段是类加载过程的最后一步,是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。
<clinit>()方法不需要显式调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。

由于父类的<clinit>()方法先执行,意味着父类中定义的静态语句块要优先与子类的变量赋值操作。

<clinit>() 方法不是必需的,如果一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中不能使用静态代码块,但接口也需要通过<clinit>()方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的<clinit>()方法不需要先执行父类的<clinit>()方法,只有当父接口中定义的变量使用的,父接口才会初始化。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正常加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法。

十、类加载器

类与类加载器

判断类是否"相等"

任意一个类,都由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。

因此,比较两个类是否“相等”,只有在这个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只有加载它们的类加载器不同,那么这两个类就必定不相等。

这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

加载器种类
系统提供3种加载器
启动类加载器
扩展类加载器
应用程序类加载器
  • 启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录中的,并且能被虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为“系统类加载器”。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    在这里插入图片描述
    当然,如果有必要,还可以加入自己定义的类加载器。

双亲委派模型

什么是双亲委派模型

双亲委派模型是描述类加载器之间的层次关系。它要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。(父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码)

工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给辅流加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载。

在java.lang.ClassLoader中的loadClass方法中实现该过程。

为什么使用双亲委派模型

像java.lang.Object这些存放在rt.jar中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载,从而给使得不同加载器加载的Object类都是同一个。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个成为java.lang.Object的类,并放在classpath下,那么系统将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证。

参考

  • JVM相关
  • JVM 底层原理最全知识总结
  • ♥JVM相关知识体系详解♥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/90489.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/90489.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/90489.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

便捷删除Android开发中XML中重复字符串资源的一个办法

从android系统源码中移植一些app到android studio开发的时候可能会遇到字符串重复的编译报错。一个办法是把重复的删除&#xff0c;只剩余一条即可。例如下面的编译错误&#xff1a;Found item String/abc more than one time但是呢&#xff0c;xml中一般这种重复的很多很多&am…

免模型控制

文章目录免模型控制Q-Learning 算法原理Sarsa 算法区别&#xff1a;免模型控制 免模型控制要解决的问题是&#xff0c;如何选择动作以达到最高得分 Q-Learning 算法 原理 首先Q-Learning 确定了一个前提最优策略&#xff1a;π(s)arg⁡max⁡aQ(s,a)\pi(s) \arg\max_a Q(s,…

Vmware VSAN主机停机维护流程

当VSAN主机由于故障或进行扩容操作需要停机维护时&#xff0c;在关闭ESXi主机前和启动ESXi主机后需要进行一些必要的检查操作&#xff0c;以免对vSAN集群环境造成不可预知的风险&#xff0c;影响集群中的虚拟机运行。以下是vSAN集群中的ESXi主机停机维护的主要步骤。 1.确认受影…

中小企业安全落地:低成本漏洞管理与攻击防御方案

中小企业普遍面临 “预算有限、技术人员不足” 的困境&#xff0c;安全建设常陷入 “想做但做不起” 的尴尬。事实上&#xff0c;中小企业无需追求 “高大上” 的安全方案&#xff0c;通过 “开源工具 简化流程 聚焦核心” 的思路&#xff0c;即可用低成本实现有效的漏洞管理…

面试150 搜索二维矩阵

思路1 直接遍历搜寻&#xff0c;逐个判断即可 class Solution:def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:mlen(matrix)nlen(matrix[0])for i in range(m):for j in range(n):if matrix[i][j]target:return Truereturn False思路2 Z字形搜索从矩…

npm init vite-app runoob-vue3-test2 ,npm init vue@latest,指令区别

这两个命令都是用于创建 Vue.js 项目的脚手架命令&#xff0c;但它们在技术栈、配置方式和项目结构上有显著区别&#xff1a;1. npm init vite-app runoob-vue3-test2技术栈&#xff1a;基于 Vite 构建工具使用 Vue 3 作为默认框架由 Vite 团队维护特点&#xff1a;bash复制代码…

WPF MVVM进阶系列教程(二、数据验证)

五一出去浪吹风着凉了&#xff0c;今天有点发烧&#x1f637; 手头的工作放一放&#xff0c;更新一下博客吧。 什么是数据验证(Validation) 数据验证是指用于捕获非法数值并拒绝这些非法数值的逻辑。 大多数采用用户输入的应用都需要有验证逻辑&#xff0c;以确保用户已输入…

AI 音频产品开发模板及流程(二)

AI 音频产品开发模板及流程&#xff08;一&#xff09; 6. 同声传译 实时翻译&#xff0c;发言与翻译几乎同步&#xff0c;极大提升沟通效率。支持多语言互译&#xff0c;适用于国际会议、商务洽谈等多场景。自动断句、转写和翻译&#xff0c;减少人工干预&#xff0c;提升准…

kafka4.0集群部署

kafka4.0是最新版kafka&#xff0c;可在kafka官网下载&#xff0c;依赖的jdk版本要求在jdk17及jdk17以上tar -xzf kafka_2.13-4.0.0.tgzmv kafka_2.13-4.0.0 kafkacd kafka# 随便一台节点运行生成随机uuid&#xff0c;后面每台节点都要使用此uuidbin/kafka-storage.sh random-u…

【News】同为科技亮相首届气象经济博览会

7月18日&#xff0c;由中国气象服务协会主办的国内首个以“气象经济”为核心的国家级博览会——首届气象经济博览会&#xff08;以下简称“博览会”&#xff09;在合肥滨湖国际会展中心开幕。北京同为科技有限公司&#xff08;TOWE&#xff09;作为雷电防护领域的技术领导企业&…

数据结构 堆(2)---堆的实现

上篇文章我们详细介绍了堆和树的基本概念以及它们之间的关系&#xff0c;还要知道一般实现堆的方式是使用顺序结构的数组进行存储数据及实现。下来我们看看利用顺序结构的数组如何实现对的内容:1.堆的实现关于堆的实现&#xff0c;也是三个文件&#xff0c;头文件&#xff0c;实…

Arraylist与LinkedList区别

&#x1f4da; 欢迎来到我的Java八股文专栏&#xff01; &#x1f389;各位程序员小伙伴们好呀~ &#x1f44b; 我是雪碧聊技术&#xff0c;很高兴能在CSDN与大家相遇&#xff01;✨&#x1f680; 专栏介绍这个专栏将专注于分享Java面试中的经典"八股文"知识点 &…

Java实战:基于Spring Cloud的电商微服务架构设计——从拆分到高可用的全流程解析

引言 2023年双十一大促期间,某传统电商平台的单体应用再次“爆雷”:凌晨1点订单量突破50万单/分钟时,用户服务因数据库连接池被订单模块占满,导致登录接口响应时间从200ms飙升至5秒,大量用户流失。技术团队紧急回滚后发现:这个运行了7年的单体应用,早已变成“代码泥潭”…

STL学习(二、vector容器)

1.vector构造函数函数原型vector<int> v // 默认构造&#xff0c;size为0vector(const_iterator beg, const_iterator end) // 将v的[begin, end) 元素拷贝过来vector(n, elem) // 构造函数将n个elem拷贝到本身vector(const vector & v) // 拷贝构造2.vect…

深度学习-算子

概念&#xff1a;标识数字图像中亮度变化明显的点处理步骤1.滤波处理算子通常被称为滤波器。2.增强确定各点sobel算子概念&#xff1a;主要用于获得数字图像的一阶梯度&#xff0c;本质是梯度运算。Scharr算子Scharr算子 是一种用于边缘检测的梯度算子&#xff0c;它是Sobel算子…

全国产8通道250M AD FMC子卡

4片8路ADS42LB69标准FMC采集子卡自研成品ADC采集子卡和定制化设计ADC采集子卡&#xff0c;实测采集指标均与手册标称值一致。该板卡有全国产化和进口两个版本&#xff0c;基于FMC标准设计&#xff0c;实现8路16bit/250MSPS ADC采集功能&#xff0c;遵循 VITA 57 标准&#xff0…

【牛客网C语言刷题合集】(三)

&#x1f31f;菜鸟主页&#xff1a;晨非辰的主页 &#x1f440;学习专栏&#xff1a;《C语言刷题集》 &#x1f4aa;学习阶段&#xff1a;C语言方向初学者 ⏳名言欣赏&#xff1a;"任何足够先进的bug都与魔法无异。" 前言&#xff1a;刷题博客主要记录在学习编程语言…

Python之--字典

定义字典&#xff08;dict&#xff09;是一种无序、可变且可哈希的数据结构&#xff0c;字典是根据一个信息来查找另一个信息&#xff0c;它表示索引用的键和对应的值构成的成对关系。特点&#xff08;1&#xff09;字典与列表一样&#xff0c;是Python里面的可变数据类型。&am…

【ARM】ARM微架构

1、 文档目标对 ARM 微架构的概念有初步的了解。2、 问题场景在和客户沟通和新同事交流时对于 ARM 架构和微架构二者有什么区别和联系&#xff0c;做一个简单的介绍。3、软硬件环境1、软件版本&#xff1a;不涉及2 、电脑环境&#xff1a;不涉及4、关于 ARM 架构和微架构架构不…

c++注意点(11)----设计模式(工厂方法)

创建型模式工厂方法模式是一种创建型设计模式&#xff0c; 其在父类中提供一个创建对象的方法&#xff0c; 允许子类决定实例化对象的类型。为什么需要工厂方法模式&#xff1f;看一个 “没有工厂模式” 的痛点场景&#xff1a;假设你在开发一个游戏&#xff0c;最初只有 “战士…