南宁做网站哪家公司好,wordpress设置首主导航,wordpress默认头像,版面设计素材文章目录 概要加载类加载器分类双亲委派模型自定义加载器 验证准备解析初始化cinit与init 概要
jvm运行时的整体结构如下 一个Car类#xff0c;类跟Car对象的转换过程如下#xff1a;
加载后的class类信息存放于方法区#xff1b;ClassLoader只负责clas… 文章目录 概要加载类加载器分类双亲委派模型自定义加载器 验证准备解析初始化cinit与init 概要
jvm运行时的整体结构如下 一个Car类类跟Car对象的转换过程如下
加载后的class类信息存放于方法区ClassLoader只负责class文件的加载至于它是否可以运行则由Execution Engine决定如果调用构造器实例化对象则该对象存放在堆区
其中类的加载总体流程如下
加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中将其放在运行时数据区的方法区内然后在创建一个java.lang.Class对象用来封装类在方法区内的数据结构。
加载是类加载的第一个阶段。有两种时机会触发类加载
预加载 虚拟机启动时加载加载的是JAVA_HOME/lib/下的rt.jar下的.class文件这个jar包里面的内容是程序运行时非常常 常用到的像java.lang.*、java.util.、java.io. 等等因此随着虚拟机一起加载运行时加载 虚拟机在用到一个.class文件的时候会先去内存中查看一下这个.class文件有没有被加载如果没有就会按照类的全限定名来加载这个类
那么加载阶段做了什么其实加载阶段做了有三件事情
获取.class文件的二进制流将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中在内存中生成一个代表这个.class文件的java.lang.Class对象作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的不过HotSpot虚拟机比较特殊这个Class对象是放在方法区中的
类加载器分类
jvm提供了3个系统加载器分别是Bootstrp loader、ExtClassLoader、AppClassLoader 这三个加载器互相成父子继承关系 Bootstrp loader: Bootstrp加载器是用C语言写的它在Java虚拟机启动后初始化它主要负责加载以下路径的文件 %JAVA_HOME%/jre/lib/*.jar %JAVA_HOME%/jre/classes/* -Xbootclasspath参数指定的路径 可通过System.out.println(System.getProperty(sun.boot.class.path));打印查看 ExtClassLoaderExtClassLoader是用Java写的具体来说就是sun.misc.Launcher$ExtClassLoader,其主要加载 %JAVA_HOME%/jre/lib/ext/ ext下的所有classes目录 java.ext.dirs系统变量指定的路径中类库 可通过System.getProperty(java.ext.dirs)打印查看 AppClassLoader: AppClassLoader也是用Java写成的它的实现类是sun.misc.Launcher$AppClassLoader另外我们知道ClassLoader中有个getSystemClassLoader方法此方法返回的就是它。 负责加载 -classpath 所指定的位置的类或者是jar文档也是Java程序默认的类加载器 System.getProperty(java.class.path)
双亲委派模型
什么是双亲委派
双亲委派模型工作过程是如果一个类加载器收到类加载的请求它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器完成。每个类加载器都是如此只有当父加载器在自己的搜索范围内找不到指定的类时即 ClassNotFoundException 子加载器才会尝试自己去加载。
为什么需要双亲委派模型
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系通过这种层级关可以避免类的重复加载当父加载器已经加载了该类时就没有必要子加载器再加载一次。 其次是考虑到安全因素java核心api中定义类型不会被随意替换假设通过网络传递一个名为 java.lang.Integer 的类通过双亲委派模型传递到启动类加载器而启动类加载器发现这个名字的类发现该类已被加载就不会重新加载网络传递过来的 java.lang.Integer 而直接返回已加载过的Integer.class 这样便可以防止核心API库被随意篡改。
双亲委派能否打破 可以的比如在tomcat中tomcat通过 war 包进行应用的发布它其实是违反了双亲委派机制原则 看一下tomcat类加载的层次结构如下
比如Tomcat的 webappClassLoader 加载web应用下的class文件不会传递给父类加载器问题tomcat的类加载器为什么要打破该模型 首先一个tomcat启动后是会起一个jvm进程的它支持多个web应用部署到同一个tomcat里为此
对于不同的web应用中的class和外部jar包需要相互隔离不能因为不同的web应用引用了相同的jar或者有相同的class导致一个加载成功了另一个加载不了。web容器支持jsp文件修改后不用重启jsp文件也是要编译成.class文件的每一个jsp文件对应一个JspClassLoader它的加载范围仅仅是这个jsp文件所编译出来的那一个.class文件当Web容器检测到jsp文件被修改时会替换掉目前JasperLoader的实例并通过再建立一个新的Jsp类加载器来实现JSP文件的热部署功能。
如何实现双亲委派模型
双亲委派模型的原理很简单实现也简单。每次通过先委托父类加载器加载当父类加载器无法加载时再自己加载。其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了我们无需去写
几个重要的函数
loadClass 默认实现如下
public Class? loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);}再看看 loadClass(String name, boolean resolve) 函数
protected Class? loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass? 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();c findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}
从上面代码可以明显看出 loadClass(String, boolean) 函数即实现了双亲委派模型整个大致过程如下
首先检查一下指定名称的类是否已经加载过如果加载过了就不需要再加载直接返回。如果此类没有加载过那么再判断一下是否有父加载器如果有父加载器则由父加载器加载即调用 parent.loadClass(name, false); .或者是调用 bootstrap 类加载器来加载如果父加载器及 bootstrap 类加载器都没有找到指定的类那么调用当前类加载器的 findClass 方法来完成类加载。
也就是说如果要自定义类加载器就要重写fiindClass方法。
抽象类 ClassLoader 的 findClass 函数默认是抛出异常的。而前面我们知道 loadClass 在父加载器无法加载类的时候就会调用我们自定义的类加载器中的 findeClass 函数因此我们必须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象
自定义加载器
除了上面的系统提供的3种loaderjvm允许自己定义类加载器典型的在tomcat上
为什么要自定义类加载器
隔离加载类 模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器内部自定义了好几中类加载器用于隔离web应用服务器上的不同应用程序。修改类加载方式 除了Bootstrap加载器外其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。扩展加载源 比如还可以从数据库、网络、或其他终端上加载防止源码泄露 java代码容易被编译和篡改可以进行编译加密类加载需要自定义还原加密字节码
自定义类加载器的加载流程 自定义加载器
public class MyClassLoader extends ClassLoader {private String codePath;protected MyClassLoader(ClassLoader parent, String path) {super(parent);this.codePath path;}public MyClassLoader(String classPath) {this.codePath classPath;}Overrideprotected Class? findClass(String name) throws ClassNotFoundException {String fileName codePath name .class;try ( // 输入流BufferedInputStream bis new BufferedInputStream(new FileInputStream(fileName));// 输出流ByteArrayOutputStream baos new ByteArrayOutputStream()) {int len;byte[] data new byte[1024];while ((len bis.read(data)) ! -1) {baos.write(data, 0, len);}//5.获取内存中字节数组byte[] byteCode baos.toByteArray();//6.调用defineClass 将字节数组转成Class对象Class? defineClass defineClass(null, byteCode, 0, byteCode.length);return defineClass;} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return null;}
}有以下注意点 所有用户自定义类加载器都应该继承ClassLoader类 在自定义ClassLoader的子类是,我们通常有两种做法: 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)重写findClass方法 (推荐)
验证
连接阶段的第一步这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求并且不会危害虚拟机自身的安全。 Java语言本身是相对安全的语言相对C/C来说但是前面说过.class文件未必要从Java源码编译而来可以使用任何途径产生甚至包括用十六进制编辑器直接编写来产生.class文件。在字节码语言层面上Java代码至少从语义上是可以表达出来的。虚拟机如果不检查输入的字节流对其完全信任的话很可能会因为载入了有害的字节流而导致系统崩溃所以验证是虚拟机对自身保护的一项重要工作。
验证阶段主要做以下几方面的工作
文件格式验证是不是CAFEBABYE开头主次版本号是否在当前jvm虚拟机可运行的范围内等元数据验证段主要验证属性、字段、类关系、方法等是否合规字节码验证这里主要验证class里定义的方法看方法内部的code是否合法符号引用验证字节码里有的是直接引用有的是指向了其他的字节码地址。而符号引用验证的就是这些引用的对应的内容是否合法
准备
准备阶段是正式为类变量分配内存并设置其初始值的阶段这些变量所使用的内存都将在方法区中分配。关于这点有两个地方注意一下
这时候进行内存分配的仅仅是类变量被static修饰的变量而不是实例变量实例变量将会在对象实例化的时候随着对象一起分配在Java堆中这个阶段赋初始值的变量指的是那些不被final修饰的static变量比如public static int value 123value在准备阶段过后是0而不是123给value赋值为123的动作将在初始化阶段才进行比如public static final int value 123;就不一样了在准备阶段虚拟机就会给value赋值为123。
各个数据类型的零值如下表
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。来了解一下符号引用和直接引用有什么区别
符号引用 符号引用是一种定义可以是任何字面上的含义而直接引用就是直接指向目标的指针、相对偏移量。 这个其实是属于编译原理方面的概念符号引用包括了下面三类常量
类和接口的全限定名字段的名称和描述符方法的名称和描述符
看一段代码
public class TestMain {private static int i;private double d;public static void print() {}private boolean trueOrFalse() {return false;}
}反编译后得到
Constant pool:#1 Methodref #3.#17 // java/lang/Object.init:()V#2 Class #18 // com/ocean/classloading/TestMain#3 Class #19 // java/lang/Object#4 Utf8 i#5 Utf8 I#6 Utf8 d#7 Utf8 D#8 Utf8 init#9 Utf8 ()V#10 Utf8 Code#11 Utf8 LineNumberTable#12 Utf8 print#13 Utf8 trueOrFalse#14 Utf8 ()Z#15 Utf8 SourceFile#16 Utf8 TestMain.java#17 NameAndType #8:#9 // init:()V#18 Utf8 com/ocean/classloading/TestMain#19 Utf8 java/lang/Object
可以看到常量池中有22项内容其中带Utf8的就是符号引用。比如#2它的值是com/ocean/classloading/TestMain表示的是这个类的全限定名又比如#4为i#5为I它们是一对的表示变量时Integerint类型的名字叫做i#12、#16表示的都是方法的名字。
符号引用就是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的引用的目标未必已经加载到内存中了。
直接引用
直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的同一个符号引用在不同
解析阶段负责把整个类激活串成一个可以找到彼此的网。那这个阶段都做了哪些工作呢大体可以分为
类或接口的解析类方法解析接口方法解析字段解析
初始化
最后一个步骤经过这个步骤后类信息完全进入了jvm内存直到它被垃圾回收器回收。 前面几个阶段都是虚拟机来搞定的。我们也干涉不了从代码上只能遵从它的语法要求。而这个阶段是赋值才是我们应用程序中编写的有主导权的地方
初始化阶段就是执行类构造器()方法的过程。 ()方法并不是程序员在Java代码中直接编写 的方法 它是Javac编译器的自动生成物()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}块 中的 语句合并产生的。
()方法与类的构造函数即在虚拟机视角中的实例构造器()方法 不同 它不需要显 式地调用父类构造器 Java虚拟机会保证在子类的()方法执行前 父类的()方法已经执行 完毕。 因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object。
由于父类的()方法先执行 也就意味着父类中定义的静态语句块要优先于子类的变量赋值 操作
()方法对于类或接口来说并不是必需的 如果一个类中没有静态语句块 也没有对变量的 赋值操作 那么编译器可以不为这个类生成()方法。 接口中不能使用静态语句块 但仍然有变量初始化的赋值操作 因此接口与类一样都会生成 ()方法。 但接口与类不同的是 执行接口的()方法不需要先执行父接口的()方法 因为只有当父接口中定义的变量被使用时 父接口才会被初始化。 此外 接口的实现类在初始化时也 一样不会执行接口的()方法。
Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步 如果多个线程同 时去初始化一个类 那么只会有其中一个线程去执行这个类的()方法 其他线程都需要阻塞等 待 直到活动线程执行完毕()方法。 如果在一个类的()方法中有耗时很长的操作 那就 可能造成多个进程阻塞 在实际应用中这种阻塞往往是很隐蔽的
class TestDeadLoop {static class DeadLoopClass {static {
// 如果不加上这个if语句 编译器将提示“Initializer does not complete normally”并拒绝编译if (true) {System.out.println(Thread.currentThread() init DeadLoopClass);while (true) {}}}}public static void main(String[] args) {Runnable script new Runnable() {public void run() {System.out.println(Thread.currentThread() start);DeadLoopClass dlc new DeadLoopClass();System.out.println(Thread.currentThread() run over);}};Thread thread1 new Thread(script);Thread thread2 new Thread(script);thread1.start();thread2.start();}
}与
上面说的()方法可以理解为是cinit对象的初始化方法构造函数也就是反编译之后看到方法是init这两者什么区别呢 看一段代码
public class ParentA {static {System.out.println(1);}public ParentA() {System.out.println(2);}
}public class SonB extends ParentA {static {System.out.println(a);}public SonB() {System.out.println(b);}public static void main(String[] args) {ParentA ab new SonB();ab new SonB();}}上面的打印结果是
1
a
2
b
2
b其中 static 字段和 static 代码块是属于类的在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区在同一个类加载器下这些信息有一份就够了所以上面的 static 代码块只会执行一次它对应的是 cinit方法。 所以上面代码的 static 代码块只会执行一次对象的构造方法执行两次。再加上继承关系的先后原则不难分析出正确结果 小结 方法cinit的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作) ,只执行一次 方法 init的执行时期: 对象的初始化阶段