Java编译和类加载详述
Java程序运行时,必须经过编译和运行两个步骤。首先将后缀名为.java的源文件进行编译,最终生成后缀名为.class的字节码文件。然后Java虚拟机将编译好的字节码文件加载到内存(这个过程被称为类加载,是由加载器完成的),然后虚拟机针对加载到内存的java类进行解释执行,显示结果。
Java的运行原理
在Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(ByteCode),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。
Java代码编译整个过程
Java代码编译是由Java源码编译器来完成,流程图如下所示:
词法分析器:
词法分析器一般以函数的形式存在,供语法分析器调用。
这里的单词是一个字符串,是构成源代码的最小单位。从输入字符流中生成单词的过程叫作单词化(Tokenization),在这个过程中,词法分析器还会对单词进行分类。
词法分析器通常不会关心单词之间的关系(属于语法分析的范畴),举例来说:词法分析器能够将括号识别为单词,但并不保证括号是否匹配。
词法分析(lexical analysis)或扫描(scanning)是编译器的第一个步骤。词法分析器读入组成源程序的字符流,并且将它们组织成有意义的词素(lexeme)的序列,并对每个词素产生词法单元(token)作为输出。
简单的来说,词法分析就是将源程序(可以认为是一个很长的字符串)读进来,并且“切”成小段(每一段就是一个词法单元 token),每个单元都是有具体的意义的,例如表示某个特定的关键词,或者代表一个数字。而这个词法单元在源程序中对应的文本,就叫做“词素”。词法分析注重的是每个单词是否合法,以及这个单词属于语言中的哪些部分
token流
词法分析器的结果,就是把程序的语句进行分词得到的的一个个“单词”!
语法分析器:
是对token流进行语法检查、并构建由输入的单词组成的数据结构(语法树/抽象语法树)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”;语法分析的上下文无关文法注重的是一个一个的推导式,是将词法分析中得到的单词按照语法规则进行组合
语法树/抽象语法树:
是源代码的抽象语法结构的树状表现形式,这里特指java的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于if(;;){ //当符合条件执行的任务}或者 while(true){//当符合条件执行的任务}这样的条件跳转语句,可以使用带有两个分支的节点来表示。
语义分析器:语义分析就是要了解各个推导式之间的关系是否合法,主要体现在推导式中使用的终结符和非终结符之间的关系,也就是它们的类型。
注解抽象语法树:经过 语义分析器将语法树/抽象语法树转化为注解抽象语法树
字节码生成
目的:将注解语法树转化成字节码,并将字节码写入*.class文件。
流程:
将java的代码块转化为符合JVM语法的命令形式,这就是字节码
按照JVM的文件组织格式将字节码输出到*.class文件中
类加载详解:
在Java 中分为主动引用和被动引用 主动引用都会触发类的加载!比如:访问这个类的静态变量,方法,和 通过new ,jvm标记加载的类(存在main方法的类),反射等,父类在子类加载的时候也会被加载
被动引用:比如访问静态常量或者创建数组内部对象
类加载主要是由jvm虚拟机负责的,过程非常复杂,类加载分三步 加载 》 连接 》初始化
加载
程序运行之前jvm会把编译完成的.class二进制文件加载到内存,供程序使用,用到的就是类加载器classLoader ,java程序的运行并不是直接依 靠底层的操作系统,而是基于jvm虚拟机。
类加载器
负责读取字节码,并转换成java.Long.Class类的一个对象存在于方法区
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始
注意:要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。
这里指的“相同”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。
java中加载器的种类大致可以分为四种:
1.Bootstrap ClassLoader(由C++语言写成)顶级加载器,%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。
2.系统加载器(也就是内部类AppClassLoader) 加载classpath下的路径下的class
3.ExtClassLoader,加载jre\lib\ext\classes文件下的class
4.java.net.UrlClassLoader. 加载指定的url下的class
当我们运行一个程序时,首先是找到JDK安装目下的jvm.dll来启动JAVA虚拟机。
而后Bootstrap ClassLoader产生。
接下来就是Bootstrap ClassLoader来加载ExtClassLoader,并且指定ExtClassLoader的父加载器为Bootstrap ClassLoader,但是因为Bootstrap ClassLoader用C++语言写的,所以用JAVA的观点来看,这个加载器的实例是不存在的所以ExtClassLoader的父加载器被设置为了null。
然后就是Bootstrap ClassLoader将AppClassLoader装载,并指定其父加载器为ExtClassLoader。
类加载器初始化过程:创建JVM启动器实例sun.misc.Launcher。sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应 用类加载器)。JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们 的应用程序。
//Launcher的构造方法
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//构造扩展类加载器,在构造的过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader,
//Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自 己写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
//。。。。。。省略一些不需关注代码
}
双亲委派机制
JAVA是按照加载器的委派模型来实现的。这种模型是JAVA安全性机制的保证。
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式。
我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader 的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:
首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接 返回。
如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加 载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加 载。
//ClassLoader的loadClass方法,里面实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查当前类加载器是否已经加载了该类
Class<?> c = findLoadedClass(name);
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
c = findClass(name);
// this is the defining class loader; record the stats 29 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 ‐ t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//不会执行
resolveClass(c);
}
return c;
}
如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。
为什么要设计双亲委派机制?
沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性
全盘负责委托机制
“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类 所依赖及引用的类也由这个ClassLoder载入。
连接
连接是很重要的一步,过程比较复杂,分为三步 验证 》准备 》解析
验证
确保类加载的正确性。一般情况由javac编译的class文件是不会有问题的,但是可能有人的class文件是自己通过其他方式编译出来的,这就很有可能不符合jvm的编译规则,这一步就是要过滤掉这部分不合法文件
准备
为类的静态变量分配内存,将其初始化为默认值 。我们都知道静态变量是可以不用我们手动赋值的,它自然会有一个初始值 比如int 类型的初始值就是0 ;boolean类型初始值为false,引用类型的初始值为null 。这里注意,只是为静态变量分配内存,此时是没有对象实例的
解析
初始化:
为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值!
类加载完成!!!
备注:
当 JVM 遇到 new 指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载过,如果没有就先执行类加载。如果类已经被加载过,则会为新生对象分配内存(所需内存大小在类加载后就可以确定),分配对象内存采取的方式是“指针碰撞”或“空闲列表”,前者是在内存比较规整的情况下,后者是在空闲内存和已使用内存相互交错的情况下,而内存是否规整这又取决于垃圾回收器。