Java源码编译机制、类加载机制、类执行机制 男娘i 2022-06-09 07:46 140阅读 0赞 **首先问一个问题,Java代码是如何运行的?** 1. 写好一份.Java代码 2. 被打包成jar包或war包,打包过程中,被编译成了.class字节码文件 3. 使用命令”java -jar” 命令,运行这份java代码(或系统),此时就启动了一个JVM进程。 所以,我们平时部署一个系统并运行的时候,其实就是启动了一个JVM,由JVM来运行这台机器上的这个系统。 JVM要运行系统java代码,就需要**类加载器**将class文件中的类加载进JVM内存当中,由JVM自己的**字节码执行引擎**来执行加载进来的类文件,比如,找到并执行系统入口函数main()。 那么,代码是如何被编译的呢?类加载器何时加载类呢?它主要包含了以下重要机制: > **Java源码编译机制** > > **类加载机制** > > **类执行机制** 这里对其一一简单的介绍。 \----------------------------------------------------------------------------------------------------------------- # 1 Java代码编译 # 代码编译由JAVA源码编译器来完成。主要是将源码编译成字节码文件(class文件);字节码文件格式主要分为两部分:**常量池**和**方法字节码**。 Java代码编译是由Java源码编译器来完成,流程图如下所示: ![SouthEast][] Java源码编译机制由以下三个过程组成: > 1. **分析和输入到符号表** > 2. **注解处理** > 3. **语义分析和生成class文件** (javac–verbose 输出有关编译器正在执行的操作的消息) ![SouthEast 1][] 最后生成的class文件由以下部分组成: > **结构信息**:包括class文件格式、版本号、各部分的数量与大小的信息 > > **元数据**:对应于Java源码中声明与常量的信息。包含类/继承的超类/实现的接口的声明信息、域与方法声明信息和常量池 > > **方法信息**:对应Java源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息。 # 2 类加载机制 # ## 2.1 类的生命周期 ## 类的生命周期由被加载到虚拟机内存中开始,到卸载出内存结束,共有七个阶段,其中到初始化之前的都是属于类加载的部分: **加载---验证---准备---解析----初始化---使用---卸载** 系统可能在第一次使用某个类时加载该类,也可能采用预加载机制来加载某个类,当运行某个java程序时,会启动一个java虚拟机进程,两次运行的java程序处于两个不同的JVM进程中,两个jvm之间并不会共享数据。 ### 1、加载阶段 ### 这个流程中的加载是类加载机制中的一个阶段,段需要完成的事情有: 1)通过一个类的全限定名来获取定义此类的二进制字节流。 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 3)在java堆中生成一个代表这个类的Class对象,作为访问方法区中这些数据的入口。 由于第一点没有指明从哪里获取以及怎样获取类的二进制字节流,所以这一块区域留给我开发者很大的发挥空间。 ### 2、准备阶段 ### 这个阶段正式为类变量(被static修饰的变量)分配内存并设置类变量初始值,这个内存分配是发生在方法区中。 1、注意这里并没有对实例变量进行内存分配,实例变量将会在对象实例化时随着对象一起分配在JAVA堆中。 2、这里设置的初始值,通常是指数据类型的零值。 private static int a = 3; 这个类变量a在准备阶段后的值是0,将3赋值给变量a是发生在初始化阶段。 ### 3、初始化阶段 ### 初始化是类加载机制的最后一步,这个时候才正真开始执行类中定义的JAVA程序代码。在前面准备阶段,类变量已经赋过一次系统要求的初始值,**在初始化阶段最重要的事情就是对类变量进行初始化**,关注的重点是父子类之间各类资源初始化的顺序。 java类中对类变量指定初始值有两种方式: > 1)声明类变量时指定初始值; > > 2)使用静态初始化块为类变量指定初始值。 **初始化的时机** 1)创建类实例的时候,分别有:1、使用new关键字创建实例;2、通过反射创建实例;3、通过反序列化方式创建实例。 new Test(); Class.forName(“com.mengdd.Test”); 2)调用某个类的类方法(静态方法) *`Test.doSomething();`* 3)访问某个类或接口的类变量,或为该类变量赋值。 `int b=Test.a; Test.a=b;` 4)初始化某个类的子类。当初始化子类的时候,该子类的所有父类都会被初始化。 5)直接使用java.exe命令来运行某个主类。 除了上面几种方式会自动初始化一个类,其他访问类的方式都称不会触发类的初始化,称为被动引用。 **被动引用的情况** *1、子类引用父类的静态变量,不会导致子类初始化。* > publicclass SupClass > { > public static int a = 123; > static > { > System.out.println("supclassinit"); > } > } > publicclass SubClass extends SupClass > { > static > { > System.out.println("subclassinit"); > } > } > publicclass Test > { > public static void main(String[] args) > { > System.out.println(SubClass.a); > } > } > > 执行结果: > > supclass init > > 123 *2、引用常量时,不会触发该类的初始化* 用final修饰某个类变量时,它的值在编译时就已经确定好放入常量池了,所以在访问该类变量时,等于直接从常量池中获取,并没有初始化该类。 初始化机制: > 1、如果该类还没有加载和连接,则程序先加载该类并连接。 > > **2、如果该类的直接父类没有加载,则先初始化其直接父类。** > > 3、如果类中有初始化语句,则系统依次执行这些初始化语句。 **在第二个步骤中,如果直接父类又有直接父类,则系统会再次重复这三个步骤来初始化这个父类,依次类推,JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有的父类都会被初始化。** ## 2.2 类加载机制 ## ### 类加载器结构关系 ### JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述: ![SouthEast 2][] 1)**Bootstrap ClassLoader /启动类加载器** 是ClassLoader子类 ,自身也没有子类,并且不遵守classLoader加载机制;是JVM内核中的加载器,由C++实现;负责加载$JAVA\_HOME中jre/lib/rt.jar里所有的class。 2)**Extension ClassLoader/扩展类加载器** 是用JAVA编写,且它的父加载器是Bootstrap,但是因为BootStrap是用C++写的,所以有时候也说ExtClassLoader没有父加载器,自身也是顶层父类,但是血统不纯,不全是JVM实现的。 负责加载java平台中扩展功能的一些jar包,包括$JAVA\_HOME中jre/lib/\*.jar或-Djava.ext.dirs指定目录下的jar包 > 通过程序来看下系统变量java.ext.dirs所指定的路径: > > public class Test{ > public static void main(String[] args) { > System.out.println(System.getProperty("java.ext.dirs")); > } > } > > 执行结果: > > C:\\Program Files(x86)\\Java\\jdk1.6.0\_43\\jre\\lib\\ext;C:\\Windows\\Sun\\Java\\lib\\ext 3)**App ClassLoader/ 系统类加载器** 也称为应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件。它的父加载器为Ext ClassLoader。 4)**Custom ClassLoader/用户自定义类加载器** (java.lang.ClassLoader的子类) 属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader **这几种类加载器的层次关系如下图所示:** ![SouthEast 3][] ### 类加载机制 ### 类加载机制的特点: > **全盘负责**,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入 > > **父类委托**,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类 > > **缓存机制**,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效 由上述分析可知,类的加载机制采用的是一种**父类委托机制**,也叫作**双亲委派机制**或者**父优先等级加载机制**: > **如果一个类加载器收到了一个类加载请求,它不会自己去尝试加载这个类,而是首先会自下而上的检查该类是否已被加载,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,并将结果逐层向下反馈;如果没有加载,则继续向上层检查,所有的类加载请求都应该传递到最顶层的启动类加载器中,只有到父类加载器反馈自己无法完成这个加载请求(在它的搜索范围没有找到这个类)时,子类加载器才会尝试自己去加载,这种委派机制的好处就是保证了一个类不被重复加载。** 所以说,类加载检查顺序是自下而上,而加载的顺序是自顶向下,也就是由上层来逐层尝试加载类。 这种类加载机制的实现比较简单,源码如下: protectedsynchronized Class<?> loadClass(String paramString, boolean paramBoolean) throws ClassNotFoundException { //检查是否被加载过 Class localClass =findLoadedClass(paramString); //如果没有加载,则调用父类加载器 if (localClass == null) { try { //父类加载器不为空 if (this.parent != null) localClass = this.parent.loadClass(paramString,false); else { //父类加载器为空,则使用启动类加载器,传统意义上启动类加载器没有父类加载器 localClass =findBootstrapClass0(paramString); } } catch (ClassNotFoundExceptionlocalClassNotFoundException) { //如果父类加载失败,则使用自己的findClass方法进行加载 localClass = findClass(paramString); } } if (paramBoolean) { resolveClass(localClass); } return localClass; } 代码大意就是先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass方法,若父类加载器不存在,则使用启动类加载器。如果父类加载器加载失败,则抛出异常之后看,再调用自己定义的的findClass方法进行加载。 ## **2.3 自定义类加载器** ## 通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java类的字节码,为保证**安全性,**这些字节码经过了**加密处理**,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。**自定义类加载器一般都是继承自** **ClassLoader****类**,从上面对 loadClass方法来分析来看,我们只需要重写 findClass方法即可。 下面我们通过一个示例来演示自定义类加载器的流程: public class MyClassLoader extendsClassLoader { private String root; protected Class<?> findClass(String name) throwsClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0,classData.length); } } private byte[] loadClassData(String className) { String fileName = root + File.separatorChar + className.replace('.',File.separatorChar) + ".class"; try { InputStream ins = new FileInputStream(fileName); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 1024; byte[] buffer = new byte[bufferSize]; int length = 0; while ((length = ins.read(buffer)) != -1) { baos.write(buffer, 0, length); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } public String getRoot() { return root; } public void setRoot(String root) { this.root = root; } public static void main(String[] args) { MyClassLoader classLoader = new MyClassLoader(); classLoader.setRoot("E:\\temp"); Class<?> testClass = null; try { testClass =classLoader.loadClass("com.neo.classloader.Test2"); Object object = testClass.newInstance(); System.out.println(object.getClass().getClassLoader()); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } 自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意: > 1)这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。 > > 2)最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。 > > 3)这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。 # 3 类执行机制 # Java字节码的执行是由JVM执行引擎来完成,流程图如下所示: ![SouthEast 4][] JVM是基于栈的体系结构来执行class字节码的。 > 线程创建后,都会产生一个线程私有的**程序计数器(PC**寄存器)和**栈(Stack)**: > > **程序计数器**存放程序正常执行时下一条要执行的指令在方法内的偏移量地址; > > **栈**中存放一个个栈帧,各个方法每调用一次就会创建一个自己私有的**栈帧**,栈帧分为**局部变量表、操作数栈、动态连接、方法返回地址和一些附加信息**: > > 1) 局部变量区是一组变量值存储空间,用于存放方法中的参数、局部变量; > > 局部变量表的容量以变量槽(slot)为最小单位,一个slot可以存放一个32位以内的数据类型,而[**Java**][Java]中占32位以内的数据类型有boolean、byte、char、short、int、float、reference(也可以64位)和returnAddress八种类型 > > Java语句中明确规定的64位的数据类型只有long和double两种(reference可能是32位,也可能是64位)故long和double不是原子操作,只是局部变量表建立在线程的堆栈上,是线程私有的数据,无论读写两个连续的slot是否是原子操作,都不会引起数据安全问题 > > 2) 操作数栈中用于存放方法执行过程中产生的中间结果。 > > 3) 动态连接 > > 符号引用一部分会在类加载阶段或第一次使用的时候转换成为直接引用,这种转换称为**静态解析**。另外一部分将在每一次的运行期间转换为直接引用,这部分称为**动态引用** > > 4)方法返回地址 > > 当一个方法被执行后,有两种方式退出这个方法。 > > **第一种是**执行引擎,遇到一个方法返回的字节码指令,这时可能会返回值传递给上层的方法调用者。这种退出方式为正常完成出口 > > **另一种是**遇到异常并且没有在方法体内得到处理(throws不属于方法体内处理),这种退出方式是不会给它的上层调用者产生任何返回值的。 > > 一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。 **方法退出的实质** > 实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表盒操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等 *再续相关部分……* [SouthEast]: /images/20220609/702895e645ca475391f7fcf91bb16727.png [SouthEast 1]: /images/20220609/0324b20d184a4a47863579b2e396634a.png [SouthEast 2]: /images/20220609/abf5aa8d89034e3ca4cb597fea760d14.png [SouthEast 3]: /images/20220609/fe84e9bfafa6462a9e2eed7b3665d568.png [SouthEast 4]: /images/20220609/cbd1f4b45ecf42b8b2ee78f0546d892e.png [Java]: http://lib.csdn.net/base/java
还没有评论,来说两句吧...