Java虚拟机

本文详细介绍了Java虚拟机(JVM)的工作原理,包括内存区域、垃圾回收机制、类加载过程以及内存溢出异常的处理。JVM内存分为堆、方法区、虚拟机栈、本地方法栈和程序计数器,其中堆和方法区是垃圾回收的主要区域。垃圾回收使用可达性分析算法,分为新生代和老年代,采用不同的收集策略。此外,文章还讨论了类加载器、运行时常量池和内存分配与回收算法,以及如何通过参数设置和工具进行内存优化。
摘要由CSDN通过智能技术生成

书籍

《深入学习Java虚拟机》

了解虚拟机的目的

Java把控制内存的权力交给Java虚拟机,一旦出现内存泄漏或溢出问题,不了解虚拟机怎么使用内存,那么排查修正错误将会毫无方向。学好jvm也可以更深入理解Java数据在操作系统层上的变化过程,配合《Core Java》使用极佳

JVM工作过程七步骤

加载,字节码校验,静态变量初始化,引用转换,对象实例化,反射调用,GC回收

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

JVM 的主要组成部分及其作用

类加载系统包含各级别类加载器。根据字节码来把类放入方法区

执行引擎中有解释器,编译器,垃圾回收器。
前端编译器会将java文件变为字节码,也是语法糖的放置地方
解释器会直接编译字节码为机器码并执行
即时编译器也用于编译字节码文件为机器语言,它只会编译热点代码所在的完整方法体,一般热点的判断基于周期性采样栈顶方法或统计方法出现次数来判断。
另外,也存在提前编译器(直接把程序编译成本地机器码),但很少使用
垃圾回收器用于回收堆空间不常用的类。

内存区域含堆,程序计数器,方法区,虚拟机栈,本地方法栈。
堆中存放着对象
程序计数器存储着下一条字节码指令的位置
方法区存储着类和常量,静态变量,即使编译后的代码的信息
虚拟机栈存放着方法帧,线程隔离。虚拟栈中的局部变量表存放着函数return地址,对象引用地址,基础类型的值。
本地方法栈存放着本地方法帧,线程隔离。
本地方法是由其它语言编写的,编译成和处理器相关的机器代码。本地方法保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的(JAVA方法是与平台无关的,但是本地方法不是。)

虚拟机栈方法的载体-栈帧

栈帧是虚拟机栈的栈元素,也是线程特有的,它是用于支持方法和调用的数据结构,也就是说每个方法都有一个,存储了局部变量表,操作数栈,动态链接,方法返回地址和一些额外附加信息。栈的局部变量表和操作数栈在编译阶段已经被解析和写入方法表的Code属性。对于运行的线程,只有处于头部的栈帧是正在运行的。

局部变量表和它的单位slot

Java程序在编译时就会在方法Code属性中max_locals规定其最大容量
局部变量表的索引单位,每一单位代表32位,即使在64位虚拟机中使用64位物理内存空间实现变量槽,也会通过对齐和补白使外观与32位的一样。
Java中占用不超过32位的有boolean,byte,char,short,int,float,reference,returnAddress,reference表示对一个对象的引用,它至少实现两件事,可以找到堆中对象首地址或索引和可以找到方法区中对应类型信息。returnAddress存放正常情况下的返回地址,异常情况下返回地址在绝大多数虚拟机是放在异常表的
Java中明确64位只有long和double
如果是实例方法第0位索引会被默认为this,其余参数按参数表,方法内声明顺序依次下排。
slot可以复用,当变量超出作用域时,新变量就可以直接使用该变量槽。不过在某些情况下这会影响垃圾回收
未赋值的局部变量常常会在编译器被检查到,即使侥幸通过也会在类加载时被发现而失败

操作数栈

java虚拟机的解释引擎是基于栈的执行引擎,解释引擎会根据字节码指令和操作数栈中的操作数解释为二进制码以供cpu执行,每种类型必须用其固定类型的字节码,除了short,char,byte,int根据需求变为不同int型,也就是通用iload,iadd等方法。i++于++i的区别,其实也仅在于字节码顺序

Java程序在编译时就会在方法Code属性中max_stacks规定其最大容量
一个单位深度仍是32位
load 将一个变量从局部变量表压入操作数栈
store 将一个变量从操作数栈存入局部变量表
push 将一个值压入栈

栈顶缓存技术:一种将栈顶操作数栈元素缓存到cpu寄存器来提高执行效率的方法

动态链接与静态链接

字节码文件中常量池有大量指向实际地址引用的符号引用,而链接就是指向符号引用,动态链接是指在执行时把这些符号引用改为实际地址引用,静态链接是指在类加载的解析阶段就把符号引用换成了直接引用

堆的作用

存放对象实例

周期大小比较

Java堆(虚拟机级)=方法区(虚拟机级)> 本地方法区(线程级)>程序计数器(线程级)==虚拟机栈

运行时常量池与方法区的关系,作用上的区别

方法区是一种逻辑归纳,是除了对象以外的需要共享的数据的集合
运行时常量池是方法区的一部分,除此之外还有类型信息,常量,静态变量,即时编译器的代码缓存等
常量池存储用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到运行时常量池
编译期:java->.class的过程
字面量:直接量,变量对应的值,通常存在于对变量的赋值
符号引用:某个变量,在编译的时候,无法确定其内存地址,通常存在于对变量的操作。

本地变量表,本地方法栈

本地变量表就是临时变量表,位于虚拟机栈
本地方法栈是和虚拟机栈同属于线程专有的,用于装Native(即非Java语言实现)的方法

在HotSpot虚拟机使用对象

创建

创建的简要步骤

1 类加载检查
检查常量池是否有类的指向,有则检查该指向是否有加载,解析和初始化过,否则就重新进行类加载
2 内存分配
根据Java堆的垃圾收集器是否带有空间压缩整理能力,使用指针碰撞或空闲列表的方式分配内存和更新空间
但是对当前对象分配内存和对空闲空间的重新指定在多线程时并非线程安全的,所以要不各线程分别指定缓冲区,在缓冲区用完后通过同步分配新缓冲区(这种行为称为Thread Local Allocation Buffer,TLAB);要不将分配内存和指定空间给同步起来
3 内存空间初始化为零值
使用TLAB时可以在内存分配时完成。
4 设置对象
根据属性设置对象,根据虚拟机当前运行状态设置对象头

在Java程序视角中的创建

对象创建才刚开始,构造方法尚未执行(Class文件的())

内存布局

内存划分

对象头
MarkWord和类型指针。MarkWord存储哈希码,GC分代年龄,持有锁,偏向锁ID等,类型指针;类型指针指向实例的类

实例数据
我们定义的各种类型的字段值,无论是父类继承的,还是子类定义的

对象填充
占位符,由于HotSpot自动内存管理系统要求对象起始地址必须是8字节的整数倍

对于引用对象的两种访问定位

1 堆有额外的句柄池,句柄池里有指向堆实例池数据的指针,和方法区类型数据的指针
2 虚拟栈reference类型直接指向堆实例数据,数据中有方法区类型数据的指针

内存溢出异常

Java堆溢出

参数设定

-Xms :堆的最小值
-Xmx :堆的最大值
-XX:+HeapDumpOnOutOfMemoryError :让虚拟机在内存溢出时Dump内存堆转储快照

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。

溢出判断

使用内存影响分析工具,确认OOM对象是否必要,分清内存泄漏还是内存溢出。如果是内存泄漏,用工具查看对象到GC Roots的引用链,定位到对象创建的位置,找到泄漏代码。如果是内存溢出,调整最大最小堆参数,调整垃圾回收算法更快更全面地更新堆。

虚拟机栈和本地方法栈溢出

-Xss :设置每个线程栈大小

栈的两种异常情况:

StackOverflowError :达到规定栈大小,如局部变量过多,多次调用方法栈帧挤满栈
OutOfMemoryError:一些虚拟机可以在栈内存不够时向JVM再次申请内存,实现栈的动态扩展,但目前HotSpot虚拟机只允许在创建线程时申请内存,也就是说线程运行不会因为扩展导致内存溢出。如果想在HotSpot虚拟机上发生这种问题,需要建立很多线程

多线程在不减少线程数或更换64位虚拟机,可以通过减少最大堆(每个进程在操作系统上都有其最大内存,堆栈的少了,栈可以用的内存就大了)和减少栈容量来换取更多的线程。

方法区和运行时常量池溢出

在JDK6前常量池分配在永久代,可以用以下间接限制常量池大小
-XX:PermSize
-XX:MaxPermSize 限定永久代大小
而JDK8中把常量池放到堆空间,元空间代替永久代,而且元空间不在虚拟机中而在本地内存,这使得更难填满元空间,但为了防止动态代理,反射等恶意生成类还是提供以下参数控制元空间
-XX:MaxMetaspaceSize:设置元空间最大值,默认位不限制,只受限于本地内存
-XX:MetaspaceSize:指定元空间初始空间大小,以字节为单位,触发该值会对类进行删除,并且收集器会对该值进行调整,在空间剩余大,可以适量减少,如果过少,可以适量增大,但不可以大于最大值
-XX:MinMetaspaceFreeRatio:在垃圾收集之后控制最小的元空间剩余容量的百分比,如果回收后空闲比小于该值,则说明空闲空间过多,一次GC收集回收太多类了,这些类生存时间太短所以没有引用GCRoots导致清理,需要更大的空间再回收,可减少因为元空间不足导致的垃圾收集的频率。
-XX:MaxMetaspaceFreeRatio:在垃圾收集之后控制最大的元空间剩余容量的百分比,如果回收后空闲比大于该值,则说明空闲空间过少,一次GC收集回收太少类了,由于空间太大使得类得到充分引用,进入“老年代”,可减少因为元空间过多导致的垃圾收集的频率,会释放空间

本机直接内存溢出

参数设定

默认和最大堆容量一样
-XX:MaxDirectMemorySize 指定直接内存容量大小

异常名称

OutOfMemoryError:具体根据后面描述来判断

另外,直接内存导致的溢出一个明显特征就是在Heap Dump中不会看见有什么明显的异常情况。如果内存溢出产生的Dump文件很小,而程序直接或间接使用了DirectMemory(典型:NIO),可以重点考虑

内存分配与回收

需要算法才能分配回收的区域

堆和方法区,因为它们不是线程私有的,线程私有的可以在编译器基本分配内存,在线程消亡时回收

引用计数算法

简单用法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器值为0的对象就是不可能再被使用

缺陷

很多例外情况需要考虑(比如对象相互引用)

可达性分析算法(Java正在用的)

简单用法

将GCRoots不可达的点回收

Java中可作为GCRoots的对象包括以下几种:
虚拟机栈局部变量表变量引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数,局部变量。因为局部变量随着方法结束而被回收,所以不用害怕长期占据内存
本地方法栈中Native方法中引用的对象
方法区中静态属性和常量引用的对象
所有被同步锁(synchronized)持有的对象
Java虚拟机内部的引用,如基本数据类型的Class对象,一些异常对象(如OutOfMemoryError),系统类加载器(Bootstrap,Extension,Application)
如果JVM进行局部回收,根据垃圾收集器以及当前回收区域不同,GCRoot有可能会把被其它区域引用的对象加入GCRoots
(以下未见过)
反映虚拟机内部情况的JMXBean,JVMTI注册的回调,本地代码缓存等

引用

传统定义

refrence类型数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存或对象的引用。
然而只有引用和未被分类不足以满足更多需求

更具体的四种分类

强引用:传统定义的引用,只要间接引用到GC Roots,就永远不会被回收,内存不够只会报错

值得一说的是,以下三种引用,实际上并不是通过声明变量指向对象,而是创建一个新的对象,对象中包含了引用对象的指针
举个栗子

WeakReference<Object> weakRef = new WeakReference<Object>(new Object());

public class WeakReference<T> extends Reference<T> {
        public WeakReference(T referent) {
        super(referent);
    }
}

public abstract class Reference<T> {
    private T referent;         /* Treated specially by GC */
    Reference(T referent) {
        this(referent, null);
    }
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}

软引用:如果只是普通的垃圾回收,不会回收SoftReference对象引用的对象;但如果内存将满,会将列进回收范围。如果这次回收后仍没足够内存才报错,用WeakReference类可实现

弱引用:无论内存是否满,只要发生垃圾回收,便回收,用WeakReference类可实现

虚引用:垃圾回收实际和弱引用相同,不同的是,虚引用对象调用get()直接返回null,也就是实际上是无法查看到虚引用所引用的对象。(关于Queue的源码太复杂了,直接来结论了)回收的对象会进入引用队列,为一个对象设置虚引用关联的目的只是为了能在这个对象被收集器回收时收到一个系统通知。

finalized()-生存与死亡间的缓刑

finalized能做的工作使用try-finally或其他可以做得更好,大家完全可以忘记这个方法

垃圾回收时发现对象GCRoots不可达标记一次,会检查对象是否重写finalize(),未重写直接回收,如果重写了,会检查虚拟机是否已调用过finalize(),已调用直接回收,如果未调用,则会使用finalize()将对象放入队列。

由一条虚拟机执行的线程去根据队列执行它们的finalize(),但是这并不意味着一定会执行完finalize(),会根据执行时间来决定是否放弃执行从而避免死循环。

如果在finalize()中可以和GC Roots引用对象进行引用,那么可以获得一次逃脱被回收的机会

方法区自己还有回收方法

方法区主要回收废弃常量和不再使用的类,
常量的回收可以参考堆的回收,如果不被引用,直接回收。

类的回收需要同时满足以下三个条件:
1 该类所有实例都被回收,堆中找不到类实例或任何继承类实例
2 加载该类的类加载器已被回收,这点最难满足
3 java.lang.Class对象没有被引用,避免反射等需要使用

但是着并不意味着类就一定会被回收,虚拟机还提供了其它参数来进行查看和控制回收开关(-Xnoclassgc)

理论和它衍生的几种算法的粗略分析

分代收集理论

大多数程序运行满足以下两个假说:
1 弱分代:绝大多数对象朝生夕灭
2 强分代:熬过越多次垃圾回收的对象越难消亡

因此,老年代的存在很有必要 ,熬过多次回收的对象很难被回收,对其进行垃圾回收性价比低,把它们集中起来放入老年代,而对新生代进行频繁回收,可以节约开销
但老年代可能存在着引用新生代的问题,清除了新生代会给依赖新生代的老年代造成困扰。

延伸假说3:跨代引用对于同代引用而言数量极少
因为对象的相互引用使得它们彼此存活,被老年代引用的新生代也会渐渐转为老年代而减少跨代数量

所以我们知道被老年代引用的新生代比例是不会越来越高的而且数量少,可以在新生代中指定一块数据结构(数据集),它把老年代内存分为几块,并把含有跨代引用的老年代所在内存块标记,在新生代回收属性中在这一块的对象无论是否引用,都会加入GC Roots,这样比整体扫描老年代确认开销小。

标记-清除算法及其弊端

一次性标记存活对象,再一次性清除未被标记对象;或标记清除对象,保留未被标记对象

弊端:
1 清除完空间碎片化,难以分配给大对象
2 大量对象时,标记和判断标记再清除的操作耗费大量时间

标记-复制算法

存在原因

用来解决标记清除在处理大量对象时的乏力

初生的算法-半区复制

将内存分为两半,标记保留对象,复制到另一半区域,直接清空当前该半区域
弊端:50%的可用内存

更新的算法-Appel式回收

默认状态下
按8:1:1分配Eden区和两块Survivor区,每次分配时将Eden和一块Survivor区的保留对象复制到另一块Survivor区。如果存在保留对象超过10%,启用“逃生门”(一般是把对象放入老年代)

标记-整理算法

存在原因

由于不想浪费空间,标记-复制算法往往采用担保制度,但这需要额外空间进行担保,老年代一般不采用这种方法

操作

标记保留对象,将其移动到左边界,清空边界以外数据

特点分析

老年代的对象极难回收,所以对于对象移动和更新引用位置的成本很大,而且对象移动必须全程暂停用户程序。但即便如此,比起直接回收内存,依靠内存层次来给零碎内存分配对象空间(如使用“分区空闲分配链表”),还是更加节约时间,因为内存的访问一直是用户程序最频繁的操作,如果再这上面增加流程,基本会直接“叠加”时间。

所以一般由更关注吞吐量(如Parallel Old)而不是时延(如CMS)的垃圾回收器所使用

两者求中的一个思路

在没有大到影响对象分配时,采用标记-清除算法。否则再采用标记-整理算法

HotsSpot具体算法实现

回收工具介绍

经典垃圾收集器

“经典”定义

指JDK7后JDK11发布前,HotSpot所包含的所有可用垃圾收集器,尽管不算先进,但是足够成熟稳定

Serial收集器的适用范围和弊端

简单高效的Serial消耗内存同时也是最小的,很适合只给虚拟机分配小内存的应用。但它是单线程工作,进行垃圾收集时必须停止其他一切线程。

ParNew收集器的适用范围和淘汰原因

ParNew实质上是Serial收集器的多线程并行版本,可以被使用的处理器核心较多时,ParNew更为实用
淘汰原因:替代,ParNew变得只能和CMS收集器配合,HotSpot历史上第一款退出历史舞台的收集器

Parallel Scanvenge收集器的适用范围和原理

Parallel Scanvenge收集器专注于稳定的吞吐率,更适合对于停顿时间趋于平衡的程序
吞吐量指(运行用户代码时间)/(运行用户代码时间+运行垃圾收集时间)
Parallel Scanvenge收集器通过两个参数精确控制吞吐量及一个参数来自动调节吞吐量和停顿时间,分别是控制最大垃圾收集停顿时间-XX:MaxHCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数,和开关参数-XX:UseAdaptiveSizePolicy

CMS收集器(JDK9后不建议使用)
适用范围

以最短回收停顿时间为目标,适合应用集中在互联网网站或者基于浏览器的B/S系统的服务端

工作流程

1 初始标记:标记GC Roots能直接关联到的对象
2 并发标记:遍历整个对象连接图,期间不停止用户线程
3 重新标记线程 为了修正并发标记期间,被用户进程改变标记的对象
4 并发清除阶段 清理删除掉标记阶段判断为死亡的对象

弊端和对应补救措施

1 对处理器资源敏感,
2
3 CMS基于标记-清除算法,收集结束后会产生大量碎片

G1收集器
适用范围

创作的目的是代替CMS,目前比CMS更适合于6G以上大内存应用,主要面向服务端应用

G1的目的和具体实现方法

目的:能够支持在一段时间内消耗在垃圾回收的时间大概在一个范围内
实现
1 Mixed GC模式,衡量标准不同于过往任何虚拟机,不再限制于分代,而是哪块内存垃圾多数量最多
2 基于Region的堆内存布局,不再固定大小和数量的分区,而是把对划分为大小相同的独立区,Region根据需要扮演新生代的Eden,Survivor,和老年代。Region通过参数-XX:G1HeapRegionSize设定,取值在1-32M,且应为2的N次幂
3 专门用来存储大对象的Humongous区域。容量大于1/2个Region即为大对象

弊端和对应补救措施

低延迟垃圾收集器

垃圾收集器的选择

类文件结构

Class文件结构

Class文件是什么

是一种字节码文件,一组已8个字节未基础单位的二进制流
不同语言所写的程序经过对应编译器会转换成.class文件,虚拟机可以直接执行字节码文件

结构初解

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
u2fieldsfields_count
u2methods_count1
u2methodsmethods_count
u2attributes_count1
u2atttributesattributes_count

魔数

确定是否是虚拟机可接受的Class文件,当值为0xCAFEBABE时,可被虚拟机接受
魔数也被用于各种扩展名区别相互

版本号

第5-6个字节是次版本号,7-8个字节是主版本号
Java版本从45开始,JDK版本从1.1开始,每增0.1可以编译1以内的Class文件,如JDK1.2可以支持版本号为45.0-46.65535的Class文件

常量池

值为0x16,即十进制的22,有21项常量,索引范围为1-22
对于Class文件结构而言只有常量池容量计数是从1开始的
0的意义是不指向任何一个常量池项目

访问标志

用于识别一些类或者接口层次的访问信息,例如指明是类还是接口,是否是abstract类型,是否是final型

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

类索引确定这个类的全限定名,父类索引确定父类的全限定名。
接口索引集合描述实现了哪些接口,这些被实现的接口顺序从implements关键字后顺序从左向右排列(如果这个Class文件表示的是一个接口,则应当是extends关键字)。
它们具体寻找流程如下:类索引的值会寻找常量池中记录该索引类全限定名的字段,即CONSTANT_Class_info这个字段值便是全限定名字符串值的真实位置,也位于常量池,即CONSTANT_Utf8_info

字段表

描述类变量和实例变量,但并不描述局部变量
结构

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

access_flags记录字段特性,有public,private,protected,static,final,volatile,transient,synthetic,enum
name_index记录方法的名称
descriptor_index记录字段的描述符,和记录方法的描述符一样,所以一贯讲了
参数会按顺序放在括号,后面跟上返回值
返回值和参数的描述方法是一样的,如果是数组,用[表示维度,引用类型用L+全限定名表示其余类型用首字母大写表示
如int indexOf(char[][] a,String b,int c)表示为([[CLjava/lang/StringI)I

字节码指令

字节码指令是什么

由一个字节长度的操作码即跟随其后的零至多个操作数构成的。由于虚拟机采用面向操作栈,所以大多数指令只包含操作码,操作数放置于操作栈中
该指令级限定长度为一个字节,又放弃了编译后操作数长度对齐,这意味着当遇到并没有规定在指令集所支持的数据类型时,无法简单地拆分成两块之后就装入操作数栈,而是需要通过一定的数据结构来表示承载,如一个16位无符号整数用两个无符号字节(byte1<<8|byte2) 次数可以用

字节码数据类型

如果每一种和数据结构相关的字节码都支持虚拟机所有运行时数据类型时,那么指令的数量恐怕会超过一字节表示的数量范围,因此不同操作往往只提供了有限的类型相关指令去支持它,有一些单独的指令在必要时候可以将一些不支持的类型转换成可支持的类型

指令类型

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈
load和aload都是用于将局部变量加载到操作栈,区别在于基本类型和引用类型

运算指令

大体上是对浮点数和整型运算

类型转换指令
对象创建与访问指令

类实例和数组的字节码实际上是不同的,因为它们的类型创建过程不同

操作数栈管理指令

包含出栈,复制压栈,换栈顶两数值

控制转移指令

通过条件分支,五条件分支,符合条件分支实现条件判断跳转

方法调用和返回指令
异常处理指令
同步指令

类加载机制

类加载机制起着什么作用

检查一个类的正确性,把它放入JVM内存中,并赋好初值

加载

通过全限定名,找到字节码中对应二进制流,转化到JVM内存元空间和堆的字符串常量池,并生成一个Class对象作为方便调用的我数据结构,Class对象有指向元空间和常量池对应位置的指针

验证

确保语义和语法正确

文件格式验证

验证字节流是否符合Class文件格式规范
如常量池常量是否又不被支持的常量类型,指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量

元数据验证

对字节码粗略描述信息进行语义解析
如,这个类是否有父类,父类是否不能继承(final),非抽象类是否实现所有接口和父类要求实现的方法

字节码验证

对类的方法体具体数据进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为

符号引用验证

对类自身以外的各类信息进行匹配行校验,查看是否缺少依赖的类等信息或依赖的信息禁止访问,为解析阶段符号引用转化为直接引用做好准备

准备

将在类中定义的静态变量分配内存并设置类变量初始值的阶段

解析

将常量池内的符号引用直接替换为直接引用,因为之前在字节码中,无法确定加载后的内存位置,所以只是用符号表示,这一步将会把符号转为数据的地址
需要注意的是,解析时如果找不到地址会尝试先加载符号对应的类,加载失败就会报错

类和接口解析
字段解析
方法解析

初始化

如果存在静态变量赋值和静态块语句,会生成<clinit>()作为合并所有静态变量赋值和静态块的方法
父类的<clinit>()一定在子类前面。
但这对接口并不适用,对于接口只有子接口需要父接口变量才会尝试调用父接口<clinit>(),对于类实现接口也是如此

类加载器

通过一个类的全限定名来获取描述该类的二进制字节流,以便让应用程序自己决定如何去获取所需的类。类由内容和和类加载器共同确立,两个完全相同的类,由不同的类加载器加载出来,仍然是不同的类。

双亲委派模型

将加载任务交给父加载器,父加载器不能完成的才让加载器完成。
类和加载器一起具备了优先级的层次关系,大程度上保证了类的相同度,如Object

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值