jvm学习(一)

1 篇文章 0 订阅

《深入理解Java虚拟机》学习笔记

Java内存区域

线程私有

1.程序计数器
线程执行Java方法时,记录的是正在执行的虚拟机字节码指令的地址;线程执行Native方法时,值为空(undefined)
唯一一个在Java虚拟机规范中没有规定任何OOM情况的区域

2.Java虚拟机栈
生命周期与线程相同。
描述了Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。一个方法从调用至执行完成的过程对应着一个栈帧在虚拟机栈中入栈到出栈的过程
异常状况:线程请求的栈深度大于jvm允许的深度,抛出StackOverflowError异常;若虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就抛出OOM异常

3.本地方法栈
与Java虚拟机栈的区别只在于本地方法栈为jvm使用到的Native方法服务。hotspot虚拟机把两者合二为一。

线程共享

1.Java堆
唯一的目的是存放内存实例,几乎所有对象实例都在这里分配内存(例外:栈上分配、标量替换)
Java堆是GC管理的主要区域,由于gc基本都采用分代收集算法,Java可以细分为Eden区、from survivor区、to survivor区等
线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(thread local allocation buffer, TLAB)
Java堆可以处在物理上不连续的内存空间,而逻辑上仍然是连续的,与磁盘空间类似。既可以是固定大小的,也可以是可扩展的(主流jvm都是可扩展的)
若堆中没有可用内存完成实例分配,且堆也无法扩展了,则抛出OOM异常

2.方法区
存储已被jvm加载的类信息(Class)、常量、静态变量、即时编译器编译后的代码等数据
hotspot把gc分代收集扩展至方法区,因此方法区又称永久代,省去了专门为方法区编写内存管理代码的工作,但这样更容易遇到OOM问题(String.intern()),jdk1.7的hotspot已经把永久代中的字符串常量池移出
方法区的内存回收主要针对常量池回收与类型卸载(条件比较严苛)
当方法区无法满足内存分配需求时抛出OOM异常

2.1运行时常量池
方法区的一部分。Class文件中除了类的版本、字段、方法、接口等信息外,还有常量池,用于存放编译期生成的各种字面量和符号引用(这些数据在类加载后进入方法区的运行时常量池)
运行时常量池具有动态性,可以在运行期间将新的常量放入池中(String.intern())

3.直接内存
不是jvm规范中定义的内存区域。
(jdk1.4新加入的NIO类,引入了基于channel和buffer的IO方式,它可以使用Native函数库直接分配堆外内存,并通过存储在Java堆中的DirectByteBuffer对象作为堆外内存的引用进行操作,通过避免在Java堆和Native堆来回复制数据提高了性能)

jvm对象

对象创建

1.类加载检查

2.内存分配
分配方式:
a.指针碰撞
Serial、ParNew等带Compact过程的收集器
b.空闲列表
CMS等基于Mark-Sweep算法的收集器

并发情况下创建对象的线程安全问题
a.对分配内存空间的操作进行同步处理(jvm实际用cas+失败重试保证更新操作的原子性)
b.把内存分配的动作按照线程划分在不同的空间之中,即为每个线程预先分配一小块内存(TLAB)。线程要分配内存时优先在TLAB上分配,TLAB用完并分配新的TLAB时才需要同步锁定

3.对象初始化,执行方法

对象内存布局
对象头实例数据对齐填充
对象访问定位

Java程序通过栈上的reference数据操作堆上的具体对象
主流访问对象的方式
1.通过句柄访问对象
Java堆中划分一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄包含了对象实例数据和类型数据格子的具体地址信息
2.通过直接指针访问对象
reference存储对象地址,对象的内存布局要考虑如何防止访问类型数据的相关信息

优缺点对比
使用句柄访问的好处是reference中存储的是稳定的句柄地址,对象被移动时只会改变句柄中的实例数据指针,reference本身不需要修改;
直接指针访问的好处是节省了一次指针定位的时间开销,速度更快(Hotspot的实现方式,由于对象的访问在Java中很频繁,这个优势会积少成多)

OOM异常情况

Java堆溢出
List<OOMObject> list = new ArrayList<OOMObject>();
while(true){
	list.add(new OOMObject);
}

分析手段

虚拟机栈和本地方法栈

jvm规范中描述了2种异常:
1.如果线程请求的栈深度大于jvm所允许的最大深度,将抛出StackOverflowError异常
2.如果jvm在扩展栈时无法申请到足够的内存空间,将抛出OOM异常
但单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候jvm都是抛出栈溢出异常

通过不断创建线程的方式可以产生OOM异常

private void dontStop() {
	while(true){}
}
...
while(true){
	new Thread(()->{
    	@Override
        public void run(){
        	dontStop();
        }
    }.start();
}
方法区和运行时常量池溢出

运行时常量池导致的OOM

int i = 0;
while(true){
	list.add(String.valueOf(i++).intern());
}

借助CGLib使方法区出现OOM

while(true){
    Enhancer enhancer=new Enhancer();
    enhancer.setSuperclass(OOMObject.class);
    enhancer.setUseCache(false);
    enhancer.setCallback(new MethodInterceptor(){
        public Object intercept(Object obj,Method method,Object[]args,MethodProxy proxy)throws Throwable{
            return proxy.invokeSuper(obj,args);
        }
    });
    enhancer.create();
}
本机直接内存溢出
int _1MB = 1024*1024;
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true){
    unsafe.allocateMemory(_1MB);
}

垃圾回收器与内存分配策略

对象存活判断

引用计数法

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

缺点:很难解决对象之间相互循环引用的问题

可达性分析算法

通过一系列称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的

Java中可作为GC Roots的对象
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般所说的Native方法)引用的对象

引用

jdk1.2之前,Java引用的定义:如果reference类型变量存储的数值代表另一块内存的起始地址,就称其为那块内存的引用。这种定义下一个对象只有被引用或者没有被引用两种状态,无法描述那种食之无味弃之可惜的对象。

jdk1.2后Java对引用概念进行了扩充,目的是为了描述这样一类对象:当内存空间充足时,能保留在内存;如果内存空间在经过gc后还是非常紧张,则可以抛弃这些对象

1.强引用
类似

Object obj = new Object()

这类引用,只要强引用还存在,gc时永远不会回收掉被引用的对象

2.软引用 SoftReference类
描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收

3.弱引用 WeakReference类
也是用来描述非必需对象,但其强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次gc发生之前。gc时无论当前内存是否足够,都会回收掉制备弱引用关联的对象

4.虚引用(幽灵引用、幻影引用) PhantomReference类
最弱的一种引用关系,一个对象是否有虚引用与之关联,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。为对象设置虚引用关联的唯一目的是能在这个对象被gc回收时收到一个系统通知

finalize方法

即使在可达性分析算法中不可达的对象,至少要经过两次标记过程才能真正被宣告死亡:
1.如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并进行一次筛选,如果对象覆盖了finalize方法且finalize方法还没被调用过,那么这个对象将会被放入一个叫F-Queue的队列中,并在稍后由一个jvm自动建立、低优先级的Finalizer线程去执行它(执行是指jvm会触发这个方法,但不承诺会等待它运行结束,原因是防止某对象的finalize方法执行缓慢或者发生死循环而导致F-Queue队列其他对象永久处于等待)
2.如果对象的finalize方法没被覆盖或者对象已经第二次被标记了,则再次gc时对象将被回收,没有自救机会

回收方法区

永久代的gc主要回收两部分内容:废弃常量和无用的类

判断一个类是否是"无用的类"需要同时满足3个条件:
a.该类所有实例都已经被回收,即Java堆中不存在该类的任何实例
b.加载该类的classloader已经被回收
c.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

jvm可以对满足上述条件的无用类进行回收,但具体是否真要回收,还要看jvm设置的can’sh

在大量使用反射、动态代理、cglib等字节码框架、动态生成jsp以及osgi这类频繁自定义classloader的场景都需要jvm具备类卸载的功能,以保证永久代不发生溢出

gc算法

mark-sweep

“标记-清除"算法是最基础的gc算法,分为"标记"和"清除”:先标记出所有需要回收的对象,标记完成后统一回收。

不足之处:
1.效率问题,两个过程的效率都不高
2.空间问题,标记清除后会产生大量不连续的内存碎片,可能之后要分配大块内存给大对象时无法找到足够的连续内存而不得不提前触发另一次gc

copying

复制算法,将可用内存按容量等量分为两块,每次只用其中一块,用完一块时就将仍存活的对象复制到另一块上,再把之前那块内存空间一次清理
优势:实现简单,运行高效
缺点:代价高,可用内存变为原本的一半

IBM研究后发现新生代中98%的对象朝生夕死,因此并不需要按1:1划分内存空间。而是将内存划分为1块较大的Eden空间和2块较小的Survivor空间,每次使用Eden和其中一块Survivor,回收时,将存活的对象一次性复制到剩下那块Survivor,最后清掉Eden和之前使用的Survivor。Hotspot默认Eden:Survivor=8:1。可用内存空间由之前的0.5变为0.9
万一有内存大小超过10%的对象存活下来了,Survivor空间不够用,需要依赖其他内存(指老年代)进行分配担保(Handle Promotion)

mark-compact

标记整理算法
当对象存活率较高时复制算法要进行较多的复制操作,效率变低。且需要有额外空间进行分配担保以应对被使用内存中所有对象都存活的极端情况,复制算法不适用于老年代。

标记过程结束后,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

generational-collection

分代收集
根据对象存活周期的不同将内存划分为几块。一般是将Java堆划分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。
新生代对象存活率低,选用复制算法;老年代对象存活率高,采用"标记清理"或"标记整理"算法

hotspot的算法实现

枚举根节点

枚举根节点时有两个难点:
1.GC Roots存在于方法区的常量引用、类静态属性、执行上下文(虚拟机栈、本地方法栈中栈帧的本地变量表)中。很多应用仅方法区就有数百兆,如果逐个检查所有引用,会消耗很多时间
2.枚举根节点时要暂停所有Java执行线程,保证可达性分析工作在再可确保一致性的快照中进行(不可用出现分析过程中对象引用关系还在不断变化的情况)

解决方案:
使用一组称为OopMap的数据结构存放对象引用所在的位置

安全点

有了OopMap虚拟机可以快速准确地完成GC Rotts枚举,但可能导致引用关系变化的指令非常多,为每条指令都生成对应OopMap也不现实。
因此Hotspot只是在特定位置(安全点)记录了这些信息,程序也只有执行到安全点时才能暂停。
安全点选定标准:是否具有让程序长时间执行的特征,最明显的就是指令序列复用,如方法调用、循环跳转、异常跳转等
jvm相关博客

如何在gc发生时让所有线程(不包括执行JNI调用的线程)都跑到最近的安全点上再停顿下来?
1.抢先式中断
gc发生时,先把所有线程都中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点。
2.主动式中断
gc需要中断线程时,设置一个标志,让各个线程执行时主动去轮询这个标志,发现中断标志为true时就自己中断挂起。轮询标志的地方与安全点重合,另外再加上创建对象需要分配内存的地方

安全区域

安全点未能完全解决如何进入gc的问题,即线程"不执行"的时候(sleep或blocked状态),线程无法响应jvm的中断请求,走到安全的地方去中断挂起,让jvm等待线程获得cpu时间也不现实。

安全区域是指在一段代码片段之中,引用关系不发生变化。在这区域中的任意地方开始gc都是安全的

线程执行到安全区域的代码时就首先标识自己已经进入了安全区域。这段时间里jvm要gc时就不用管标识为safe region的线程。线程即将离开safe region时,要检查系统是否已完成根节点枚举,若完成了,就接着执行;否则,必须等待直到收到可以安全离开safe region的信号为止

HotSpot垃圾收集器

新生代收集器
Serial收集器

单线程的收集器,且gc时必须暂停其他所有工作线程

对于运行在Client模式下的虚拟机来说是个很好的选择

ParNew收集器

其实是Serial收集器的多线程版本

是许多运行在Server模式下的jvm中首选的新生代收集器,一个重要原因是只有它能与CMS(concurrent mark sweep)收集器配合工作

Parallel Scavenge收集器

新生代、复制算法、多线程

关注点:达到一个可控的吞吐量

gc自适应调节策略

老年代收集器
Serial Old收集器

Serial收集器的老年代版本,单线程,使用"标记-整理"算法,适用于Client模式的JVM

Parallel Old收集器

Parallel Scavenge的老年代版本,多线程,“标记-整理”,吞吐量优先

CMS收集器

Concurrent Mark Sweep
目标是获得最短回收停顿时间
基于"标记-清除"算法
运作过程:
1.初始标记(停顿,只标记根节点直接关联的对象,耗时短)
2.并发标记(根节点枚举过程,与用户线程并发执行)
3.重新标记(需停顿,修正并发标记期间因用户线程继续运行而导致标记发生变动的那一部分对象的标记记录)
4.并发清除(与用户线程并发执行)

耗时最长的是并发标记和并发清除,由于这俩过程都可以跟用户线程并发执行,所以总体来说CMS收集器的内存回收过程是与用户线程一起并发执行的

缺点:
1.对cpu资源非常敏感
2.无法处理浮动垃圾,可能出现"Concurrent Mode Failure"而导致另一次full gc的产生
3.基于标记清除算法实现,会产生大量内存碎片

G1

Garbage-First
面向服务端应用

特点:
1.并行与并发(能充分利用多cpu、多核环境下的硬件优势,缩短停顿时间)
2.分代收集(可以不需要其他收集器配合就能独立管理整个gc堆)
3.空间整合(从整体看是基于标记整理算法实现的,但局部上看是基于复制算法实现,都不会产生内存碎片)
4.可预测的停顿(建立了可预测的停顿时间模型,能让使用者明确指定在一个长度为m毫秒的时间片内,消耗在gc上的时间不得超过n毫秒)

G1收集器将整个Java堆划分为多个大小相等的region,新生代、老年代仅是逻辑概念,物理上不再隔离,它们都是一部分region(不需要连续)的集合

G1可以有计划地避免在整个Java堆中进行全区域的gc。G1跟踪每个region里垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个有序表,每次根据允许的收集时间优先回收价值最大的region

难点
region不是孤立的,某个region上的对象并非只能被本region中的其他对象引用。

难道要扫描整个堆吗?
通过Remembered Set记录region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,以避免全堆扫描

每个region都有一个与之对应的Remembered Set,jvm发现程序在对引用类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的region之中,是的话通过CardTable把相关引用信息记录到被引用对象所属region的Remembered Set之中。进行gc时把Remembered Set也纳入根节点枚举范围内,可保证不进行全堆扫描也无遗漏

G1收集大致步骤
1.初始标记(单线程,停顿,标记一下根节点直接关联到的对象,并修改TAMS值(Next Top at Mark Start),让下一阶段用户程序并发运行时能在正确的region中创建对象)
2.并发标记
3.最终标记(多线程,停顿,修正信息存在Remembered Set Logs里,要把修正信息整合到Remembered Set中)
4.筛选回收(根据用户期望停顿时间制定回收计划)

内存分配与回收策略

对象优先在Eden区分配

通常情况下对象在Eden区分配,如果Eden区没有足够空间进行分配,jvm将发起一次Minor GC

新生代GC(Minor GC):指发生在新生代的gc,因为Java大多数对象朝生夕死,因此Minor GC很频繁,速度也快
老年代GC(Major GC/Full GC):指发生在老年代的GC,Major GC通常伴随着至少一次的Minor GC。Major GC一般比Minor GC慢10倍以上

大对象直接进入老年代

Serial和ParNew收集器提供了
-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配

长期存活的对象将进入老年代

jvm给每个对象定义了一个年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍存活,并且能被Survivor区容纳,将被移动到Survivor区,且年龄设为1.对象每在Survivor区熬过一次Minor GC,年龄加1,当年龄增加到一定程度时(默认15),将会晋升到老年代
晋升老年代的域值由
-XX:MaxTenuringThreshold设置

动态对象年龄判定

jvm并非总是要求对象年龄达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代

空间分配担保

在gc之前,jvm先检查老年代最大可用连续空间是否在于新生代所有对象总空间或者历次晋升对象的平均大小,若大于则进行MinorGC,否则进行FullGC

类文件结构

无关性的基石

平台无关性:jvm提供商发布了许多可以运行在各种不同平台上的jvm,这些jvm都可以载入和执行同一种平台无关的字节码,实现程序的一次编写到处运行

语言无关性:仍是以jvm和字节码存储格式为基础。jvm不与包括Java在内的任何语言绑定,只与Class文件这种特定二进制文件格式所关联,各种语言通过各自的编译器将代码编译成字节码,然后在jvm上运行

Class类文件结构

魔数与Class文件的版本

Class文件的头四个字节
0xCAFEBABE
第5~6的字节:次版本号
第7~8的字节:主版本号

高版本的jdk能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件

常量池

紧接着主次版本号之后的是常量池入口

常量池入口放置一项u2类型的数据,标识常量池容量计数器,计数器从1开始计数
将第0项常量空出的目的在于满足后面某些指向常量池的索引值的数据在特定的情况下需要表达"不引用任何一个常量池项目"的含义,这时就可以把索引值置为0

只有常量池容量计数是从1开始的,其他集合类型都是从0开始

常量池主要存放两大类常量:

  1. 字面量
  • 文本字符串
  • 声明为final的常量值等
  1. 符号引用
  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中的每一项都是一个表,到jdk1.7时一共有14种表,共同特点是表开始的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型

访问标志

常量池结束之后紧接着的两个字节代表访问标志,用于识别一些类或者接口层次的访问信息,包括

  • 这个Class是类还是接口
  • 是否定义为public
  • 是否定义为abstract
  • 如果是类的话是否被声明为final
  • 等待
类索引、父类索引与接口索引集合

类索引this_class,父类索引super_class都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件由这三项数据来确定这个类的继承关系

  • 类索引:确定类的全限定名
  • 父类索引:确定这个类的父类的全限定名
  • 接口索引集合:描述这个类实现了哪些接口,被实现的接口按implements语句后的顺序从左到右排列在接口索引集合中

类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

接口索引集合入口的第一项是u2类型的接口计数器,表示索引表的容量

字段表集合

用于描述接口或者类中声明的变量

依次包括访问标识access_flags,名称索引name_index,描述符索引descriptor_index,属性表集合attributes

字段包括类级变量(static)以及实例级变量,不包括在方法内部声明的局部变量

字段的修饰符(如字段作用域、static修饰符、可变性(final)、并发可见性volatile、可否被序列化transient等)都是Boolean,适合用标志位表示

字段的名称,数据类型无法固定,只用引用常量池中的常量来描述

方法表集合

同字段表集合类似,依次包括访问标识access_flags,名称索引name_index,描述符索引descriptor_index,属性表集合attributes

方法里的Java代码经编译器编译成字节码指令后存放在方法属性表集合中一个名为Code的属性里

如果父类方法没有被子类重写,方法表集合中不会出现来自父类的方法信息

属性表集合

Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息

对于每个属性,其名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构完全自定义,只需要通过一个u4的长度属性去说明属性值所占位数

  1. Code属性
    程序方法体中的代码经javac编译器处理后变为字节码指令存储在Code属性内,存储于方法表的属性集合之中
    接口和抽象类的方法不存在Code属性
  2. Exceptions属性
    列举出方法中可能抛出的受查异常,即throws关键字后面列举的异常
  3. LovalVariableTable
    描述栈帧中局部变量中的变量与Java源码中定义的变量之间的关系
  4. ConstantValue
    通知jvm自动为静态变量赋值
    非static类型的变量(实例变量)的赋值在实例构造器方法中进行;
    类变量有两种选择:如果同时用static和final修饰一个变量,且这个变量的类型是基本类型或者java.lang.String,就生成ConstantValue属性来进行初始化;如果变量没有被final修饰,或者并非基本类型或String,则会在类构造器 方法中进行初始化
  5. InnerClasses
    记录内部类与宿主类之间的关联
  6. StackMapTable

字节码指令

字节码与数据类型
加载和存储指令
运算指令
类型转换指令
对象创建于访问指令
操作数栈管理指令
控制转移指令
方法调用和返回指令
异常处理指令

通过异常表实现

同步指令

使用管程Monitor支持同步
jvm可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。方法调用时,调用指令检查方法的ACC_SYNCHRONIZED访问标志是否被设置了,如果是,执行线程必须先成功持有管程,然后才能执行方法,当方法完成后(即使是非正常退出)释放管程

jvm的指令集中有monitorenter和monitorexit两条指令来支持Java的synchronized关键字的语义,正确实现synchronized关键字需要javac编译器与jvm两者共同协作支持

为保证monitorenter和monitorexit指令在出现异常时仍可以配对执行,编译器会自动产生一个异常处理器,这个异常处理声明可处理所有异常,目的就是为了执行monitorexit指令

虚拟机类加载机制

类加载的时机

类的整个生命周期包括:加载,验证,准备,解析,初始化,使用和卸载

加载,验证,准备,初始化和卸载必须按顺序开始(只是按顺序开始,通常会交叉混合进行)

类的初始化时机

  1. 遇到new,getstatic,pustatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化
  2. 使用java.lang.reflect包的方法对类进行反射调用时
  3. 初始化一个类时,如果其父类还没初始化,先初始化其父类
  4. 当vm启动时,用户需要指定一个要执行的主类(包含main方法的类),vm会先初始化这个主类
  5. 使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个方法句柄对应的类没有初始化,则需要先触发其初始化

上述5种场景中的行为称为对一个类进行主动引用

接口的初始化场景
第3点与类有区别:一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在使用到父接口的时候(如引用接口中定义的常量)才会初始化

类加载的过程

加载

加载阶段要完成3件事

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的方法入口
验证
  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证
准备

正式为类变量分配内存并设置类变量初始值的阶段
类变量初始化为零值;字段属性表中存在ConstantValue属性的类字段初始化为ConstantValue属性所指定的值

解析

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

两个概念

  1. 符合引用
    以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能无歧义地定位到目标即可。符号引用与虚拟机内存布局无关,引用的目标并不一定已经加载到内存中。各虚拟机能接收的符号引用都一致
  2. 直接引用
    可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用与jvm内存布局相关,若有了直接引用,引用的目标肯定已经存在于内存中
  3. 类或接口的解析
  4. 字段解析
  5. 类方法解析
  6. 接口方法解析
初始化

初始化阶段是执行类构造器方法的过程

  1. 方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,收集顺序由语句在源文件中出现的顺序决定,静态语句块中只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,在前面的静态语句块里可以赋值,但不能访问
static{
    i = 0; //可以编译通过
    System.out.println(i); //编译器会提示"非法前向引用"
}
static int i = 1;
  1. 方法与实例构造器方法不同,它不需要显式调用父类构造器,jvm会保证在子类的方法执行之前,父类的方法已经执行完毕。所有jvm中第一个被执行的方法的类一定是java.lang.Object
  2. 因为父类的方法先执行,意味着父类中定义的静态语句块优于子类的变量赋值操作
  3. 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成方法
  4. 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,故接口也会像类一样生成方法。但接口的方法不需要先执行父接口的方法,只有在使用父接口中定义的变量时父接口才会初始化
  5. jvm保证一个类的方法在多线程环境下被正确加锁、同步,多线程并发去初始化一个类时,又会有一个线程成功执行这个类的方法,其他线程都需要阻塞等待。活动线程执行方法完毕后,其他线程不会再次进入方法

类加载器

类与类加载器

对于任意一个类,都需要由加载它的类加载器和类本身一同确定其在jvm中的唯一性
每一个类加载器都有一个独立的类名称空间

双亲委派模型

类加载器分类
jvm的角度

  1. bootstrap classloader启动类加载器,C++实现,虚拟机自身的一部分
  2. 所有其他类加载器,Java实现,独立于虚拟机,全都继承了抽象类java.lang.ClassLoader

更细的划分

  1. 启动类加载器(Bootstrap Loader)
  2. 扩展类加载器(Extension ClassLoader)
  3. 应用程序类加载器(Application ClassLoader)

双亲委派模型使用组合关系来复用父加载器的代码

工作过程:如果一个类加载器收到类加载请求,它首先将请求委派给父类加载器去完成,每一层次的类加载器都如此,因此所有的加载请求都会到达启动类加载器中,只有当父类加载器无法完成加载请求时,当前类加载器才尝试自己加载

java.lang.Object存放在rt.jar中,无论哪个类加载器去加载它,最终都会委派给启动类加载器去加载,保证了Object类在程序的各种类加载器环境中都是同一个类

启动类加载器

负责将存放在<JAVA_HOME>\lib目录下的,或者被-Xbootclasspath参数所指定的路径中的,并且是jvm识别的类库加载到jvm内存中。启动类加载器无法被Java程序直接引用,用户自定义类加载器时,用null代替,表示把加载请求委托给启动类加载器

扩展类加载器

由sun.misc.Launcher$EXTClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定的路径中的所有类库。开发者可以直接使用扩展类加载器

应用程序类加载器

由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,故一般也称它为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用这个类加载器,一般情况下为系统默认类加载器

破坏双亲委派模型

虚拟机字节码执行引擎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值