您的位置:首页 > 其它

JVM-类加载器-学习笔记

2018-06-23 11:49 120 查看

Java语言是一个解释性语言科普下编译性语言和解释性语言的区别对C 语言或者其他编译型语言来说,编译生成了目标文件,而这个目标文件是针对特定的 CPU 体系的,为 ARM 生成的目标文件,不能被用于 MIPS 的 CPU。这段代码在编译过程中就已经被翻译成了目标 CPU 指令,所以,如果这个程序需要在另外一种 CPU 上面运行,这个代码就必须重新编译。对于各种非编译型语言(例如python/java)来说,同样也可能存在某种编译过程,但他们编译生成的通常是一种『平台无关』的中间代码,这种代码一般不是针对特定的 CPU 平台,他们是在运行过程中才被翻译成目标 CPU 指令的,因而,在 ARM CPU 上能执行,换到 MIPS 也能执行,换到 X86 也能执行,不需要重新对源代码进行编译。

简单总结:
1,编译型语言在编译过程中生成目标平台的指令,解释型语言在运行过程中才生成目标平台的指令。
2,虚拟机的任务是在运行过程中将中间代码翻译成目标平台的指令。Java类加载器原理启动(Bootstrap)类加载器:引导类加载器是用 本地代码实现的类加载器,它负责将 <JAVA_HOME>/lib下面的核心类库 或 -Xbootclasspath选项指定的jar包等 虚拟机识别的类库 加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以 不允许直接通过引用进行操作。  扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 加载到内存中。开发者可以直接使用标准扩展类加载器。  系统(System)类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库 加载到内存中。开发者可以直接使用系统类加载器。

 在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用)。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形)。 关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。1.系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)2.扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null(null 本身就代表着引导类加载器)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)虚拟机出于安全等因素考虑,不会加载<JAVA_HOME>/lib目录下存在的陌生类,换句话说,虚拟机只加载<JAVA_HOME>/lib目录下它可以识别的类。因此,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的但在JVM中,一个类用其 全名 和 一个ClassLoader的实例 作为唯一标识,不同类加载器加载的类将被置于不同的命名空间所以:由不同ClassLoader对象加载的同名类属于不同类不同的类加载器加载同一个类的时候,及时A加载器加载好了,但是B加载器仍然是未加载JVM规范中指明:在同一个类加载器下,一个类型只会被初始化一次,注意是同一个类加载器上下文类加载器(待补充)

查看编译后的CLASS文件

javap -verbose HelloWorld,用于研究(所有常量池中的常量)

javap -c Test.class,通过反汇编把Test.class对应的JVM机器码弄出来:类加载过程

类的生命周期是:加载(类加载机制)->验证->准备->解析->初始化->使用->卸载,并且只有在准备阶段和初始化阶段才会涉及类变量的初始化和赋值

加载(Loading):参考类加载器原理

验证(Verification):java安全机制准备: 准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中,final的变量准备阶段就有值
解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。注意:链接一个类包括对该类、其直接超类、其直接superinterfaces进行验证(verifying)、准备(preparing),如果该类为数组类型,还包括对数组元素类型的链接。Loading, Linking, and Initializing的时间顺序遵循以下两个原则:(1)一个类一定要在链接之前加载完成;(2)一个类的验证和准备一定要在初始化之前完成。        关于类中的符号引用什么时候进行解析,这个要看JVM的实现。例如有的JVM实现采用懒解析("lazy"or"late" resolution),即在需要进行该符号引用的解析时才去解析它,这样的话,可能该类都已经初始化完成了,如果其他的类链接到该类中的符号引用,需要进行解析,这个时候才会去解析;还有的JVM实现采用静态解析("eager" or "static" resolution),即在该类在进行验证时一次性将其中的符号引用全部解析完成。在JVM的实现中,一般采用懒解析。

初始化:类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕,在同一个类加载器下,一个类型只会被初始化一次。

Java的双亲委派模式解决不了SPI,正常情况下,java中一个接口以及它的实现必须是同一个类加载器来完成,如果接口和实现不是一个类加载器的话,就会出现找不到实现的情况


所以JDK提供了线程上下文加载器来解决这个问题

以下是针对SPI的一种直白的解释

直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里(即最底层的AppClassLoader中),后续你想怎么操作(驱动实现类的static代码块)就是你的事了。JVM内存说明

根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native MethodStack)、方法区(Method Area)、堆(Heap)。


 1、所有线程共享的内存数据区:方法区,堆。而虚拟机栈,本地方法栈和程序计数器都是线程私有的。 2、存放于栈中的东西如下:
   2.1 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)。对象都存放在堆区中。2.2 每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
   2.3 方法的形式参数,方法调用完后从栈空间回收
   2.4 引用对象的地址,引用完后,栈空间地址立即被回收,堆空间等待GC 3、存放于堆中的东西如下:3.1 存储的全部是对象,每个对象包含一个与之对应的class信息3.2 Jvm只有一个堆区(heap)被所有线程共享,堆区中不存放基本类型和对象引用,只存放对象本身 4、存放于方法区中的东西如下:4.1 存放线程所执行的字节码指令4.2 跟堆一样.被所有线程共享.方法区包含:所有的class和static变量4.3 常量池位于方法区中,见如下图示说明类的实例化过程

Java实例化对象的顺序,类中

在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。

 

 


 


 

 

 

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: