jvm基础知识

jvm基础知识

参考文档

  1. https://blog.csdn.net/qq_25046005/article/details/104352616
  2. https://blog.csdn.net/lpw_cn/article/details/84594859

一、Java 内存区域与内存溢出异常

1 概述

java把控制内存的权利交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那排查错误,修正问题将会成为一项异常艰难的工作。

本博客将讲述java内存模型及故障处理工具。

2 运行时数据区域

java虚拟机在执行java程序的过程中会把所管理的内容划分为若干个不同的数据区域。如图:
在这里插入图片描述

2.1 程序计数器

程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于java虚拟机的多线程是通过线程的轮流切换、分配处理器执行时间的方式来实现的,在任何一个时刻,一个处理器都只会执行一个线程中的指令。因此,为了线程切换后能回到正确的执行位置,每个线程需要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,即程序计数器是线程私有的内存区域。

如果线程正在执行的一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是本地(Native)方法,计数器的值为空。此内存区域是唯一一个在《java虚拟机规范》中没有规定任何OutOfMemeryError情况的区域。

2.2 java虚拟机栈

java虚拟机栈也是线程私有的,它生命周期与线程相同。虚拟机栈描述的是java方法执行的线程内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它并不等同于对象本身,可能是一个执行对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

操作数栈:也常来被称为操作栈,它是一个后入先出栈。在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈。

动态链接:Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分将在类加载阶段或第一次使用的时候就被转化为直接引用,这种转化称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这种转化被称为动态链接。

方法返回地址:方法执行开始后,有两种方式退出这个方法。第一种方式正常调用完成,另一种退出方式是在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。

2.3 本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会StackOverflowError 和 OutOfMemoryError 异常。

2.4 java堆

对于java程序来说,java堆是虚拟机所管理的内存中最大的一块。java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。java堆的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。《java虚拟机规范》中堆java堆的描述是:所有的对象实例以及数组都应当在堆上分配内存。

java堆是垃圾收集器管理的内存区域,因此也被称为”gc堆“。垃圾回收也指的是回收堆内存空间。

由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以java堆中会经常出现“新生代“”老年代“”永久代”“Eden空间“”From Survivor空间“”To Survivor空间“等名词。这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,并非是java虚拟机具体实现的固有内存布局。

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

2.4.1 java堆内存模型
2.4.1.1 堆内存分配原则
  1. 对象优先分配在 Eden:
    当对象在 Eden出生后,在经过一次 Minor GC 后,存活的对象复制到S1区,然后清理Eden 以及 S0区域 ,并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁),这些对象就会进入老年代。
  2. 大对象直接进入老年代:
    需要大量连续内存空间的对象,长字符串或数组
  3. 长期存活的对象将进入老年代:
    年龄到一定程度(默认为15),进入老年代
  4. 动态对象年龄判定:
    如果survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半,年龄大于等于该年龄的对象就可以进入老年代
  5. 空间分配担保:
    Minor Gc后大量对象存活,需要老年代进行分配担保,把survivor无法容纳的对象直接进入老年代
2.4.1.2 堆内存模型

在这里插入图片描述

2.5 方法区

方法区是线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

运行时常量池也是方法区的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内存在类加载后存放到方法区的运行时常量池中。

方法区实现变化:

​ jdk1.7之前:方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;

​ jdk.7:存储在永久代的部分数据就已经转移到Java Heap或者Native memory。但永久代仍存在于JDK 1.7中,并没有完全移除,譬如符号引用(Symbols)转移到 了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics variables )转移到了Java heap;

​ jdk1.8:仍然保留方法区的概念,只不过实现方式不同。取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上 可认为在堆中。

 1 移除了永久代(PermGen),替换为元空间(Metaspace);
 2 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
 3 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
 4 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)。

3 HotSpot 虚拟机对象探秘

3.1对象的创建

1 当java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那么必须先执行相应的类加载过程。

2 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。

对象的大小在类加载后完全可以确定,为对象分配空间的任务实际等同于把一块确定大小的内存从java堆中划分出来。

分配方式有两种:

​ 1) 指针碰撞

​ 假设java堆中的内存是绝对规整的,所有被使用的内存都被放到一边,空闲的放到另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把 那个指针向空方向挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。

​ 2) 空闲列表

​ 如果java堆中的内存不是规整的,已被使用的内存和未被使用的内存相互交错在一起,那就没办法简单的使用指针碰撞了,虚拟机就必须维护一个列表,记录 哪些内存块是可用的,在分配内存的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表。

3 内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值。

4 填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。

5 执行<init>方法,安装程序员的意愿对对象初始化

3.2 对象的内存布局

在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit,官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

3.3 对象的访问定位

使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

通过句柄访问

Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。详情见图。

在这里插入图片描述

使用直接指针访问

reference 中直接存储对象地址

在这里插入图片描述

4 JVM 执行方法过程

根据一个代码实例来介绍虚拟机中解释器的执行过程,代码如下所示:

public int calculate(){  
    int a = 100;  
    int b = 200;  
    int c = 300;  
    return (a + b) * c;  
}

由上面的代码可以看出,该方法的逻辑很简单,就是进行简单的四则运算加减乘除,我们编译代码后使用javap -verbose命令查看字节码指令,具体字节码代码如下所示:

public int calculate();  
  Code:  
   Stack=2, Locals=4, Args_size=1  
   0:   bipush  100  
   2:   istore_1  
   3:   sipush  200  
   6:   istore_2  
   7:   sipush  300  
   10:  istore_3  
   11:  iload_1  
   12:  iload_2  
   13:  iadd  
   14:  iload_3  
   15:  imul  
   16:  ireturn  
}  

根据字节码可以看出,这段代码需要深度为2的操作数栈(Stack=2)和4个Slot的局部变量空间(Locals=4)。下面,使用7张图片来描述上面的字节码代码执行过程中的代码、操作数栈和局部变量表的变化情况。

首先,执行偏移地址为0的指令,Bipush指令的作用

是将单字节的整型常量值(-128~127)推入操作数栈,

跟随有一个参数,指明推送的常量值,这里是100。

执行偏移地址为2的指令,istore_1指令的作用是将操

作数栈顶的整型值出栈并存放到第一个局部变量槽中。

后续4条指令(直到偏移为11的指令为止)都是做一

样的事情也就是在对应代码中把变量a、b、c赋值为

100、200、300。

执行偏移地址为11的指令,iload_1指令的作用是将局

部变量表第一个变量槽中的整型值复制到操作数栈顶。

执行偏移地址为12的指令,iload_2指令的执行与过程

与iload_1类似,把第2个变量槽的整型值入栈。

执行偏移地址为13的指令,iadd指令的作用是将操作数

栈中头两个栈顶元素出栈,做整型加法,然后把结果重

新入栈。在iadd指令执行完毕后,栈中原有的100和200

出栈,它们的和300被重新入栈。

执行偏移地址为14的指令,iload_3指令把存放在第3个

局部变量槽中的300入栈到操作数栈中。这时候操作数栈

为两个整数300。下一条指令imul是将操作数栈中头两个

栈顶元素出栈,做整型乘法,然后把结果重新入栈。

执行偏移地址为16的指令,ireturn指令是方法返回指

令之一,它将结束方法执行并将操作数栈顶的整型值返回

给该方法的调用者。到此为止,这段方法执行结束。

二 垃圾收集器

1 垃圾回收器

收集算法是内存回收的理论,而垃圾回收器是内存回收的实践。
在这里插入图片描述

说明:如果两个收集器之间存在连线说明他们之间可以搭配使用。

1.1 Serial 收集器

这是一个单线程收集器。意味着它只会使用一个 CPU 或一条收集线程去完成收集工作,并且在进行垃圾回收时必须暂停其它所有的工作线程直到收集结束。
在这里插入图片描述

1.2 ParNew 收集器

可以认为是 Serial 收集器的多线程版本。
在这里插入图片描述
并行:Parallel

指多条垃圾收集线程并行工作,此时用户线程处于等待状态

并发:Concurrent

指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。

1.3 Parallel Scavenge 收集器

这是一个新生代收集器,也是使用复制算法实现,同时也是并行的多线程收集器。

在这里插入图片描述

CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程所停顿的时间,而 Parallel Scavenge 收集器的目的是达到一个可控制的吞吐量(Throughput = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))。

作为一个吞吐量优先的收集器,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整停顿时间。这就是 GC 的自适应调整策略(GC Ergonomics)。

1.4 Serial Old 收集器

Serial 收集器的老年代版本,单线程,使用 标记 —— 整理。

1.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本。多线程,使用 标记 —— 整理

1.6 CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于 标记 —— 清除 算法实现。
运作步骤:

  1. 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
  2. 并发标记(CMS concurrent mark):进行 GC Roots Tracing
  3. 重新标记(CMS remark):修正并发标记期间的变动部分
  4. 并发清除(CMS concurrent sweep)
    在这里插入图片描述缺点:对 CPU 资源敏感、无法收集浮动垃圾、标记 —— 清除 算法带来的空间碎片
1.7 G1 收集器

面向服务端的垃圾回收器。
优点:并行与并发、分代收集、空间整合、可预测停顿。

运作步骤:

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)
    在这里插入图片描述

2 常见配置汇总

堆设置

-Xms 初始堆大小
-Xmx 最大堆大小
-XX:NewSize=n 设置年轻代大小
-XX:NewRatio=n 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n 设置持久代大小

收集器设置

-XX:+UseSerialGC 设置串行收集器
-XX:+UseParallelGC 设置并行收集器
-XX:+UseParalledlOldGC 设置并行年老代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器

垃圾回收统计信息
-XX:+PrintGC`
`-XX:+PrintGCDetails`
`-XX:+PrintGCTimeStamps`
`-Xloggc:filename
并行收集器设置

-XX:ParallelGCThreads=n 设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n 设置并行收集最大暂停时间
-XX:GCTimeRatio=n 设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

-XX:+CMSIncrementalMode 设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n 设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

3 Jvm调优

调优的基础是 使用Jdk8及以下,因为Jdk9 默认使G1收集器,一个比较牛的收集器。

3.1 默认配置

使用java -XX:+PrintCommandLineFlags -version --打印命令行参数(可以看默认垃圾回收器)

-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_111"
Java(TM) SE Runtime Environment (build 1.8.0_111-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.111-b14, mixed mode) 

3.2 吞吐量优先的并发收集器

主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

配置1:配置回收最大线程

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

解释:

-XX:+UseParallelGC 选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

-XX:ParallelGCThreads=20 配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。

-XX:+UseParallelOldGC 配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

配置2:规定的最低相应时间

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100-XX:+UseAdaptiveSizePolicy

解释:

-XX:+UseParallelGC 选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。

-XX:+UseParallelOldGC 配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

-XX:MaxGCPauseMillis=100 设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

-XX:+UseAdaptiveSizePolicy 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

3.3 响应时间优先的并发收集器

主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。

配置1:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

解释:

-XX:+UseConcMarkSweepGC设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC 设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

配置2:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 
-XX:+UseCMSCompactAtFullCollection

解释:

-XX:CMSFullGCsBeforeCompaction 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection 打开对年老代的压缩。可能会影响性能,但是可以消除碎片

三 虚拟机类加载机制

1 概述

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、装换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
在 Java 语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。

2 类加载的七个阶段

在这里插入图片描述
图中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段后再开始,这是为了支持java语言的运行时绑定(也称为动态绑定或晚期绑定)。
虚拟机规范严格规定了有且仅有5种情况必须立即对类进行“初始化“(而加载、验证、准备自然需要在此之前)。

1 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
2 使用 java.lang.reflect 包的方法对类进行反射调用的时候。
3 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
4 当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
5 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。

前面的五种方式是对一个类的主动引用,除此之外,所有引用类的方法都不会触发初始化,佳作被动引用。
举几个被动引用例子:

public class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }
    public static int value = 1127;
}
 
public class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}
 
public class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }
    public static final String HELLOWORLD = "hello world!"
}
 
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
        /**
         *  output : SuperClass init!
         * 
         * 对于静态字段,只有直接定义这个字段的类才会被初始化,
         * 因此通过其子类来引用父类中定义的静态字段,
         * 只会触发父类的初始化而不会触发子类的初始化
         */
 
        SuperClass[] sca = new SuperClass[10];
        /**
         *  output : 
         * 
         * 通过数组定义来引用类不会触发此类的初始化
         * 虚拟机在运行时动态创建了一个数组类
         */
 
        System.out.println(ConstClass.HELLOWORLD);
        /**
         *  output : 
         * 
         * 常量在编译阶段会存入调用类的常量池当中,本质上并没有直接引用到定义类常量的类,
         * 因此不会触发定义常量的类的初始化。
         * “hello world” 在编译期常量传播优化时已经存储到 NotInitialization 常量池中了。
         */
    }
}

2.1 加载
1 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。

数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:

1 如果数组的组件类型是引用类型,那就递归采用类加载加载。
2 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
3 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。
加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。

2.2 验证

是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。

2.2.1 文件格式验证
1 是否以魔数 0xCAFEBABE 开头
2 主、次版本号是否在当前虚拟机处理范围之内
3 常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
4 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
5 CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
6 Class 文件中各个部分集文件本身是否有被删除的附加的其他信息

只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。

2.2.2 元数据验证
1 这个类是否有父类(除 java.lang.Object 之外)
2 这个类的父类是否继承了不允许被继承的类(final 修饰的类)
3 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
4 类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)

这一阶段主要是对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。

2.2.3 字节码验证
1 保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
2 保证跳转指令不会跳转到方法体以外的字节码指令上
3 保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)

这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。

2.2.4 符号引用验证
1 符号引用中通过字符创描述的全限定名是否能找到对应的类
2 在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
3 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问

最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。
符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

2.3 准备

这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

public static int value = 123;

变量value 在准备阶段过后的初始值为0而不是123,因为这个时候尚未执行任何java方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段(初始化阶段执行clinit()(初始化类变量和静态语句块(static{})))才会对 value 进行赋值。

public static final int value = 123;

编译时javac将会为value生成ConstatValue属性,在准备阶段虚拟机就会根据ConstatValue的设置将value赋值为123。

2.4 解析

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

1 符号引用
符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
2 直接引用
直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。

2.5 初始化

前面过程都是以虚拟机主导,而初始化阶段开始执行类中的 Java 代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员的主观计划去初始化类变量和其它资源。
其实初始化阶段就是执行类构造器()方法的过程。

1 <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,收集的顺序是文件中的编写顺序,
	静态语句块只能访问到定义在静态块之前的变量,定义在之后的变量,在静态块中可以赋值,但是不能访问。
	示例:非法向前引用变量
	public class Test {
	 static{
	 	i=0;//给变量赋值可以正常编译通过
	 	Sysout.out.print(i);// 这句话会提示 非法向前引用
	 }
	 static int i = 1;
	}
2 <clinit>()方法与 实例构造器的<init>()不同。作用不同。
3 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,
	示例:结果输出的B的值是2。
	static calss Parent{
		public static int A = 1;
		static{
			A = 2;
		}
	}
	static class Sub entends Parent{
		public static int B = A;
	}
	public static void main (String[] args){
		System.out.println(Sub.B)
  	}

3 类加载器

通过一个类的全限定名来获取描述此类的二进制字节流。

3.1 双亲委派模型

从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)

1 启动类加载器
加载 lib 下或被 -Xbootclasspath 路径下的类
2 扩展类加载器
加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类
3 引用程序类加载器
ClassLoader负责,加载用户路径上所指定的类库。
在这里插入图片描述

除顶层启动类加载器之外,其他都有自己的父类加载器。
工作过程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是把这个请求委派给父类加载器。只有父类无法完成时子类才会尝试加载。

3.2 破坏双亲委派模型

三次较大规模的破坏双亲委派模型
1 JDK1.2时期为了向下兼容
2 父类加载器需要请求子类加载器去完成类加载(JBDI、JDBC、JCE、JAXB、JBI)
3 OSGI 实现模块化热部署

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值