本文共 7791 字,大约阅读时间需要 25 分钟。
综上所述:JDK包含JRE,JRE包含JVM,关系如下图所示
类的加载过程是指将java字节码文件装载到JVM虚拟机内(准确的说是将class文件读入到内存中,将其放在运行时数据区方法区内,然后在堆区创建一个java.lang.Class对象,用来封装在方法区内的数据结构)
类的加载过程主要分为加载–>链接–>初始化三个阶段类的加载过程中主要做如下三件事
1:验证 – 验证被加载类的正确性
2:准备 - 为类的变量分配内存空间并设置初始化默认值
3:解析 - 将常量迟中的符号引用转化为直接引用的过程
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
在初始化阶段,主要为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量时指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
加载器名称 | 加载器说明 |
---|---|
Bootstrap ClassLoader 【启动类加载器】 | 主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。 |
Extention ClassLoader 【扩展类加载器】 | 加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。 |
Appclass Loader【应用程序类加载器】 | 加载当前应用的classpath的所有类 |
加载器名称 | 加载器说明 |
---|---|
Custom ClassLoader【自定义类加载器】 | 加载应用程序根据自身需要定义的ClassLoader,如tomcat、jboss会根据及j2ee规范自行实现ClassLoader |
类加载器示意图如下:
类加载的三种方式。
(1)通过命令行启动应用时由JVM初始化加载含有main()方法的主类。
(2)通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。
(3)通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。
public class FDDloaderTest { public static void main(String[] args) throws ClassNotFoundException { ClassLoader loader = HelloWorld.class.getClassLoader(); System.out.println(loader); }}//一、 使用ClassLoader.loadClass()来加载类,不会执行初始化块 loader.loadClass("Fdd");//二、 使用Class.forName()来加载类,默认会执行初始化块 Class.forName("Fdd"); //三、使用Class.forName()来加载类,指定ClassLoader,初始化时不执行静态块 Class.forName("Fdd", false, loader); } }
遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。
//遵守双亲委派原则 public class MyClassLoader extends ClassLoader { private String libPath; public DiskClassLoader(String path) { libPath = path; } @Overrideprotected Class findClass(String name) throws ClassNotFoundException { String fileName = getFileName(name); File file = new File(libPath, fileName); try { FileInputStream is = new FileInputStream(file); ByteArrayOutputStream bos = new ByteArrayOutputStream(); int len = 0; try { while ((len = is.read()) != -1) { bos.write(len); } } catch (IOException e) { e.printStackTrace(); } byte[] data = bos.toByteArray(); is.close(); bos.close(); return defineClass(name, data, 0, data.length); } catch (IOException e) { e.printStackTrace(); } return super.findClass(name);} //获取要加载 的class文件名 private String getFileName(String name) { int index = name.lastIndexOf('.'); if (index == -1) { return name + ".class"; } else { return name.substring(index + 1) + ".class"; } } }
综合示意图
JVM的整体内存模型结构如下图所示JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区
域【JAVA 堆、方法区】、直接内存负责加载文件存入运行时数据, 详见本文第三部分
负责存储运行时的数据的地方
方法区存储类的构造信息+常量+静态变量+即时编译后的代码数据
Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧(Stack Frame)用于存储局部变量表(局部变量表需要的内存在编译期间就确定了所在方法运行期间不会改变大小)、操作数栈、动态链接、方法出口等信息,每一个方法从调用至出栈的过程,就对应着栈帧在虚拟机中从入栈到出栈的过程。
虚拟机栈通过压/出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上 在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定 栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等栈帧结构:
局部变量表
保存方法中用到的所有局部变量,包括基本数据类型和引用类型操作数栈
操作数栈是一个后入先出(Last In First Out)栈,方法的执行操作在操作数栈中完成,每一个字节码指令往操作数栈进行写入和提取的过程,就是入栈和出栈的过程。
同局部变量表一样,操作数栈的最大深度也是Java 程序编译成 Class 文件时被写入到 Class 文件格式属性表的 Code 属性的 max_stacks 数据项中。
操作数栈的每一个元素可以是任意的 Java 数据类型,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,在方法执行的任何时候,操作数栈的深度都不会超过在 max_stacks 数据项中设定的最大值(指的是进入操作数栈的 “同一批操作” 的数据类型的栈容量的和)。
当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,通过一些字节码指令从局部变量表或者对象实例字段中复制常量或者变量值到操作数栈中,也提供一些指令向操作数栈中写入和提取值,及结果入栈,也用于存放调用方法需要的参数及接受方法返回的结果。例如,整数加法的字节码指令 iadd(使用 iadd 指令时,相加的两个元素也必须是 int 型) 在运行的时候将操作数栈中最接近栈顶的两个 int 数值元素出栈相加,然后将相加结果入栈。
// java 代码public void test() { byte a = 1; short b = 1; int c = 1; long d = 1L; float e = 1F; double f = 1D; char g = 'a'; boolean h = true;}// 字节码指令0: iconst_1 // 把 a 压入操作数栈栈顶1: istore_1 // 将栈顶的 a 存入局部变量表索引为1的 Slot2: iconst_1 // 把 b 压入操作数栈栈顶3: istore_2 // 将栈顶的 b 存入局部变量表索引为2的 Slot4: iconst_1 // 把 c 压入操作数栈栈顶5: istore_3 // 将栈顶的 c 存入局部变量表索引为3的 Slot6: lconst_1 // 把 d 压入操作数栈栈顶7: lstore 4 // 将栈顶的 d 存入局部变量表索引为4的 Slot,由于 long 是64位,所以占2个 Slot9: fconst_1 // 把 e 压入操作数栈栈顶10: fstore 6 // 将栈顶的 e 存入局部变量表索引为6的 Slot12: dconst_1 // 把 f 压入操作数栈栈顶13: dstore 7 // 将栈顶的 f 存入局部变量表索引为4的 Slot,由于 double 是64位,所以占2个 Slot15: bipush 97 // 把 g 压入操作数栈栈顶17: istore 9 // 将栈顶的 g 存入局部变量表索引为9的 Slot19: iconst_1 // 把 h 压入操作数栈栈顶20: istore 10 // 将栈顶的 h 存入局部变量表索引为10的 Slot
动态链接
方法出口
当一个方法开始执行后,只有2种方式可以退出这个方法 :java字节码指令
1:如下一段java代码程序、执行javac命令生成class文件
javac C:\Users\admin\Desktop\Demo.java2:使用javap命令反解析class文件生成汇编代码如下图所示
javap -c C:\Users\admin\Desktop\Demo.class
3:部分指令说明
指令 | 说明 |
---|---|
aload_0 | |
invokespecial | |
bipush | |
putfield | |
ldc | 常量池中的常量 zhangsan 入栈 |
加载本地方法的数据区
记录当前字节码指令执行到的地址
转载地址:http://xxqms.baihongyu.com/