快捷搜索:  汽车  科技

java 自定义类加载作用(Java类加载过程)

java 自定义类加载作用(Java类加载过程)词法分析是将源代码的字符转变为标记(Token)集合的过程,单个字符是代码源文件编写时的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。如“int x = y 1”这句代码中就包含了6个标记,分别是int、x、=、y、 、1,虽然关键字int由3个字符组成,但它是一个独立的标记,不可再拆分。词法分析过程由com.sun.tools.javac.parser.Scanner类来实现。词法、语法分析即时编译器:通常称为JIT编译器(Just In Time Compiler),运行期把字节码转变成本地机器码。代表产品有HotSpot虚拟机的C1、C2编译器,Graal编译器。提前编译器:通常称为AOT编译器(Ahead Of Time Compiler),直接把程序编译成与目标机器指令集相关的二进制代码。代表产品有JDK的Jaotc、GNU Compil

JAVA类是如何编译以及加载到Java虚拟机中呢? 看下图所示,简要概括了整个相关流程:

java 自定义类加载作用(Java类加载过程)(1)

1.编译

在Java技术中,有下面三种类型的编译器:

前端编译器:把*.java文件转变成*.class文件。代表性的产品有JDK的Javac、Eclipse JDT中的增量式编译器(ECJ)。

即时编译器:通常称为JIT编译器(Just In Time Compiler),运行期把字节码转变成本地机器码。代表产品有HotSpot虚拟机的C1、C2编译器,Graal编译器。

提前编译器:通常称为AOT编译器(Ahead Of Time Compiler),直接把程序编译成与目标机器指令集相关的二进制代码。代表产品有JDK的Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET。

其中,最符合我们对Java程序编译认知的是前端编译器,也是本文要重点介绍的。

词法、语法分析

词法分析是将源代码的字符转变为标记(Token)集合的过程,单个字符是代码源文件编写时的最小元素,但标记才是编译时的最小元素。关键字、变量名、字面量、运算符都可以作为标记。如“int x = y 1”这句代码中就包含了6个标记,分别是int、x、=、y、 、1,虽然关键字int由3个字符组成,但它是一个独立的标记,不可再拆分。词法分析过程由com.sun.tools.javac.parser.Scanner类来实现。

语法分析是根据标记序列构造抽象语法树的过程,抽象语法树(Abstract Syntax Tree,AST)是一种用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值,甚至代码注释都可以是一种特定的语法结构。语法分析过程由com.sun.tools.javac.parser.Parser类实现,构造出的抽象语法树以com.sun.tools.javac.tree.JCTree类表示。

注解处理器

JDK5之后,Java语言提供了对注解(Annotations)的支持。原本设计上,注解与普通Java代码一样,都只会在程序运行期间发挥作用,但在JDK6又提出并设计了一组被称为“插入式注解处理器”的标准API,可以提前至编译期对代码中的特定注解进行处理,从而影响到前端编译器的工作过程。开发人员能使用插入式注解处理器来实现许多原本只能在编码中由人工完成的事情。比如我们常用的Lombok,它可以通过注解来实现自动产生getter/setter方法等等,帮助我们提升开发效率并消除冗长的Java代码。

语义分析

经过语法分析之后,编译器获得了程序代码的抽象语法树表示,抽象语法树能够表示一个结构正确的源程序,但无法保证源程序的语义是符合逻辑的。所以,语义分析的主要任务是对结构上正确的源程序进行上下文相关的检查,比如类型检查、控制流检查等等。我们编写代码时,在IDE中看到的由红线标注的错误提示,绝大部分都是来源于语义分析阶段的检查结果。

字节码生成

字节码生成是Javac编译过程的最后一个阶段,由com.sun.tools.javac.jvm.Gen类来完成。字节码生成阶段不仅仅是把前面各个步骤所生成的信息转化成字节码指令写到磁盘中,编译器还进行了少量的代码添加和转换工作,如添加默认构造函数、把字符串的 操作替换为StringBuffer或StringBuilder的append()操作等等。最终,生成class文件,编译过程结束。

即时编译(JIT,Just In Time Compiler)

在程序执行过程中能够随着程序的需要生成并执行新的代码即称为即时编译。在目前大多数虚拟机里面都实现了JIT即时编译技术。虽然Java虚拟机规范并没有明确指出必须实现它,但是JIT编译性能、代码优化程度却是衡量一款虚拟机最关键的指标之一。

2.加载

类加载器完成类的加载,主要包括三部分工作:

  • 获取class字节码文件二进制字节流
  • 将文件静态结构载入内存方法区转化为运行时数据结构,即类信息
  • 将载入后的类信息进行组装,在堆空间中生成类对象

类加载器

java 自定义类加载作用(Java类加载过程)(2)

JVM预定义了三种类型类加载器:

Bootstrap Classloader(启动类加载器):是用本地代码实现的类装入器,它负责将
<JAVA_HOME>/jre/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

Extension Classloader(标准扩展类加载器):是由sun.misc.Launcher$ExtClassLoader实现的。它负责将<JAVA_HOME>/jre/lib/ext目录或者由系统变量java.ext.dirs指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

System Classloader(系统类加载器):是由sun.misc.Launcher$AppClassLoader实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

双亲委派

当前类加载器需要加载某个类时不会自己直接去加载,而是尝试将加载这个类的任务向上传递交给父加载器去完成,如果父类加载器能加载则由父类加载器来完成而自己不会再去加载。双亲委派模式的具体工作过程如下:

①当System ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。

②当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。

③如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>/lib中未找到所需类),就会让Extension ClassLoader尝试加载。

④如果Extension ClassLoader也加载失败,就会使用System ClassLoader加载。

⑤如果System ClassLoader也加载失败,就会使用自定义加载器(User Classloader)去尝试加载。

一个简单示意图,有助于理解:

java 自定义类加载作用(Java类加载过程)(3)

双亲委派模式优点

  • 避免重复加载

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,子ClassLoader就没有机会加载。

  • 避免JDK核心类被篡改

考虑这样一个问题:能不能自己写个类叫 java.lang.System ?

答案:JDK中的System核心类是Bootstrap加载器加载的,就算自己重写,当加载自定义的System类时,通过双亲委托模式传递到Bootstrap加载器,Bootstrap加载器发现该类已被加载,所以并不会再次加载自定义的System类,总是使用JDK自带的System类,自己写的System类根本没有机会被加载。这样可以防止JDK核心类被随意篡改。

显式加载与隐式加载

显式加载:程序主动调用下列方法之一去主动加载一个类

  • classloader.loadClass(className)
  • Class.forName(className)

隐式加载:被显式加载的类对其它类可能存在如下引用

  • 继承(extend)
  • 实现接口(implements)
  • 域变量
  • 方法定义
  • 方法中定义的本地变量

被引用的类会被动地一并加载到JVM,这种加载方式属于隐式加载。

猜您喜欢: