Java虚拟机
时间:2023-07-15 02:21:01 | 来源:网站运营
时间:2023-07-15 02:21:01 来源:网站运营
Java虚拟机:参考《深入理解Java虚拟机》
java虚拟机的无关性
java虚拟机的无关性1、JVM的运行时数据区域
1.1 程序计数器(Program Counter Register)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个时刻,一个处理器(对于一个多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果线程正在执行的是Native方法,这个计数器值为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
1.2 Java虚拟机栈(Java Virtual Machine Stacks)
线程私有,生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个帧栈(Stack Frame)用于存储局部变量表、操作数栈、动态链表、方法出口等信息。
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在Java虚拟机规范中,对这个区域规定了两种异常:
1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2)如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError的异常。
1.3 本地方法栈(Native Method Stack)
为虚拟机使用到的Native方法服务。
(Sun HotSpot虚拟机直接把方法栈和虚拟机栈合二为一)
会抛出StackOverflowError异常和OutOfMemoryError的异常。
1.4 Java堆(Java Heap)
1)对于大多数应用来说,是Java虚拟机所管理的内存中最大的一块。
2)是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例,几乎所有的对象实例都在这里分配内存。
在Java虚拟机规范中:所有的对象实例以及数组都要在堆上分配。
【随着JIT编译期的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不那么“绝对”了】
3)Java堆是垃圾收集器管理的主要区域,又被称为“GC堆”(Garbage Collected Heap)。
4)Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可。实现上既可以是固定大小,也可以是可扩展的。不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果堆中没有内存完成实例分配,并且堆也无法再扩展,将会抛出OutOfMemoryError异常。
1.5 方法区(Method Area)
别名:Non-Heap,目的与Java堆区分
是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。不需要连续内存和可以选择固定大小或者可扩展,还可以选择不实现垃圾收集。这个区域的内存回收主要针对常量池的回收和对类型的卸载。
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
1.6 运行时常量池(Runtime Constant Pool)
方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,比较常用的是String类的intern()方法。
当常量池无法再申请到内存时,会抛出OutOfMemoryError异常。
1.7直接内存(Direct Memory)
不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但这部分内存被频繁使用。而且也可能导致OutOfMemoryError异常。
本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。
注意:服务器管理员在分配虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。
2、HotSpot虚拟机对象
2.1、对象的创建
1)类加载检查
2)为新生对象分配内存
(1)分配方式:指针碰撞(java堆中的内存是规整的)
(2)分配方式:空闲列表(内存不规整)
【选择哪种分配方式由java堆是否规整决定,java堆内存是否规整由所采用的垃圾收集器是否带有压缩整理功能决定】
解决方案:
(1)对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
(2)TLAB本地线程分配缓冲:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定。
3)需要将分配到的内存空间都初始化为零值(不包括对象头)
4)对对象进行必要的设置——对象头
2.2、对象的内存布局
1)对象头
(1)用于存储对象自身的运行时数据;Mark Word
Mark Word被设计成一个非固定的数据结构,会根据对象的状态复用自己的存储空间
(2)类型指针
即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果是数组,则必须有一块记录数组长度的数据,因为java虚拟机可以通过普通java对象的元数据信息确定java对象的大小。
2)实例数据
对象真正存储的有效信息
存储顺序:虚拟机分配策略参数和字段在java源码中定义的顺序的影响
HotSpot默认分配策略:longs/doubles、ints、shorts/chars、bytes、booleans、oops
如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙中。
3)对齐填充
不必然存在,也没有特别含义,起占位符作用
HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。
2.3、对象的访问定位
主流的访问方式:
1)句柄
优点:reference存储的是稳定的句柄地址,在对象被移动时,只改变句柄中的实例数据指针,而reference本身不用改变
2)直接指针(Sun HotSpot主要使用这种)
优点:速度快,节省了一次指针定位的时间开销
2.4实战:OutOfMemoryError异常
内存映像分析工具:内存泄露(Memory Leak) or 内存溢出(Memory Overflow)
使用eclipse memory analyzer工具
1、官网下载,解压缩后直接打开即可使用。
http://www.eclipse.org/downloads/download.php?file=/mat/1.8.1/rcp/MemoryAnalyzer-1.8.1.20180910-win32.win32.x86_64.zip 1)java堆溢出package testJVM;import java.util.ArrayList;import java.util.List;/** * VM Args: -verbose:gc -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * @author laibn * */public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } }}
输出:
分析:
如果是内存泄露,可进一步通过工具查看泄露对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄露对象的类型信息及GC Roots引用链的信息,就可以比较准确地定位出泄露代码的位置。
如果不存在泄露,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
2)虚拟机栈和本地方法栈溢出由于HotSpot虚拟机中不区分虚拟机栈和本地方法栈,因此,对于Hotspot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
(1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
(2)如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
package testJVM;/** * VM Args: -Xss128k * @author laibn * */public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable{ JavaVMStackSOF oom = new JavaVMStackSOF(); try{ oom.stackLeak(); }catch(Throwable e){ System.out.println("stack length:"+oom.stackLength); throw e; } }}
在单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
如果测试时不限于单线程,通过不断地建立线程的方式倒是可以产生内存溢出异常。
操作系统分配给每个进程的内存是有限制的,譬如32位windows限制为2GB,虚拟机提供了参数来控制Java堆和方法区的这两部分内存的最大值。剩余的内存为2GB(操作系统限制)减去Xmx(最大堆容量),再减去MaxPermSize(最大方法区容量),程序计数器消耗内存很小,可以忽略掉。如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机栈和本地方法栈“瓜分”了。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
package testJVM;/** * VM Args: -Xss2M(我的机子跑不动,会死掉,要注意) * @author laibn * */public class JavaVMStackOOM { private void dontStop(){ while(true){ } } public void stackLeakByThread(){ while(true){ Thread thread = new Thread(new Runnable(){ public void run(){ dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); }}
3)方法区和运行时常量池溢出package testJVM;import java.util.ArrayList;import java.util.List;/** * -verbose:gc -XX:PermSize=10M -XX:MaxPermSize=10M * @author laibn * */public class RuntimeConstantPoolOOM { public static void main(String[] args) { /*List<String> list = new ArrayList<String>(); int i = 0; while(true){ list.add(String.valueOf(i++).intern()); }*/ String str1 = new StringBuilder("计算机").append("软件").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2); }}
结果:(JDK1.7)
true
false
原因:JDK1.7中的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”原则,而“计算机软件”是首次出现,因此返回true。
String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
package testJVM;import java.lang.reflect.Method;import net.sf.cglib.proxy.Enhancer;import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy;/** * -XX:PermSize=10M -XX:MaxPermSize=10M * 引入cglib-2.2.2.jar和asm-3.3.1.jar * @author laibn * */public class JavaMethodAreaOOM { public static void main(String[] args) { 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{ try{ return proxy.invokeSuper(obj, args); }catch(Throwable e){ e.printStackTrace(); throw e; } } }); enhancer.create(); } } static class OOMObject{ }}
码字时遇到的问题:
java.lang.NoClassDefFoundError: org/objectweb/asm/Type异常
需要asm的jar包
java.lang.IncompatibleClassChangeError: class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class
CGLib的jar包版本问题,需要将CGLib包改为2.2.2,添加对asm的引用。
我的eclipse只返回
方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收状况。这类场景除了上面提到的程序使用了CGLib字节码增强和动态语言外,常见的还有:大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于OSGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。
4)本机直接内存溢出3、垃圾搜集算法
3.1、标记-清除
1)标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:效率问题,标记和清除两个过程都效率不高;空间问题,会产生大量不连续的内存碎片,碎片太多会导致以后在程序运行过程中需要分配大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。
3.2、复制算法
将内存按容量划分为大小相等的两块,每次只使用其中的一块,当这块用完了,将还存活的对象复制到另一块上面,然后把已使用过的内存空间一次清理掉。
不足:将内存缩小为原来的一半
HotSpot虚拟机:Eden 8: Survivor:1 : Survivor:1,回收时,将Eden和Surviver还存活的对象一次性复制到另一块Servivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
3.3、标记-整理
标记所有需要回收的对象,在标记完成后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
3.4、分代算法
根据对象存活周期不同,将内存划分为几块。一般把java堆分为新生代和老年代。
新生代:少量存活->复制算法;
老年代:存活率高->标记清理或者标记整理算法
4、Class类文件的结构
Class文件是以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件中,中间没有添加任何分隔符。
不完整class截图4..1 数据类型:无符号数和表
无符号数:基本数据类型,以u1、u2、u4、u8表示1个字节、2个字节、4个字节、8个字节的无符号数,可用以描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表:由多个无符号数或者其它表构成的复合数据类型,所有表以"_info"结尾。用以描述有层次关系的复合结构的数据,整个Class文件本质上是一张表。
4.2 魔数
魔数:Class文件的前4个字节,唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
Class文件魔数值:0xCAFEBABE
魔数后的四个字节为Class文件的版本号:5、6个字节为次版本号,7、8个字节为主版本号
4.3常量池
4.4 访问标志
两个字节,位于常量池之后,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类,是否被声明为final等。
4.5 类索引、父类索引和接口索引结合
4.6 字段表集合
4.7方法表集合
4.8属性表集合
5、字节码指令
java虚拟机的指令由
一个字节(0~255)长度的、代表着某种特定操作含义的数字(操作码)以及紧随其后的零至多个代表此操作所需参数(操作数)而构成。
Java虚拟机采用面向操作数栈的架构,大部分指令不包括操作数,只有一个操作码
Java虚拟机的解释器最基本的执行模型:
do { 自动计算PC寄存器的值加1; 根据PC寄存器的指示位置,从字节码流中取出操作码; if(字节码存在操作数) 从字节码流中取出操作数; 执行操作码所定义的操作;} while(字节码流长度 > 0)
5.1 操作码助记符:
i : int 类型的数据操作;
l:long;s:short;b: byte;c:char;f: float;d: double;a: reference;
5.2 加载与存储指令:
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,包括:
将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
将一个数值从操作数栈存储到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
扩充局部变量表的访问索引的指令:wide
5.3 运算指令
用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
1)对整型数据进行运算的指令
2)对浮点型数据进行运算的指令
无论哪种算术指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char、boolean类型的算术指令,对这类数据的运算,应使用操作int类型的指令代替。整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为表现。
加:iadd、ladd、fadd、dadd
减:isub、lsub、fsub、dsub
乘:imul、lmul、fmul、dmul
除:idiv、ldiv、fdiv、ddiv
求余:irem、lrem、frem、drem
取反:ineg、lneg、fneg、dneg
位移:ishl、ishr、iushr、lshl、lshr、lushr
按位或指令:ior、lor
按位与指令:iand、land
按位异或指令:ixor、lxor
局部变量自增指令:iinc
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
5.4 类型转换指令
5.5 对象创建与访问指令
5.6 操作数栈管理指令
5.7 控制转移指令
5.8 方法调用和返回指令
5.9 异常处理指令
5.10 同步指令
6、虚拟机类加载机制
虚拟机的类加载机制:虚拟机把类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
6.1类加载时机
类的生命周期类的加载过程中,加载、验证、准备、初始化和卸载这5个阶段顺序是确定的,必须按照此顺序按部就班地
开始(并非“进行”或“完成”,通常互相交叉混合进行,通常会在一个阶段执行的过程中调用、激活另一阶段),而解析在某些情况下,可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定或晚期绑定)。
虚拟机规范中规定,有且只有 5 种情况必须立即对类进行初始化:【对类的主动引用】
1)遇到new 、getstatic、putstatic、invokestatic这4条字节码指令时,若类没有进行过初始化,则需要先触发其初始化。场景比如:
- 使用new 关键字实例化对象
- 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)
- 调用一个类的静态方法的时候
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
6.2 类加载过程
1)加载
虚拟机操作:
- 将一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 将内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
读取二进制字节流的渠道:
- 从Zip包中读取,日后JAR、EAR、WAR格式的基础
- 从网络中获取,Applet
- 运行时计算生成,动态代理技术,java.lang.reflect.Proxy中,用了ProxyGenerator.generateProxyClass来为特定接口生成形式“*$Proxy”的代理类的二进制字节流。
- 由其他文件生成,JSP
- 从数据库中读取,比如某些中间件服务器(SAP Netweaver)
2)验证
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
非常重要但非必要,可使用 -Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
i. 文件格式验证
第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
该阶段目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。该阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面3个阶段验证全部是基于方法区的存储结构进行的,不会直接操作字节流。
比如:
- 是否以魔数0xCAFEBABE开头;
- 主次版本号是否在当前虚拟机处理范围内;
- 常量池常量中是否有不被支持的常量类型(检查tag);
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量;
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据;
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息;
ii. 元数据验证
第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
该阶段主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息
比如:
- 这个类是否有父类(除java.lang.Object之外,所有的类都应当有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(覆盖了父类的final字段?出现不符合规则的方法重载?方法参数都一致但返回类型却不同?)
iii. 字节码验证
第三个阶段主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。主要对类的方法体进行校验分析,保证被校验类在运行时不会做出危害虚拟机安全的事件
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(如操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量中)
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的,如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无关系的一个数据类型,则是危险和不合法的。
iv. 符号引用验证
第四个阶段的校验主要发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作在“解析”阶段中发生。符号引用验证可看作是对类自身以外(常量池中的各种符合引用)的信息进行匹配性校验。
该阶段的目的是确保解析动作能正常执行,若无法通过验证则抛出“java.lang.IncompatibleClassChangeError”异常的子类,如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
如:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
3)准备
正式为类变量分配内存并设置类变量初始值的阶段,所使用的内存都在方法区中分配。
仅包括类变量(被static修饰的变量)【实例变量会在类对象实例化时随着对象一起分配在java堆中】
“初始值“通常情况下指数据类型的零值【final修饰的常量ConstantValue除外】
基本数据类型的零值4)解析
虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用的字面量定义在Java虚拟机规范的Class文件格式中。与虚拟机的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。与虚拟机内存布局有关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定是已经存在于内存中。
虚拟机规范要求在执行 new、anewarray、multianewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、lcd_w、putfield、putstatic这16个操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。
除了invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存,避免重复解析。
invokedynamic,必须等到程序实际运行到这条指令时,解析动作才能进行。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应于常量池:CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info。
- 类或接口的解析:若不是数组类型,则会把全限定名传递给类加载器加载,会触发父类的加载;若是数组类型且数组元素为对象时,会按照前述规则加载。
- 字段解析:递归查找,本身的直接引用->实现接口则按继承关系从下往上递归搜索父接口->非Object则按继承关系自下往上递归搜索父类->查找失败:NoSuchFieldError/查找成功,接下来进行权限验证【任一步搜索成功则返回,字段同时出现在父类和父接口中,则会拒绝编译“ambiguous”】
- 类方法解析:递归查找,class_index是否为类->本身的直接引用->按继承关系自下往上递归搜索父类 ->按继承关系从下往上递归搜索父接口->查找失败:NoSuchMethodError/查找成功,接下来进行权限验证【任一步搜索成功则返回】
- 接口方法解析:递归查找,class_index是否为接口->本身的直接引用->按继承关系自下往上递归搜索父接口->查找失败:NoSuchMethodError/查找成功,接下来进行权限验证【任一步搜索成功则返回】
5)初始化
初始化阶段是执行类构造器<clinit>()方法的过程
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。不需要显式调用父类构造器,虚拟机会保证在子类的<clinit>()执行之前,父类的<clinit>已经执行完毕,因此虚拟机中第一个被执行的<clinit>()的类是java.lang.Object。该方法非必需,若类没有静态语句块和变量初始化赋值操作,则不会生成此方法。执行接口的<clinit>()不需要先执行父接口的,只有当父接口定义的变量被初始化时,父接口才会初始化。接口的实现类初始化时,也不会执行接口的<clinit>()。虚拟机会保证多线程环境中,只有一个线程执行<clinit>(),其他线程阻塞等待直到执行完毕,但此时其他线程也不会再次进行<clinit>(),同一个类加载器下,一个类只会初始化一次。
7、类加载器
类加载器:实现“通过一个类的全限定名来获取描述此类的二进制字节流”,即实现类的加载动作。每一个类加载器都有一个独立的类名称空间。详见另一篇文章
Bonnie Lai:JAVA 类加载器
8、字节码执行引擎
9、内存模型