Java类加载探究
class loading 理解
编译型语言的连接和Java的连接
C/C++等纯编译语言从源码到最终执行一般要经历:编译、连接和运行三个阶段,连接是在编译期间完成,而java在编译期间仅仅是将源码编译为Java虚拟机可以识别的字节码Class类文件,Java虚拟机对中Class类文件的加载、连接都在运行时执行
编译型语言有连接,Java 也有连接,那么所谓的连接到底是干什么的。C 的连接:
把外部函数的代码(通常是后缀名为.lib和.a的文件),添加到可执行文件中。这就叫做连接(linking)。这种通过拷贝,将外部函数库添加到可执行文件的方式,叫做静态连接(static linking),后文会提到还有动态连接
看起来就是所谓连接,连接的是将有相互调用关系的代码(函数)对接到一起,让他们组织成一个可执行的整体(他们有调用关系,本来就该在一起)。那么 Java 的连接有是啥(Linking)?
加载
解析类的二进制字节流,加载进内存并形成运行时数据结构,生成一个 Class 对象。如果单单是一次 class loading 的加载阶段,并且加载阶段还没有完成,此时 Class 对象可能是不完整(至少可能不是完全解析的)。一些 JVM 实现(比如有 HotSpot)是按需解析符号引用的,Class 内的一些符号引用直到在最终使用的时候才会去解析。详细内容见下面的解析阶段。
验证
安全,错误校验 …
准备
为类中的变量分配内存(静态变量)。内存内一定会有个初始值(0/1),所以这个阶段的变量都会有一个初始值,这个初始值是零值。但有一种例外,被 final 修饰的静态变量,这个阶段就会直接给赋上申明的值。
名为准备,准备的其实是内存,类变量的内存。final 修饰时内存的默认值为指定的,不然就是零值。
解析
这个阶段将符号引用替换为直接引用。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
符号引用是如何转化成直接引用的?
虚拟机实现可以根据需要来判断:到底是在类被加载器加载时就对符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
不同的JVM实现可能选择不同的解析策略。一种做法是在链接的时候,就递归的把所有依赖的形式引用都进行解析。而另外的做法则可能是只在一个形式引用真正需要的时候才进行解析。也就是说如果一个Java类只是被引用了,但是并没有被真正用到,那么这个类有可能就不会被解析。
初始化
估计是执行 static 块代码,赋值 static 变量(不带 final)。执行
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器
() 方法的过程
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
方法与类的构造函数(或者说实例构造器 方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的 方法执行之前,父类的 方法已经执行完毕。因此在虚拟机中第一个被执行的 方法的类肯定是 java. lang.Object
由于父类的
方法优先执行,也就意味着父类中定义的静态语句块要优先于子类。
方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
方法。但接口与类不同的是,执行接口的 方法不需要先执行父接口的 方法。只有当父接日中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 方法。
虚拟机会保证一个类的
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 方法,其他线程都需要阻塞等待,直到活动线程执行 方法完毕。如果在-一个类的 方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种:阻塞往往是很隐蔽的。
使用
卸载
Java 源文件是编译成字节码的,称为 class。class (来自文件或者字节流),class 是一个类的元信息(模板)。JVM 实例化一个类是需要这个元信息的。
命名空间
类和类加载器唯一确定一个类
双亲委派
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java. Lang.Obj ect,它存放在 rt.Jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.Lang. Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
问题记录
类初始化时机,什么时候会触发一个类的初始化
符号引用是啥??符号引用这个东西好像是有印象很重要
平时的编译错误一般都是自己写的类会报编译错误,比如 import 不存在的类。引用的其他 jar 包如果也 import 不存在的类,应该是不会在编译的时候报错的,而是在运行的时候报 class not found。那么问题是:为什么编译的时候,jar 包内的类引用错误不会报编译错误?
为什么需要双亲委派机制?为什么需要具有层级的类加载器?为什么需要命名空间,他是如何工作的?为什么需要类加载器?
Java 安全机制:沙盒?
https://docstore.mik.ua/orelly/java-ent/security/ch01_02.htm
https://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/SecurityManager 是干啥子用的哟?
https://www.jianshu.com/p/54339e09ef35
https://docs.oracle.com/javase/7/docs/api/java/lang/SecurityManager.htmljava 程序的启动,使用哪个类加载器,从有 main 方法的类如何一步一步启动?
应该是 App ClassLoader 加载的,main 方法以后就看用到什么类就去加载什么类双亲委派的作用