网站怎样做域名绑定,wordpress教程 pdf下载,网站建设时间安排表,h5调用小程序api1 类加载的过程 1.1 加载
“加载”是“类加载”(Class Loading)过程的第一步。这个加载过程主要就是靠类器实现的#xff0c;包括用户自定义类加载器。 加载的过程 在加载的过程中#xff0c;JVM主要做3件事情 1#xff09;通过一个类的全限定名来获取定义此类的二进制字节…1 类加载的过程 1.1 加载
“加载”是“类加载”(Class Loading)过程的第一步。这个加载过程主要就是靠类器实现的包括用户自定义类加载器。 加载的过程 在加载的过程中JVM主要做3件事情 1通过一个类的全限定名来获取定义此类的二进制字节流(class文件)在程序运行过程中当要访问一个类时若发现这个类尚未被加载并满足类初始化的条件时就根据要被初始化的这个类的全限定名找到该类的二进制字节流开始加载过程
2将这个字节流的静态存储结构转化为方法区的运行时数据结构
3在内存中创建一个该类的java. lang. Class对象作为方法区该类的各种数据的访问入口 程序在运行中所有对该类的访问都通过这个类对象也就是这个Class对象是提供给外界访问该类的接口。
加载源 JVM规范对于加载过程给予了较大的宽松度.一般二进制字节流都从已经编译好的本地class文件中读取此外还可以从以下地方读取。 1zip包 Jar、War、Ear等 2其它文件生成 由JSP文件中生成对应的Class类. 3数据库中 将二进制字节流存储至数据库中然后在加载时从数据库中读取.有些中间件会这么做用来实现代码在集群间分发 4网络 从网络中获取二进制字节流.典型就是Applet. 5运行时计算生成 动态代理技术用ProxyGenerator.generateProxyClass为特定接口生成形式为*$Proxy的代理类的二进制字节流.
类和数组加载的区别 数组也有类型称为“数组类型”.如:
String[] str new String[10];这个数组的数组类型是[Ljava.lang.String 而String只是这个数组的元素类型。
数组类和非数组类的类加载是不同的具体情况如下 非数组类 是由类加载器来完成。 数组类 数组类本身不通过类加载器创建它是由java虚拟机直接创建但数组类与类加载器有很密切的关系因为数组类的元素类型最终要靠类加载器创建。
加载过程的注意点 1JVM规范并未给出类在方法区中存放的数据结构 类完成加载后二进制字节流就以特定的数据结构存储在方法区中但存储的数据结构是由虚拟机自己定义的虚拟机规范并没有指定。
2JVM规范并没有指定Class对象存放的位置 在二进制字节流以特定格式存储在方法区后JVM会创建一个java.lang.Class类的对象作为本类的外部访问接口。 既然是对象就应该存放在Java堆中不过JVM规范并没有给出限制不同的虚拟机根据自己的需求存放这个对象。 HotSpot将Class对象存放在方法区。
3加载阶段和链接阶段是交叉的 类加载的过程中每个步骤的开始顺序都有严格限制但每个步骤的结束顺序没有限制。也就是说类加载过程中 必须按照如下顺序开始: 加载 - 链接 - 初始化
但结束顺序无所谓因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。
1.2 验证
验证阶段比较耗时它非常重要但不一定必要(因为对程序运行期没有影响)如果所运行的代码已经被反复使用和验证过那么可以使用 -Xverify:none参数关闭以缩短类加载时间。
验证的目的 保证二进制字节流中的信息符合虚拟机规范并没有安全问题。
验证的必要性 虽然Java语言是一门安全的语言它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行.也就是说Java语言的安全性是通过编译器来保证的.
但是我们知道编译器和虚拟机是两个独立的东西虚拟机只认二进制字节流它不会管所获得的二进制字节流是哪来的当然如果是编译器给它的那么就相对安全但如果是从其它途径获得的那么无法确保该二进制字节流是安全的。
通过上文可知虚拟机规范中没有限制二进制字节流的来源在字节码层面上上述Java代码无法做到的都是可以实现的至少语义上是可以表达出来的为了防止字节流中有安全问题需要验证
验证的过程 1文件格式验证 验证字节流是否符合Class文件格式的规范并且能被当前的虚拟机处理. 本验证阶段是基于二进制字节流进行的只有通过本阶段验证才被允许存到方法区 后面的三个验证阶段都是基于方法区的存储结构进行不会再直接操作字节流。
印证【加载和验证】是交叉进行的 1.加载开始前二进制字节流还没进方法区而加载完成后二进制字节流已经存入方法区 2.而在文件格式验证前二进制字节流尚未进入方法区文件格式验证通过之后才进入方法区也就是说加载开始后立即启动了文件格式验证本阶段验证通过后二进制字节流被转换成特定数据结构存储至方法区中继而开始下阶段的验证和创建Class对象等操作
2元数据验证 对字节码描述信息进行语义分析确保符合Java语法规范.
3字节码验证 本阶段是验证过程的最复杂的一个阶段。
本阶段对方法体进行语义分析保证方法在运行时不会出现危害虚拟机的事件。
字节码验证将对类的方法进行校验分析保证被校验的方法在运行时不会做出危害虚拟机的事一个类方法体的字节码没有通过字节码验证那一定有问题但若一个方法通过了验证也不能说明它一定安全。
4符号引用验证 发生在JVM将符号引用转化为直接引用的时候这个转化动作发生在解析阶段对类自身以外的信息进行匹配校验确保解析能正常执行。
1.3 准备
仅仅为类变量即static修饰的字段变量分配内存并且设置该类变量的初始值即零值这里不包含用final修饰的static因为final在编译的时候就会分配了编译器的优化同时这里也不会为实例变量分配初始化。类变量会分配在方法区中 而实例变量是会随着对象一起分配到Java堆中。
准备阶段主要完成两件事情 1为已在方法区中的类的静态成员变量分配内存 2为静态成员变量设置初始值 初始值为0、 false、 null等 比如
public static int x 1000;注意 实际上变量x在准备阶段过后的初始值为0而不是1000 将x赋值为1000的putstatic指令是程序被编译后存放于类构造器方法之中
但是如果声明为 public static final int x 1000; 在编译阶段会为x生成ConstantValue属性在准备阶段虚拟机会根据ConstantValue属性将x赋值为1000。
1.4 解析
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四种常量类型。
1. 类或接口的解析 判断所要转化成的直接引用是对数组类型还是普通的对象类型的引用从而进行不同的解析。
2. 字段解析 对字段进行解析时会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段如果有则查找结束如果没有则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口还没有则按照继承关系从上往下递归搜索其父类直至查找结束(优先从接口来然后是继承的父类.理论上是按照上述顺序进行搜索解析但在实际应用中虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中或同时在自己或父类的接口中出现编译器可能会拒绝编译.
3. 类方法解析 对类方法的解析与对字段解析的搜索步骤差不多只是多了判断该方法所处的是类还是接口的步骤而且对类方法的匹配搜索是先搜索父类再搜索接口。
4. 接口方法解析 与类方法解析步骤类似只是接口不会有父类因此只递归向上搜索父接口就行了。
1.5 初始化
初始化是类加载过程的最后一步到了此阶段才真正开始执行类中定义的Java程序代码(初始化成为代码设定的默认值)。在准备阶段类变量已经被赋过一次系统要求的初始值而在初始化阶段则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源或者可以从另一个角度来表达初始化阶段是执行类构造器()方法的过程。
其实初始化过程就是调用类初始化方法的过程完成对static修饰的类变量的手动赋值还有主动调用静态代码块如果这两个不存在的话就没有初始化过程。
初始化过程的注意点: 1方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的编译器收集的顺序是由语句在源文件中出现的顺序所决定的. 2静态代码块只能访问到出现在静态代码块之前的变量定义在它之后的变量在前面的静态语句块可以赋值但是不能访问.
public class Test {static {i0; System.out.println(i);//编译失败:非法向前引用 }static int i 1;
}3实例构造器需要显式调用父类构造函数而类的不需要调用父类的类构造函数虚拟机会确保子类的方法执行前已经执行完毕父类的方法.因此在JVM中第一个被执行的方法的类肯定是java.lang.Object.
4如果一个类/接口中没有静态代码块也没有静态成员变量的赋值操作那么编译器就不会为此类生成方法.
5接口也需要通过方法为接口中定义的静态成员变量显示初始化。 接口中不能使用静态代码块但仍然有变量初始化的赋值操作因此接口与类一样都会生成方法.不同的是执行接口的方法不需要先执行父接口的方法.只有当父接口中的静态成员变量被使用到时才会执行父接口的方法.
6虚拟机会保证在多线程环境中一个类的方法别正确地加锁同步.当多条线程同时去初始化一个类时只会有一个线程去执行该类的方法其它线程都被阻塞等待直到活动线程执行方法完毕.其他线程虽会被阻塞只要有一个方法执行完其它线程唤醒后不会再进入方法.同一个类加载器下一个类型只会初始化一次.
使用静态内部类的单例实现
public class Student {private Student() {}/** 此处使用一个内部类来维护单例 JVM在类加载的时候是互斥的所以可以由此保证线程安全问题*/private static class SingletonFactory {private static Student student new Student();}/* 获取实例 */public static Student getSingletonInstance() {return SingletonFactory.student;}
}2. 类加载的时机
什么时候开始加载虚拟机规范并没有强制性的约束对于其它大部分阶段究竟何时开始虚拟机规范也都没有进行规范这些都是交由虚拟机的具体实现来把握。所以不同的虚拟机它们开始的时机可能是不同的。但是对于初始化却严格的规定了有且只有四种情况必须先对类进行“初始化”(加载验证准备自然需要在初始化之前完成)
遇到new、getstatic、putstatic和invokestatic这四条指令时如果对应的类没有初始化则要对对应的类先进行初始化。
这四个指令对应到我们java代码中的场景分别是
new关键字实例化对象的时候读取或设置一个类的静态字段读取被final修饰已在编译器把结果放入常量池的静态字段除外 调用类的静态方法时。 使用java.lang.reflect包方法时对类进行反射调用的时候。 初始化一个类的时候发现其父类还没初始化要先初始化其父类。 当虚拟机开始启动时用户需要指定一个主类虚拟机会先执行这个主类的初始化。
3. 类加载器 启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的或通过-Xbootclasspath参数指定路径中的且被虚拟机认可按文件名识别如rt.jar的类。由C实现不是ClassLoader子类 扩展类加载器(Extension ClassLoader)负责加载 JAVA_HOME\lib\ext 目录中的或通过java.ext.dirs系统变量指定路径中的类库。 应用程序类加载器(Application ClassLoader)负责加载用户路径classpath上的类库。
JVM的类加载是通过ClassLoader及其子类来完成的 类的层次关系和加载顺序可以由下图来描述 加载过程中会先检查类是否被已加载检查顺序是自底向上从Custom ClassLoader到BootStrapClassLoader逐层检查只要某个classloader已加载就视为已加载此类保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下也就是由上层来逐层尝试加载此类。
3.1 自定义类加载器
自定义类加载器步骤 1继承ClassLoader 2重写findClass方法 3调用defineClass方法
实践 下面写一个自定义类加载器指定类加载路径在D盘下的lib文件夹下。 1在本地磁盘新建一个 Test.java类代码如下
package jvm.classloader;
public class Test {public void say(){System.out.println(Hello MyClassLoader);}
}2使用 javac -d . Test.java命令将生成的Test.class文件放到D:/lib/jvm/classloader文件夹下。
3在Eclipse中自定义类加载器代码如下
package jvm.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;public class MyClassLoader extends ClassLoader{private String classpath;public MyClassLoader(String classpath) {this.classpath classpath;}Overrideprotected Class? findClass(String name) throws ClassNotFoundException{try {byte [] classDategetData(name);if(classDatenull){}else{//defineClass方法将字节码转化为类return defineClass(nameclassDate0classDate.length);}} catch (IOException e) {e.printStackTrace();}return super.findClass(name);}//返回类的字节码private byte[] getData(String className) throws IOException{InputStream in null;ByteArrayOutputStream out null;String pathclasspath File.separatorChar className.replace(.File.separatorChar).class;try {innew FileInputStream(path);outnew ByteArrayOutputStream();byte[] buffernew byte[2048];int len0;while((lenin.read(buffer))!-1){out.write(buffer0len);}return out.toByteArray();}catch (FileNotFoundException e) {e.printStackTrace();}finally{in.close();out.close();}return null;}
}测试代码如下(在Eclipse中)
package jvm.classloader;
import java.lang.reflect.Method;
public class TestMyClassLoader {public static void main(String []args) throws Exception{ //自定义类加载器的加载路径MyClassLoader myClassLoadernew MyClassLoader(D:\\lib); //包名类名Class cmyClassLoader.loadClass(jvm.classloader.Test);if(c!null){Object objc.newInstance();Method methodc.getMethod(say null); method.invoke(obj null); System.out.println(c.getClassLoader().toString()); }}
} 输出结果如下 自定义类加载器的作用 JVM自带的三个加载器只能加载指定路径下的类字节码。 如果某个情况下我们需要加载应用程序之外的类文件呢比如本地D盘下的或者去加载网络上的某个类文件这种情况就可以使用自定义加载器了
4. 双亲委派模型
JVM通过双亲委派模型进行类的加载当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。 1当一个类加载器收到类加载任务会先交给其父类加载器去完成因此最终加载任务都会传递到顶层的启动类加载器 2只有当父类加载器无法完成加载任务时才会尝试执行加载任务。
采用双亲委派的一个好处是 比如加载位于rt.jar包中的类java.lang.Object不管是哪个加载器加载这个类最终都是委托给顶层的启动类加载器进行加载这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。
为什么要使用双亲委托这种模型呢 因为这样可以避免重复加载当父亲已经加载了该类的时候就没有必要子ClassLoader再加载一次。
考虑到安全因素我们试想一下如果不使用这种委托模式那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型这样会存在非常大的安全隐患而双亲委托的方式就可以避免这种情况因为String已经在启动时就被引导类加载器 Bootstrcp ClassLoader 加载 所以用户自定义的ClassLoader永远也无法加载一个自己写的String 除非你改变JDK中ClassLoader搜索类的默认算法。
但是JVM在搜索类的时候 又是如何判定两个class是相同的呢 JVM在判定两个class是否相同时不仅要判断两个类名是否相同而且要判断是否由同一个类加载器实例加载的。
只有两者同时满足的情况下JVM才认为这两个class是相同的。就算两个class是同一份class字节码如果被两个不同的ClassLoader实例所加载JVM也会认为它们是两个不同class。
既然JVM已经提供了默认的类加载器 为什么还要定义自已的类加载器呢 因为Java中提供的默认ClassLoader只加载指定目录下的jar和class如果我们想加载其它位置的类或jar时。
比如我要加载网络上的一个class文件通过动态加载到内存之后要调用这个类中的方法实现我的业务逻辑。在这样的情况下默认的ClassLoader就不能满足我们的需求了所以需要定义自己的ClassLoader
5. 破坏双亲委派模型
为什么需要破坏双亲委派 因为在某些情况下父类加载器需要加载的class文件由于受到加载范围的限制父类加载器无法加载到需要的文件这个时候就需要委托子类加载器进行加载。
而按照双亲委派模式的话是子类委托父类加载器去加载class文件。这个时候需要破坏双亲委派模式才能加载成功父类加载器需要的类。也就是说父类会委托子类去加载它需要的class文件。
以Driver接口为例由于Driver接口定义在jdk当中的而其实现由各个数据库的服务商来提供比如mysql的就写了 MySQL Connector这些实现类都是以jar包的形式放到classpath目录下。
那么问题就来了DriverManager也由jdk提供要加载各个实现了Driver接口的实现类classpath下然后进行管理但是DriverManager由启动类加载器加载只能加载JAVA_HOME的lib下文件而其实现是由服务商提供的由系统类加载器加载这个时候就需要启动类加载器来委托子类来加载Driver实现从而破坏了双亲委派这里仅仅是举了破坏双亲委派的其中一个情况。(让本来由启动类加载器加载Driver改为由应用程序加载器加载Driver类原因DriverManager出现在Java1.0双亲委派出现在1.2,为了兼容老版本所以出现了破坏双亲委派模型) JDBC代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.junit.Test;
public class TestJdbc {Testpublic void testJdbc() {Connection connection null;PreparedStatement preparedStatement null;ResultSet rs null;try {// 加载数据库驱动Class.forName(com.mysql.jdbc.Driver);// 通过驱动管理类获取数据库链接connection DriverManagerconnection DriverManager.getConnection(jdbc:mysql://localhost:3306/ssm?characterEncodingutf-8 rootroot);// 定义sql语句 ?表示占位符String sql select * from user where id ?;// 获取预处理 statementpreparedStatement connection.prepareStatement(sql);// 设置参数第一个参数为 sql 语句中参数的序号从 1 开始第二个参数为 设置的preparedStatement.setint(1 1);// 向数据库发出 sql 执行查询查询出结果集 rs preparedStatement.executeQuery(); // 遍历查询结果集}catch (Exception e) {e.printStackTrace();}finally {// 释放资源 if (rs ! null) { try { rs.close();}catch (SQLException e) {e.printStackTrace();}}if (preparedStatement ! null) {try {preparedStatement.close();}catch (SQLException e) {e.printStackTrace();}}if (connection ! null) {try {connection.close();}catch (SQLException e) {}}
}
}
}