摘要:
本文简单介绍了Java类文件结构与类加载机制。类文件包含魔数、版本信息、常量池等基本结构,并通过字节码示例解析了"i=i++"结果为0的原因。类加载过程分为加载、连接(验证、准备、解析)、初始化等阶段,涉及方法区InstanceKlass和堆区Class对象的创建。重点分析了双亲委派机制的工作原理及其安全性保障,包括启动类、扩展类和应用程序类加载器的协作关系。最后探讨了JDK9模块化设计带来的变化。全文通过字节码指令和内存模型深入揭示了Java类加载的底层实现原理。
一,类文件结构
1,基本信息
(1)魔数(Magic Number)
用于标识这是一个有效的类文件,固定为:0xCAFEBABE
。
(2)版本信息(Version)
包含主版本号和次版本号,表示编译此类文件所用的Java版本。
(3)访问标志(Access Flags)
指示类的修饰符(如public
、final
、abstract
等)。
2,常量池(Constant Pool)
一个存放各种字面量(如字符串、整数)和符号引用(如类名、方法名、字段名)的表,这对整个类的结构和行为至关重要。
3,字段(Fields)
类中定义的成员变量。
4,方法(Methods)
类中定义的方法,包括构造器、普通方法等。
问题:int i = 0; i = i ++; i的结果为多少?-->0,根据字节码剖析:
i++先把0放入临时操作数栈中,然后执行自增的指令"iincr 1 by 1",在局部变量数组中 i 对应的数组值加1,但是赋值操作时又会让操作数栈中的 0 覆盖掉现在局部变量数组中的 1,所以结果为0
5,属性(Attributes)
其他描述信息,比如调试信息、注解等。
二,类的生命周期
类的生命周期主要分为:加载,连接(验证,准备,解析),初始化,使用,卸载
1,加载阶段
加载阶段会在方法区和堆区分别创造InstanceKlass和java.lang.Class对象,他们之间通过地址引用相互关联,这两个对象都包含了一个类的基本信息,字段,属性,方法,虚方法表(实现多态) 等信息
(1)类加载器根据类的全限定名以二进制流的方式获取字节码信息
(2)加载完类后,Java虚拟机会将字节码信息保存到方法区中--instanceKlass
(3)Java虚拟机还会在堆区生成一份与方法区中类似的Java.lang.Class对象
2,连接阶段
(1)验证
(2)准备
为静态变量分配内存并设置初始值(默认值),如果静态变量有final关键字就会直接在这个阶段赋设定值而不再是初始值
(3)解析
将常量池中的符号引用替换为直接引用,符号引用就是在字节码文件中使用编号如:#6,#7....等访问常量池中的内容;直接访问不在使用编号。而是使用内存的直接引用(内存地址)
3,初始化阶段
初始化阶段是为静态变量和静态代码块的变量赋值,执行顺序与代码编写顺序一致
初始化示例以及执行的初始化字节码clinit指令如下:
public class Demo_1 {
/*
0 iconst_1 将int类型常量1推送至操作数栈
1 putstatic #13 <com/muyi/jvm/test/Demo_1.i : I> 将操作数栈顶的值存入静态变量i中
4 iconst_2 将int类型常量2推送至操作数栈
5 putstatic #13 <com/muyi/jvm/test/Demo_1.i : I> 将操作数栈顶的值存入静态变量i中
8 return
*/
public static int i=1;
static {
i=2;
}
public static void main(String[] args) {
System.out.println(i);
}
}
Tips:访问父类的静态变量不需要初始化子类,而初始化子类之前一定会初始化父类。
三,类加载器
类加载器负责在类加载的过程中字节码的获取并加载到内存这一部分。通过加载字节码数据放入内存转换为byte[],接下来调用底层方法将byte[]转换为方法区和堆中的数据
【1】类加载器的分类(jdk8及之前)
1,启动类加载器
虚拟机底层实现,用于加载Java中最核心的类,如:String等,无法被Java代码直接获取(获取返回为null),可以通过虚拟机参数加载自己的jar包到启动加载器。
2,Java中默认的加载器
(1)扩展类加载器(ExtClassLoader)
加载Java安装目录/jre/lib/ext下的类文件,通用但是没有那么重要,同样可以通过虚拟机参数加载自己的jar包到该加载器
(2)应用程序加载器(AppClassLoader)
加载项目中程序员自己编写的类和第三方依赖中的类的字节码文件,其范围覆盖了启动类加载器和扩展类加载器
【2】双亲委派机制
由于Java虚拟机中有多个类加载器,双亲委派机制就是为了解决类应该由谁来加载的问题
1,作用
(1)保证类加载的安全性,确保核心类加载的完整性和安全性。
(2)避免一个类被重复加载。
2,加载过程
3,类的双亲委派机制是什么?(面试)
(1)当一个类加载器去加载某个类时,会自底向上查找该类是否被加载过,如果加载过直接返回加载后的对象,如果直到最顶层的加载器都没有加载,就会由顶向下尝试加载。
(2)应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器。
(3)该机制好处有两点:第一避免恶意代码替换核心类库,确保核心类加载的完整性和安全性,比如:Java.lang.String(举例),第二避免一个类重复的被加载。
【3】打破双亲委派机制
1,源码解析
(1)name-类名,resolve-是否进行连接,通过锁避免并发时一个类被多次加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {...}
}
(2)根据类名在当前类加载器中查找该类是否加载过,如果加载过直接返回该类对象
Class<?> c = findLoadedClass(name);
........
if (resolve) {
resolveClass(c);
}
return c;
(3)如果没有找到,调用父类加载器的加载方法,将类加载委派到父类加载器,直到parent==null,代表需要通过启动类加载器(最顶层的加载器)来进行加载,这是会调用本地接口中的native方法
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
.........
}
(4)如果返回的对象c还是为空,说明父类及以上加载器都加载失败,在当前加载器进行加载--finaClass(),该方法由子类(URLClassLoader)实现,核心逻辑是通过该类的文件路径获取到该类的字节码文件
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
(5)然后调用ClassLoader中的defineClass方法并传入二进制的字节码文件,进行一些校验工作并且通过definaeClass1调用本地接口中的方法将字节码信息保存到方法区和堆中。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(this, name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
2,自定义加载器
自定义加载器的父类加载器默认为应用程序类加载器,同时两个自定义加载器加载相同限定名的类不会冲突,因为在同一个虚拟机中,只有相同类加载器+相同类限定名才会被认为是同一个类
3,线程上下文类加载器,其实并未打破双亲委派机制:
第一,对于DriverManager这个类,在jdk8中位于rt核心类库中,所以必定是通过启动类加载器进行加载,没有违背该机制。
第二,DriverManager通过ServiceLoader来动态的加载驱动类(SPI机制的应用者),而ServiceLoader本身通过双亲委派机制被AppClassLoader加载,所以拿到的上下文类加载器是AppClassLoader
四,模块化设计的变化(jdk9及以上)