一,JVM和类
当我们调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。
当系统出现以下几种情况时,JVM进程将被终止。
二,类的加载,连接和初始化
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这3个步骤,所以有时也把这3个步骤统称为类加载或类初始化。
1.类的加载
类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
2.类的连接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。
3.类的初始化
在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态Field进行初始化。在Java类中对静态Field指定初始值有两种方式:① 声明静态Field时指定初始值;② 使用静态初始化块为静态Field指定初始值。
JVM初始化一个类包含如下几个步骤。
当执行第2个步骤时,系统对直接父类的初始化步骤也遵循此步骤1~3;如果该直接父类又有直接父类,则系统再次重复这3个步骤来先初始化这个父类……依此类推,所以JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。
类初始化的时机
当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口。
除此之外,下面的几种情形需要特别指出。
三,类加载器
类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。现在的问题是,怎么样才算“同一个类”?正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、kl)。这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构。
Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。
Extension Classloader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR包的类。通过这种方式,就可以为Java扩展核心类以外的新功能,只要我们把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。
System Classloader被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以系统类加载器作为父加载器。
类加载机制,JVM的类加载机制主要有如下3种。
注意:类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。
除了可以使用Java提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader来实现。
类加载器加载Class大致要经过如下8个步骤。
其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。
四,创建并使用自定义的类加载器
JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过继承ClassLoader抽象类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。ClassLoader类有如下两个关键方法。 loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。findClass(String name):根据二进制名称来查找类。
如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,当然我们推荐重写findClass()方法,而不是重写loadClass()方法。loadClass()方法的执行步骤如下:
从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
在ClassLoader里还有一个核心方法:Class defineClass(String name, byte[] b, intoff, int len),该方法负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等。defineClass()方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心,程序员无须重写该方法。事实上,该方法是final型,即使我们想重写也没有机会。
除此之外,ClassLoader里还包含如下一些普通方法。
package com.example.demo.ClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
// 自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载机制。
// 这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样即可通过该ClassLoader直接运行Java源文件。
public class CompileClassLoader extends ClassLoader {
// 读取一个文件的内容
private byte[] getBytes(String fileName) {
File file = new File(fileName);
long len = file.length();
byte[] raw = new byte[(int) len];
try (FileInputStream fileInputStream = new FileInputStream(file)) {
// 一次读取Class文件的全部二进制数据
int r = fileInputStream.read(raw);
if (r != len) {
throw new IOException("无法读取全部文件: " + r + " != " + len);
}
} catch (Exception e) {
e.printStackTrace();
}
return raw;
}
//定义编译指定Java文件的方法
private boolean compile(String javaFile) throws IOException {
System.out.println("CompileClassLoader:正在编译" + javaFile + "...");
// 调用系统的javac命令
Process p = Runtime.getRuntime().exec("javac " + javaFile);
try {
// 其它线程都等待这个线程完成
p.waitFor();
}catch (Exception e){
System.out.println(e);
}
// 获取javac线程的退出值
int ret = p.exitValue();
// 返回编译是否成功
return ret == 0;
}
// 重写ClassLoader的findClass方法
protected Class> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
// 将包路径中的点(.)替换成斜线(/)
String fileStub = name.replace(".","/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// 当指定Java源文件存在,且class文件不存在,或者Java源文件的修改时间比class文件的修改时间更晚时,重新编译
if (javaFile.exists() && !classFile.exists() || javaFile.lastModified() > classFile.lastModified()) {
try {
// 如果编译失败,或者该class文件不存在
if(!compile(javaFilename) || !classFile.exists()){
throw new ClassNotFoundException("ClassNotFoundException: " +javaFilename);
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
// 如果class文件存在,系统负责将该文件转换成class对象
if (classFile.exists()) {
// 将class文件的二进制数据读入数组
byte[] raw = getBytes(classFilename);
// 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
clazz = defineClass(name,raw,0,raw.length);
}
// 如果clazz为null,表明加载失败,则抛出异常
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定义一个主方法
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
// 如果运行该程序时没有参数,既没有目标类
if (args.length < 1) {
System.out.println("缺失目标类,请按如下格式运行Java源文件: ");
System.out.println("java CompileClassLoader ClassName");
}
// 第一个参数是需要运行的类
String progClass = args[0];
// 剩下的参数将作为运行目标类时的参数,将这些参数复制到一个新数组中
String[] progArgs = new String[args.length - 1];
// 原数组 从元数据的起始位置开始 目标数组 目标数组的开始起始位置 要copy的数组的长度
System.arraycopy(args,1,progArgs,0,progArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加载需要运行的类
Class> clazz = ccl.loadClass(progClass);
// 获取需要运行的类的主方法
Method main = clazz.getMethod("main", String[].class);
Object argsArray[] = {progArgs};
main.invoke(null,argsArray);
}
}
Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处是父类,就是指类与类之间的继承关系)。URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。实际上,在应用程序中可以直接使用URLClassLoader来加载类,URLClassLoader类提供了如下两个构造器。 URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。
package com.example.demo.ClassLoader;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.util.Properties;
// 示范了如何直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接。通过这种方式来获取数据库连接,可以无须将MySQL驱动添加到CLASSPATH环境变量中。
public class URLClassLoaderTest {
private static Connection conn;
// 定义一个获取数据库连接的方法
public static Connection getConn(String url, String user, String pass) throws Exception {
if (conn == null) {
// 创建一个URL数组
URL[] urls = {new URL("file:mysql-connector-java-3.1.10-bin.jar")};
// 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
URLClassLoader myClassLoader = new URLClassLoader(urls);
// 加载MySQL的JDBC驱动,并创建默认实例
Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();
// 创建一个设置JDBC连接属性的Properties对象
Properties props = new Properties();
// 至少需要为该对象传入user和password两个属性
props.setProperty("user", user);
props.setProperty("password", pass);
// 调用Driver对象的connect方法来取得数据库连接
conn = driver.connect(url, props);
}
return conn;
}
public static void main(String[] args) throws Exception {
System.out.println(getConn("jdbc:mysql://localhost:3306/my_demo?characterEncoding=UTF-8&useSSL=false&useUnicode=true&allowMultiQueries=true&autoReconnect=true&serverTimezone=GMT%2B8",
"root","123456"));
}
}
文章内容来源至《疯狂Java讲义(第二版)》