艺术网站建设模板,wordpress vip视频,做外贸用什么搜索网站,wordpress获取文章评论前言
前面学习了虚拟机的内存结构、对象的分配和创建#xff0c;但对象所对应的类是怎么加载到虚拟机中来的呢#xff1f;加载过程中需要做些什么#xff1f;什么是双亲委派机制以及为什么要打破双亲委派机制#xff1f;
类的生命周期 类的生命周期包含了如上的7个阶段但对象所对应的类是怎么加载到虚拟机中来的呢加载过程中需要做些什么什么是双亲委派机制以及为什么要打破双亲委派机制
类的生命周期 类的生命周期包含了如上的7个阶段其中验证、准备、解析统称为连接 类的加载主要是前五个阶段每个阶段基本上保持如上顺序开始仅仅是开始实际上执行是交叉混合的只有解析阶段不一定在初始化后也有可能才开始执行解析这是为了支持动态语言。
加载
加载就是将字节码的二进制流转化为方法区的运行时数据结构并生成类所对象的Class对象字节码二进制流可以是我们编译后的class文件也可以从网络中获取或者运行时动态生成动态代理等等。那什么时候会触发类加载呢这个在虚拟机规范中没有明确定义只是规定了何时需要执行初始化稍后详细分析。
验证
这个阶段很好理解就是进行必要的校验确保加载到内存中的字节码是符合要求的主要包含以下四个校验步骤了解即可
文件格式校验这个阶段要校验的东西非常多主要的有下面这些实际上远远不止 是否以魔数0xCAFEBABE开头。主、次版本号是否在当前Java虚拟机接受范围之内。常量池的常量中是否有不被支持的常量类型检查常量tag标志。指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。。。。。。。元数据校验对字节码描述信息进行语义分析。 这个类是否有父类除了java.lang.Object之外所有的类都应当有父类。这个类的父类是否继承了不允许被继承的类被final修饰的类。如果这个类不是抽象类是否实现了其父类或接口之中要求实现的所有方法。类中的字段、方法是否与父类产生矛盾例如覆盖了父类的final字段或者出现不符合规则的方法重载例如方法参数都一致但返回值类型却不同等。。。。。。。字节码校验确保程序没有语法和逻辑错误这是整个验证阶段最复杂的一个步骤。 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作例如不会出现类似于“在操作栈放置了一个 int 类型的数据使用时却按 long 类型来加载入本地变量表中”这样的情况。保证任何跳转指令都不会跳转到方法体以外的字节码指令上。保证方法体中的类型转换总是有效的例如可以把-个子类对象赋值给父类数据类型这是安全的但是把父类对象赋值给子类数据类型甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型则是危险和不合法的。。。。。。。符号引用验证这个阶段发生在符号引用转为直接引用的时候即实际上是在解析阶段中进行的。 符号引用中通过字符串描述的全限定名是否能找到对应的类。在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。符号引用中的类、字段、方法的可访问性( private、 protected. public、 )。是否可被当前类访问。。。。。。。
准备
该阶段是为类变量static分配内存并设置零值即类只要经过准备阶段其中的静态变量就是可使用的了但此时类变量的值还不是我们想要的值需要经过初始化阶段才会将我们希望的值赋值给对应的静态变量。
解析
解析就是将常量池中的符号引用替换为直接引用的过程。符号引用就是一个代号比如我们的名字而这里可以理解为就是类的完全限定名直接引用则是对应的具体的人、物这里就是指目标的内存地址。为什么需要符号引用呢因为类在加载到内存之前还没有分配内存地址因此必然需要一个东西指代它。这个阶段包含了类或接口的解析、字段解析、类方法解析、接口方法解析在解析的过程中可能会抛出以下异常
java.lang.NoSuchFieldError找不到字段java.lang.IllegalAccessError不具有访问权限java.lang.NoSuchMethodError找不到方法
初始化
这是类加载过程中的最后一个步骤主要是收集类的静态变量的赋值动作和static块中的语句合成cinit方法通过该方法根据我们的意愿为静态变量赋值以及执行static块该方法会被加锁确保多线程情况下只有一个线程能初始化成功利用该特性可以实现单例模式。虚拟机规定了有且只有遇到以下情况时必须先确保对应类的初始化完成加载、准备必然在此之前
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时。能够生成这四条指令的典型Java代码场景有 使用new关键字实例化对象的时候。读取或设置一个类型的静态字段被final修饰、已在编译期把结果放入常量池的静态字段除外的时候。调用一个类型的静态方法的时候。反射调用类时。当初始化类的时候如果发现其父类还没有进行过初始化则需要先触发其父类的初始化。当虚拟机启动时用户需要指定一个要执行的主类包含main()方法的那个类虚拟机会先初始化这个主类。当使用JDK 7新加入的动态语言支持时如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄并且这个方法句柄对应的类没有进行过初始化则需要先触发其初始化。当一个接口中定义了JDK 8新加入的默认方法被default关键字修饰的接口方法时如果有这个接口的实现类发生了初始化那该接口要在其之前被初始化。
下面分析几个案例代码读者们可以先思考后再运行代码看看和自己想的是否一样。
案例一
先定义如下两个类
public class SuperClazz {static {System.out.println(SuperClass init);}public static int value123;public static final String HELLOWORLDhello world;public static final int WHAT value;}public class SubClaszz extends SuperClazz {static{System.out.println(SubClass init);}}
然后进行下面的调用
public class Initialization {public static void main(String[]args){Initialization initialization new Initialization();initialization.M1();}public void M1(){System.out.println(SubClaszz.value);}}
第一个案例是通过子类去引用父类中的静态变量两个类都会加载和初始化么打印结果看看
SuperClass init123
可以看到只有父类初始化了那么父类必然是加载了的问题就在于子类有没有被加载呢可以加上参数-XX:TraceClassLoading再执行该参数的作用就是打印被加载了的类可以看到子类是被加载了的。所以通过子类引用父类静态变量父子类都会被加载但只有父类会进行初始化。为什么呢反编译后可以看到生成了如下指令
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic #6 // Field ex7/init/SubClaszz.value:I
6: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
9: return
关键就是getstatic指令就会触发类的初始化但是为什么子类不会初始化呢因为这个变量是来自于父类的为了提高效率所以虚拟机进行了优化这种情况只需要初始化父类就行了。
案例二
调用下面的方法
public void M2(){SubClaszz[]sca new SubClaszz[10];}
执行后可以发现使用数组不会触发初始化但父子类都会被加载。
案例三
public void M3(){System.out.println(SuperClazz.HELLOWORLD);}
引用常量不会触发类的加载和初始化因为常量在编译后就已经存在当前class的常量池。 常量本质含义
静态加final修饰的常量在编译的时能确定其值时就被放置该方法所属类的常量池中即使将常量所在类的字节码文件删除也不会影响常量的使用所以常量所在的类也不会被初始化。但是当常量值不确定时如用UUID生成时只能在运行时才确定时常量所在的类就会被初始化
案例四
public void M4(){System.out.println(SubClaszz.WHAT);}
通过常量去引用其它的静态变量会发生什么呢这个和案例一结果是一样的。
类加载器 命名空间
表示的是一个范围指加载器自己和所有父加载器加载的类加载器指的是具体的对象
子命名空间中类对象可以看到父命名空间的类对象反之则不行
父子加载器其实是包含的关系
每个类加载器都有自己的命名空间的一个命名空间中同一个类只能有一个就是说加载前判断类之前有没有加载过
同一个类被不同命名空间的加载器加载时同时加载到内存的类是属于不同的类型的
自定义的类加载器当没有包含父加载器时默认的父加载器是系统加载器
怎样确定一个类
包名加类名的形式
类的卸载
只有字节码对象没有被引用时类在方法区中就会被卸载而java虚拟机自带的类加载器在加载类时内部有集合里存放被加载类的引用所以虚拟机运行期间是不会被卸载的。而自定义的类加载器加载的类是有可能被卸载的
特性
在类里有对其他类的使用时其他类的加载器是只能用这个类加载器及其父类进行加载
其他加载器都是由根加载器进行加载的而根加载器是内嵌在java虚拟机的随虚拟机启动进行初始化的
应用类加载器可以通过设置系统属性Java.system.class.loader来设置其他加载器作为应用类加载器 类加载器和双亲委派模型
在我们平时开发中确定一个类需要通过完全限定名而不能简单的通过名字因为在不同的路径下我们是可以定义同名的类的。那么在虚拟机中又是怎么区分类的呢在虚拟机中需要类加载器完全限定名一起来指定一个类的唯一性即相同限定名的类若由两个不同的类加载器加载那虚拟机就不会把它们当做一个类。从这里我们可以看出类加载器一定是有多个的那么不同的类加载器是怎么组织的它们又分别需要加载哪些类呢 从虚拟角度看只有两种类型的类加载器启动类加载器BootstrapClassLoader和非启动类加载器。前者是C实现属于虚拟机的一部分后者则是由Java实现的独立于虚拟机的外部并且全部继承自抽象类java.lang.ClassLoader。但从Java本身来看一直保持着三层类加载器、双亲委派的结构当然除了Java本身提供的三层类加载器我们还可以自定义实现类加载器。如上图上面三个就是原生的类加载器每一个都是下一个类加载器的父加载器注意这里都是采用组合而非继承。当开始加载类时首先交给父加载器加载父加载器加载了子加载器就不用再加载了而若是父加载器加载不了就会交给子加载器加载这就是双亲委派机制。这就好比工作中遇到了无法处理的事你会去请示直接领导直接领导处理不了再找上层领导然后上层领导觉得这是个小事不用他亲自动手就让你的直接领导去做接着他又交给你去做等等。下面来看看每个类加载器的具体作用
BootstrapClassLoader启动类加载器顾名思义这个类加载器主要负责加载JDK lib包以及-Xbootclasspath参数指定的目录并且虚拟机对文件名进行了限定也就是说即使我们自己写个jar放入到上述目录也不会被加载。由于该类加载器是C使用所以我们的Java程序中无法直接引用调用java.lang.ClassLoader.getClassLoader()方法时默认返回的是null。ExtClassLoader扩展类加载器主要负责加载JDK lib/ext包以及被系统变量java.ext.dirs指向的所有类库这个类库可以存放我们自己写的通用jar。AppClassLoader应用程序类加载器负责加载用户classpath上的所有类。它是java.lang.ClassLoader.getSystemClassLoader()的返回值也是我们程序的默认类加载器如果我们没有自定义类加载器的话。
通过这三个类加载以及双亲委派机制一个显而易见的好处就是不同的类随它的类加载器天然具有了加载优先级像Object、String等等这些核心类库自然就会在我们的应用程序类之前被加载保证了系统的安全性用户自定义类加载器不可能去加载由父加载器去加载的基础可靠类避免了基础代码的替换和被覆盖Spring的父子容器也是这样的一个设计。通过下面这段代码可以看到每个类所对应的类加载器
public class ClassLoader {public static void main(String[] args) {System.out.println(String.class.getClassLoader()); //启动类加载器System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());//拓展类加载器System.out.println(ClassLoader.class.getClassLoader());//应用程序类加载器}}
输出
null
sun.misc.Launcher$ExtClassLoader4b67cf4d
sun.misc.Launcher$AppClassLoader14dad5dc 破坏双亲委派模型
刚刚我举了工作中的一个例子来说明双亲委派机制但现实中我们不需要事事都去请示领导同样类加载器也不是完全遵循双亲委派机制在必要的时候是可以打破这个规则的。下面列举四个破坏的情况在此之前我们需要先了解下双亲 委派的代码实现原理在java.lang.ClassLoader类中有一个loadClass以及findClass方法 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;}}protected Class? findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);}
从上面可以看到首先是调用parent去加载类没有加载到才调用自身的findClass方法去加载。也就是说用户在实现自定义类加载器的时候需要覆盖的是fiindClass而不是loadClass这样才能满足双亲委派模型。下面具体来看看破坏双亲委派的几个场景。
第一次
第一次破坏是在双亲委派模型出现之前 因为该模型是在JDK1.2之后才引入的那么在此之前抽象类java.lang.ClassLoader就已经存在了用户自定义的类加载器都会去覆盖该类中的loadClass方法所以双亲委派模型出现后就无法避免用户覆盖该方法因此新增了findClass引导用户去覆盖该方法实现自己的类加载逻辑。
SPI
第二次破坏是由于这个模型本身缺陷导致的因为该模型保证了类的加载优先级但是有些接口是Java定义在核心类库中但具体的服务实现是由用户提供的这时候就不得不破坏该模型才能实现典型的就是Java中的SPI机制SPI服务提供接口一般是java制定标准接口再由外部厂商进行实现这种情况下接口是由根加载器加载实现类的jar包是放在classpath中由应用加载器进行加载导致引用看不到对象的情况如jdbc的应用。
DBC的驱动加载就是SPI实现的所以直接看到java.sql.DriverManager类核心类该类中有一个静态初始化块 static {loadInitialDrivers();println(JDBC DriverManager initialized);}private static void loadInitialDrivers() {String drivers;try {drivers AccessController.doPrivileged(new PrivilegedActionString() {public String run() {return System.getProperty(jdbc.drivers);}});} catch (Exception ex) {drivers null;}AccessController.doPrivileged(new PrivilegedActionVoid() {public Void run() {ServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class);IteratorDriver driversIterator loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}} catch(Throwable t) {// Do nothing}return null;}});println(DriverManager.initialize: jdbc.drivers drivers);if (drivers null || drivers.equals()) {return;}String[] driversList drivers.split(:);println(number of Drivers: driversList.length);for (String aDriver : driversList) {try {println(DriverManager.Initialize: loading aDriver);Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println(DriverManager.Initialize: load failed: ex);}}}
主要看ServiceLoader.load方法这个就是通过SPI去加载我们引入java.sql.Driver实现类比如引入mysql的驱动包就是com.mysql.cj.jdbc.Driver public static S ServiceLoaderS load(ClassS service) {ClassLoader cl Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);}
这个方法主要是从当前线程中获取类加载器然后通过这个类加载器去加载驱动实现类这个叫线程上下文类加载器我们也可以使用这个技巧去打破双亲委派那这里会获取到哪一个类加载器呢具体的设置是在sun.misc.Launcher类的构造器中 public Launcher() {Launcher.ExtClassLoader var1;try {var1 Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError(Could not create extension class loader, var10);}try {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);if (var2 ! null) {SecurityManager var3 null;if (!.equals(var2) !default.equals(var2)) {try {var3 (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 new SecurityManager();}if (var3 null) {throw new InternalError(Could not create SecurityManager: var2);}System.setSecurityManager(var3);}}
可以看到设置的就是AppClassLoader。你可能会有点疑惑这个类加载器加载类的时候不也是先调用父类加载器加载么怎么就打破双亲委派了呢其实打破双亲委派指的就是类的层次结构延伸意思就是类的加载优先级这里本应该是在加载核心类库的时候却提前将我们应用程序中的类库给加载到虚拟机中来了。 当前类加载器(Current classloader)
每个类都会使用自己的类加载器即加载自身的类加载器来去加载其他类指的是所依赖的类)。如果classX引用了classY那么ClassX的类加载器就会去加载classY(前提是classY尚未被加载)。 在双亲委托模型下类加载是由下至上的即下层的类加载器会委托上层进行加载。但是对于SPI来说有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的而这些接口的实现却来自于不同的jar包(厂商提供) Java的启动类加载器是不会加载其他来源的jar包这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器就可以由设置的上下文类加载器来实现对于接口实现类的加载。 上下文加载器(线程中默认使用)
可以通过Thread中getContextClassload()和setContextClassload(Classloader class)进行获得和设置
机制若是没有手动进行设置上下文加载器是自动继承父线程的上下文加载器的java应用初始上下文加载器是应用加载器后面几乎所有线程都可以用该加载器进行加载类和资源
上下文加载的类可以被父加载器加载的类进行使用
怎么使用(一般写底层框架时使用)
首先是获取上下文加载器
再是可以将上下文加载器设置成你所需要的加载器进行加载相关的类
最后还原成第一步获取的默认加载器
Tomcat 上图是Tomcat类加载的类图前面三个不用说CommonClassLoader、CatalinaClassLoader、SharedClassLoader、WebAppClassLoader、JspClassLoader则是Tomcat自己实现的类加载器分别加载common包、server包、shared包、WebApp/WEB-INF/lib包以及JSP文件前面三个在tomcat 6之后已经合并到根目录下的lib目录下。而WebAppClassLoader则是每一个应用程序对应一个JspClassLoader是每一个JSP文件都会对应一个并且这两个类加载器都没有父类加载器这也就违背了双亲委派模型。为什么每个应用程序需要单独的WebAppClassLoader实例因为每个应用程序需要彼此隔离假如在两个应用中定义了一样的类完全限定名如果遵循双亲委派那就只会存在一份了另外不同的应用还有可能依赖同一个类库的不同版本这也需要隔离所以每一个应用程序都会对应一个WebAppClassLoader它们共享的类库可以让SharedClassLoader加载另外这些类加载加载的类对Tomcat本身来说也是隔离的CatalinaClassLoader加载的。为什么每个JSP文件需要对应单独的一个JspClassLoader实例这是由于JSP是支持运行时修改的修改后会丢弃掉之前编译生成的class并重新生成一个JspClassLoader实例去加载新的class。以上就是Tomcat为什么要打破双亲委派模型的原因。
OSGI
OSGI是用于实现模块热部署像Eclipse的插件系统就是利用OSGI实现的这个技术非常复杂同时使用的也越来越少了感兴趣的读者可自行查阅资料学习这里不再进行阐述。
总结
类加载的过程让我们了解到一个类是如何被加载到内存中需要经过哪些阶段而类加载器和双亲委派模型则是告诉我们应该怎么去加载类、类的加载优先级是怎样的其中的设计思想我们也可以学习借鉴最后需要深刻理解的是为什么需要打破双亲委派在遇到相应的场景时应该怎么做。