java面向对象--类加载器及Class对象

时间:2022-09-06 17:25:24

类加载器

jvm 和 类的关系

当调用 java命令运行一个java程序时,会启动一个java虚拟机进程。同一个jvm的所有线程、所有变量都处于同一个进程里,都使用该jvm进程的内存区。

jvm进程终止的情况:
1.程序运行到最后正常结束。
2.遇到System.exit()或Runtime.getRuntime.exit()。
3.遇到未捕获的异常或错误
4.程序所在的平台强制结束了JVM进程
jvm进程终止,jvm内存中的数据将全部丢失。

类加载

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会进行类加载。类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class的实例。因为java中万物皆为对象,类也是java.lang.Class类型的对象。

类加载具体有加载、连接和初始化3个步骤,加载阶段需要完成的事情有:

  1)通过一个类的全限定名来获取定义此类的二进制字节流。

  2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3)在java堆中生成一个代表这个类的Class对象,作为访问方法区中这些数据的入口。

java面向对象--类加载器及Class对象

类的加载,是由类加载器来完成的,类加载器通常由JVM提供。通过使用不同的类加载器,可以加载以下不同来源的类的二进制数据:

  1.从本地文件系统加载class文件
  2.从jar包中直接加载class文件
  3.通过网络加载class文件
  4.把一个java源文件动态编译并加载

类的连接

负责把类的二进制数据合并到JRE(java运行环境)中
  1.验证:检测被加载的类是否有正确的内部结构,是否被破坏或包含不良代码,并和其他类协调一致
  2.准备:负责为类的静态属性分配内存,并设置默认初始值
  3.解析:将类的二进制数据中的符号引用替换成直接引用

类初始化

主要对类的静态属性进行初始化。可以在声明静态属性时指定初始值和通过静态初始化块指定初始值,JVM会按照这些语句在程序中的顺序依次执行。

类初始化的时机

java虚拟机规范没有强制性约束在什么时候开始类加载的过程,由虚拟机的具体实现*把握,但是对于类的初始化,虚拟机规范则严格规定了几种情况,在类没有进行过初始化时必须先触发其初始化。
  1.创建类的实例时:通过new操作符、通过反射或通过反序列化的方式创建实例
  2.使用类的静态方法
  3.访问类的静态变量(除被final修饰的静态变量),或为静态属性赋值
  4.通过反射创建某类或接口的java.lang.Class对象,如Class.forName("Hello")。注意ClassLoader.loadClass(name,false)方法中不会链接类,只是装载该类,所以不会执行初始化。但使用Class的forName()静态方法,才会导致强制初始化该类。
  5.初始化某类的子类时,该子类的所有父类都会被初始化。
  6.main()方法所在的主类最先会被初始化。

对于final
类型的静态属性,如果在编译时(转成.class文件)就可以确定属性值,那么这个属性可被当成编译时常量。编译时,直接替换成具体的值。所以,即使使用这个静态属性,也不会导致该类的初始化,相当于使用常量!!

以上情况称为对一个类进行“主动引用”。除此之外,均不会触发类的初始化,称为“被动引用”,比如以下几种情况:

  1.子类访问父类的静态变量,子类不一定会被初始化,父类会被初始化。

  2.通过数组定义来引用类,不会触发类的初始化。

  3.访问类的常量,常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

class SuperClass {
static {
System.out.println("superclass init");
}
public static int value = 111;
} class SubClass extends SuperClass {
static {
System.out.println("subclass init");
}
} public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);// 被动引用,子类不会被初始化
SubClass[] scArr = new SubClass[5];// 被动引用,没有输出
}
}

类加载器

每个类加载器(除了根类加载器)都是java.lang.ClassLoader的实例,负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象。

同一个.class不会被同一个类加载器加载两次,如何判断是同一个类:

  java中,一个类用其全限定类名标识--包名+类名;jvm中,一个类用其全限定类名+其类加载器标识---包名+类名+类加载器名

加载器层次结构:

JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构:

1.Bootstrap ClassLoader 根类加载器(引导类加载器),加载java的核心类(限定的加载路径:jdk1.8_02/jre/lib/rt.jar)。它不是java.lang.ClassLoader的子类,而是由JVM自身实现的。虚拟机为了安全性以及功能的完整性,并不是任何存在于启动类加载器路径下的jar都会被加载,只有可信类(trusted classes )会被加载,所以开发者不要将自定义的类放在此目录。

2.Extension ClassLoader 扩展类加载器,加载JRE的扩展目录中JAR的类包(限定的加载路径:%JAVA_HOME%/jre/lib/ext/或java.ext.dirs系统属性指定的目录)

3.System ClassLoader 系统类加载器,加载java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径,可通过ClassLoader的静态方法ClassLoader.getSystemClassLoader()获取系统类加载器。如果没有特别指定,用户自定义的类加载器以该类加载器作为它的父加载器。也可以通过Class实例的getClassLoader()方法当前类的类加载器。

4.自定义类加载器

JVM系统自带的类加载器在程序运行中只能加载对应路径的.class文件,无法改变其搜索路径。如果想在运行时从其他路径加载类,就要编写自定义的类加载器。

程序基本的加载流程

1. 根据<JAVA_HOME>/jre/lib/i386/jvm.cfg决定是以client还是server模式运行JVM,然后加载<JAVA_HOME>/jre/bin/client|server/jvm.dll,开始启动JVM;

2. 在启动JVM的同时将加载Bootstrap ClassLoader(启动类加载器,使用C/C++编写,属于JVM自身的一部分);

3. 通过Bootstrap ClassLoader加载Java核心API,包括sun.misc.Launcher类(ExtClassLoader和AppClassLoader是它的内部类);

4. sun.misc.Launcher类在执行初始化阶段时,会创建一个自己的实例,在创建过程中会创建一个ExtClassLoader实例(指定父类加载器为null)、一个AppClassLoader实例(指定父类加载器为ExtClassLoader实例),并将AppClassLoader实例设置为主线程的ThreadContextClassLoader(线程上下文类加载器)。

5. 最后AppClassLoader实例就开始加载classpath路径中需要的类。

java面向对象--类加载器及Class对象

类加载的机制

1.全盘负责:某类所依赖及其引用的所有类,都由同一个加载器负责加载,除非显示使用另外一个加载器。
2.双亲委托: 当一个类加载器收到类加载的请求,首先将请求委派给父类加载器,递归到Bootstrap ClassLoader。然后加载器根据请求尝试搜索和加载类,若无法加载该类时则向子类加载器反馈信息(抛出ClassNotFoundException),由子类加载器自己去加载。
类加载期之间的父子关系并不是类继承上的父子关系,是采用组合的方式实现双亲委派模型的。
根类加载器是由c++实现的,不是由java语言实现,没有继承ClassLoader,所以扩展类加载器调用parent()返回的是null。但扩展类加载器实际上仍可以委派给根类加载器。
使用双亲委托模式的原因:被父类加载器加了的类可以避免避免被子类重新加载,因为在JVM中由全限定名+类加载器名标识类。另外可以避免加载到同sun公司核心API同名的恶意类。

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//在ClassLoader中只提供了findClass()的定义,具体实现需要由子类提供。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先判断该类是否已经被加载,如果已加载,则直接返回实例
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,则委托给父类加载器
try {
if (parent != null) {
//如果存在父类加载器,就委托父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,则当前为扩展类加载器
//会调用启动类加载器的native method
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果所有父类加载器都不能加载,才调用当前类加载器的加载方法
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

3.缓存机制:保证所有加载过的Class都会被缓存。当程序需要Class时,先从缓存区中搜寻Class对象,没有的话,才加载该类。

java面向对象--类加载器及Class对象

自定义类加载器

JVM中除了根加载器之外,所有类加载器都是ClassLoader子类的实例,开发者通过扩展ClassLoader并重写ClassLoader所包含的方法来实现自定义的类加载器。

如上源码所示,loadClass()方法内部主要是双亲委派模型的实现,在最后调用findClass()方法加载类,所以自定义类加载器重写findClass()比重写loadClass()方便,可以避免覆盖默认类加载器的父类委托和缓冲机制两种策略。

ClassLoader类的核心方法:

  1. protected synchronized Class<?> loadClass(String name, boolean resolve)根据指定的binary name(由两个不同部分组成的名字,即全限定类名)来加载类,返回Class对象。
  2. protected Class<?> findClass(String name)根据二进制名称来查找类,返回Class对象。此方法应该被自定义类加载器的实现重写,在通过父类加载器检查所请求的类后,被loadClass方法调用。
  3. final defineClass(String name,byte[] b,int off,int len)将指定类的字节码文件读入字节数组内,并把它转为Class实例,该字节码文件可以来源于网络或本地。
  4. findLoadedClass(String name)返回jvm装载的name类的Class实例,若无返回null。
  5. static getSystemClassLoader()返回系统类加载器。
  6. findSystemClass(String name)从本地文件系统加载class文件,并生成Class对象。

使用自定义类加载器可以实现如下功能:

  执行代码前自动验证数字签名;

  根据用户提供的密码解密代码,从而可以实现代码混淆器来避免反编译class文件;

  根据用户需求来动态的加载类;

  根据应用需求把其他数据以字节码的形式加载到应用中;

URLClassLoader 类

java 为ClassLoader提供了一个实现类URLClassLoader ,该类也是系统类加载器和扩展类加载器的父类,它可以从本地文件系统和远程主机获取二进制文件来加载类。

URLClassLoader的两个构造器

  URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。

  URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建。。。

一旦获得URLClassLoader对象后,就可以调用类加载器的 loadClass()方法来加载指定的类。

加载图片、视频、文本等非类资源

ClassLoader除了用于加载类外,还可以用于加载图片、视频等非类资源,也是采用双亲委派模型将加载资源的请求传递到顶层的Bootstrap ClassLoader,在其对应的目录下搜索资源,若失败才逐层返回并搜索。

相关的实例方法:

  URL getResource(String name):资源名称name是以 '/' 分隔的标识资源的路径名称,返回资源的 URL 对象的枚举。

  InputStream getResourceAsStream(String name):返回读取指定资源的输入流。

  Enumeration<URL> getResources(String name)

1)class.getResource(String path):返回URL对象

  path以'/'开头时,'/'表示ClassPath,从项目的classpath下查询资源;

  path不以'/'开头时,是从此类所在的包下查询资源;

步骤:先递归在所有parent classLoader的classpath里查找resource,如果未找到,则在JVM内置的calss loader的路径中查找。

java面向对象--类加载器及Class对象

2)class.getClassLoader().getResource(String name):返回URL对象

The name of a resource is a '/'-separated path name that identifies the resource.
name不能以'/'开头,指资源标识符(即相对于classpath的路径)。一般web项目的classpath路径为……/webapps/appName/WEB-INF/classes/。如果资源不在此目录下,需要用使用相对路径做调整。

String path= MyService.class.getClassLoader().getResource("config.properties").getPath();
Properties prop = new Properties();
prop.load(new FileReader(path));

动态加载类的方法                  

1. 利用现有的类加载器

// 会执行类的初始化
Class.forName(String name);
Class.forName(String name, true, this.getClass().getClassLoader());
// 不执行类的初始化
Class.forName(String name, false, this.getClass().getClassLoader());
this.getClass().loadClass(String name);
// 通过系统类加载器加载,不执行初始化
ClassLoader.getSystemClassLoader().loadClass(String name);
// 通过线程上下文类加载器加载,不执行初始化
Thread.currentThread().getContextClassLoader().loadClass(String name);

2. 利用URLClassLoader

URL[] baseUrls = {new URL("file:/d:/test/demo.jar")};
URLClassLoader loader = new URLClassLoader(baseUrls);
Class clazz = loader.loadClass("com.demo.Hello");
Hello hello=(Hello)class.newInstance();

3. 继承ClassLoader的自定义类加载器

public class MyClassLoader extends ClassLoader {
public Class<?> findClass(String name) throws ClassNotFoundException {
String classNameWithPackage = name;
Class<?> clazz = null;
try {
name = name.replace(".", "/");
name += ".class";
URL url = MyClassLoader.class.getClassLoader().getResource(name);
System.err.println(">>:" + url.getPath());
File file = new File(url.getPath());
FileInputStream fis = new FileInputStream(file);
byte[] b = new byte[fis.available()];
int len = fis.read(b);
fis.close();
System.err.println(len);
clazz = defineClass(classNameWithPackage, b, 0, len);
} catch (Exception e) {
e.printStackTrace();
}
return clazz;
}
}

反射

程序在运行时接收到外部传入的一个对象,无法预知该对象可能属于哪些类,但程序又需要调用该对象运行时类型的方法,此时可以使用反射来发现该对象和类的详细信息。

RTTI

编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果运行时类型和编译时类型不一致,会出现多态。那如何才能知道父类型的引用变量具体引用的是哪个子类型的对象呢?Java中通过RTTI提供相应的帮助。

运行时类型信息(runtime type information)是通过Class对象这个特殊的对象表示的,它包含了父类型引用变量引用的那个具体子类型对象对应的类型的type information。

获得Class对象

.class文件由类装载器装载后,JVM会生成一个对象,用以描述该类的元信息(name,package,methods,fields,constructors...)。这个对象为java.lang.Class的实例,构造方法为private,只能由jvm创建,通过该对象就可以访问到JVM中这个类的所有详细信息。

注:metadata:data that describes other data,meta前缀是about的意思。Metadata is defined as the data providing information about one or more aspects of the data; it is used to summarize basic information about data which can make tracking and working with specific data easier.元数据是指描述其他具体数据的数据,对具体数据的具体某些方面进行说明。比如一张照片的元数据可以包括主题,作者、文件大小、创作时间等元数据。

在Java程序中获得Class对象通常由如下3中方式:

(1)使用Class类的静态方法Class.forName(String className),字符串参数的值是某个类的全限定类名,会导致className的类被初始化。

(2)调用某个类的class属性来获取该类对应的Class对象。编译时会检查,不用try语句,而且更高效。例如Person.class将会返回Person类对应的Class对象。不会导致类被初始化。

(3)调用某个对象的getClass()方法,该方法定义在java.lang.Object类中,所有object都可以使用。

从Class中获取信息

Class类提供了大量的实例方法来获取该Class对象所对应类的详细信息:

获取Class对象对应的类所包含的构造器:(不会返回父类的构造器)

Constructor<T>  getConstructor(class<?>...parameterTypes):返回对应类指定的public构造器

Constuctor<?> [ ] getConnstuctor():返回对应类所有的public构造器

Constructor<T> getDeclaredConstructor(class<?>...parameterTypes):返回对应类指定的构造器,与构造器的访问权限无关

Constuctor<?>  [ ] getDeclaredConstructor():返回对应类定义的全部构造器,与构造器的访问权限无关

获取Class对象对应类所包含的方法

Method getMethod(String name,class<?>...parameterTypes):返回对应类指定的public方法

Method [ ] getMethods():返回对应类的所有public方法,包括继承的方法

Method getDeclaredMethod(String name,class<?>...parameterTypes):返回对应类的指定方法,与方法的访问权限无关

Method [ ] getDeclaredMethods():返回对应类本身定义的全部方法,与方法的访问权限无关

访问Class对象对应类所包含的Filed

Filed getFiled(String name):返回对应类指定的public Filed

Filed [ ] getFiled():返回对应类所有的public Filed

Filed getDeclaredFiled(String name):返回对应类指定的 Filed,与 Filed的访问权限无关

Filed [ ] getDeclaredFiled():返回对应类全部 Filed,与 Filed的访问权限无关

Class类还提供了其他API,可以查看类包含的注释、内部类、对应的父类、所实现的接口、直接父类等详细信息。

上述方法中getMethod()和getDeclaredMethod()的区别:前者返回public方法,而且包含从父类继承的public方法,后者只返回对应类本身定义的一切方法。

getMethod()方法中,需要传入多个类型为Class<?>的参数,具体是类似于String.class或Interger.class样子的类型参数。因为java中有方法的重载,必须根据参数列表才能区分不同的方法。

public class ClassTest {
private ClassTest() {
}
public ClassTest(String name) {
System.out.println("执行有参数的构造器");
}
private void info1(){
System.out.println("private方法");
}
public void info() {
System.out.println("执行无参数的info方法");
}
public void info(String str) {
System.out.println("执行有参数的info方法" + ",其str参数值:" + str);
}
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("demo3.ClassTest");
// 获取该Class对象所对应类的全部构造器
Constructor[] ctors = clazz.getDeclaredConstructors();
System.out.println("ClassTest的全部构造器如下:");
for (Constructor c : ctors) {
System.out.println(c);
}
// 获取该Class对象所对应类的全部public构造器
Constructor[] publicCtors = clazz.getConstructors();
System.out.println("ClassTest的全部public构造器如下:");
for (Constructor c : publicCtors) {
System.out.println(c);
}
// 获取该Class对象所对应类自身定义的全部public方法
Method[] mtds = clazz.getMethods();
System.out.println("ClassTest定义的全部方法如下:");
for (Method md : mtds) {
System.out.println(md);
}
// 获取该Class对象所对应类的指定方法
System.out.println("ClassTest里带一个字符串参数的info()方法为:" + clazz.getMethod("info", String.class));
// 通过getDeclaringClass()访问该类所在的外部类
System.out.println("ClassTest的包为:" + clazz.getPackage());
System.out.println("ClassTest的父类为:" + clazz.getSuperclass());
}
}

常用概念区分:

静态类加载:运行程序时就知道类名,同时加载类;通过new关键字创建对象;找不到类时抛出NoClassDefFoundExceptio错误。

动态类加载:运行程序后,先传入类名再加载类(使用时才加载??/)?;通过Class.forName(….).newInstance()创建对象;找不到类时抛出ClassNotFoundException异常。

反射