1. 类的生命周期
类在内存中完整的生命周期:加载-->使用-->卸载
。其中加载过程又分为:装载、链接、初始化
三个阶段。
2. 类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
(1)装载(Loading
)
将类的class文件读入内存,并为之创建一个java.lang.Class
对象。此过程由类加载器完成
(2)链接(Linking
)
①验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe
开头,没有安全方面的问题。
②准备Prepare:正式为类变量(static)分配内存并设置类变量默认初始值
的阶段,这些内存都将在方法区中进行分配。
③解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
(3)初始化(Initialization
)
- 执行
类构造器<clinit>()方法
的过程。类构造器<clinit>()方法
是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
- 虚拟机会保证一个
类的<clinit>()方法
在多线程环境中被正确加锁和同步。
3. 类加载器(classloader)
3.1 类加载器的作用
将class
文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class
对象,作为方法区中类数据的访问入口。
类缓存:标准的JavaSE
类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class
对象。
3.2 类加载器的分类(JDK8)
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)
和自定义类加载器(User-Defined ClassLoader)
。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader
的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用
C/C++语言
实现的,嵌套在JVM内部。获取它的对象时往往返回null - 它用来加载Java的核心库(
JAVA_HOME
/jre
/lib
/rt.jar
或sun.boot.class.path
路径下的内容)。用于提供JVM自身需要的类。 - 并不继承自
java.lang.ClassLoader
,没有父加载器。 - 出于安全考虑,Bootstrap启动类加载器只加载包名为
java
、javax
、sun
等开头的类 - 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
(2)扩展类加载器(Extension ClassLoader)
- Java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现。 - 继承于ClassLoader类
- 父类加载器为启动类加载器
- 从
java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK的安装目录的jre
/lib
/ext
子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
(3)应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由
sun.misc.Launcher$AppClassLoader
实现 - 继承于
ClassLoader
类 - 父类加载器为扩展类加载器
- 它负责加载环境变量
classpath
或系统属性java.class.path
指定路径下的类库 - 应用程序中的类加载器默认是系统类加载器。
- 它是用户自定义类加载器的默认父加载器
- 通过
ClassLoader
的getSystemClassLoader()
方法可以获取到该类加载器
(4)用户自定义类加载器(了解)
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
- 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
- 同时,自定义加载器能够实现
应用隔离
,例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。 - 自定义类加载器通常需要继承于ClassLoader。
3.3 双亲委派机制
java中类加载的过程采用双亲委派机制,加载一个类先由应用类加载器委托给扩展类加载器,再由扩展类加载器委托给启动类加载器,如果启动类加载器发现自己也加载不了的话,则由扩展类加载器加载,如果扩展类加载器也加载不了的话,则由应用类加载器加载,如果连应用类加载器都找不到的话,则报ClassNotFound
的异常。
理解:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
- 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
- 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
同理(为什么要从下到上,再又从上到下):一个类在收到类加载请求后,如果这个类没有被加载,当前类加载器不会自己加载这个类,而是把这个类加载请求向上委派给它的父类去完成,父类收到这个请求后又继续向上委派给自己的父类加载器,以此类推,直到所有的请求委派到启动类加载器中。如果这个类不属于启动类加载器加载,又会向下委派子类加载器来加载这个类,直到这个请求被成功加载,但是一直到自定义加载器都没有找到,JVM就会抛出ClassNotFund
异常。
图片取自网络。
3.3.1 双亲委派机制优势
- 避免类的重复加载
- 当自己程序中定义了一个和
Java.lang
包同名的类,此时,由于使用的是双亲委派机制,会由启动类加载器去加载JAVA_HOME/lib
中的类,而不是加载用户自定义的类。此时,程序可以正常编译,但是自己定义的类无法被加载运行。 - 保护程序安全,防止核心API被随意篡改。通过委托方式,不会去篡改核心
.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
- 例如:自己定义了一个
java.lang.String
的类,首先会由应用类加载器加载,应用类加载器识别到这个类不属于我加载,因为是JVM系统级别的类,此时应用程序加载器不会加载,会向上委托,直到启动类加载器,启动类加载器识别到这个类属于我来加载,就会去JAVA_HOME
下去加载相同包名的String
类。加载完成后,即使再有相同包名类名的String
类也不会去加载,因为相同包名的类已经被加载过了,就会造成即使开发人员定义了和JVM级别相同包的类也不会去加载自己定义的类,保证了系统级别的类加载安全性,防止核心API被随意篡改。
3.4 查看某个类的类加载器对象
(1)获取默认的系统类加载器
ClassLoader classloader = ClassLoader.getSystemClassLoader();
(2)查看某个类是哪个类加载器加载的
ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader(); //如果是根加载器加载的类,则会得到null ClassLoader classloader1 = Class.forName("java.lang.Object").getClassLoader();
(3)获取某个类加载器的父加载器
ClassLoader parentClassloader = classloader.getParent();
示例代码:
import org.junit.Test; public class TestClassLoader { @Test public void test01(){ ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println("systemClassLoader = " + systemClassLoader); } @Test public void test02()throws Exception{ ClassLoader c1 = String.class.getClassLoader(); System.out.println("加载String类的类加载器:" + c1); ClassLoader c2 = Class.forName("sun.util.resources.cldr.zh.TimeZoneNames_zh").getClassLoader(); System.out.println("加载sun.util.resources.cldr.zh.TimeZoneNames_zh类的类加载器:" + c2); ClassLoader c3 = TestClassLoader.class.getClassLoader(); System.out.println("加载当前类的类加载器:" + c3); } @Test public void test03(){ ClassLoader c1 = TestClassLoader.class.getClassLoader(); System.out.println("加载当前类的类加载器c1=" + c1); ClassLoader c2 = c1.getParent(); System.out.println("c1.parent = " + c2); ClassLoader c3 = c2.getParent(); System.out.println("c2.parent = " + c3); } }
3.5 使用ClassLoader获取流
关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流
InputStream in = null; in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties"); System.out.println(in);
举例:
//需要掌握如下的代码 @Test public void test5() throws IOException { Properties pros = new Properties(); //方式1:此时默认的相对路径是当前的module // FileInputStream is = new FileInputStream("info.properties"); // FileInputStream is = new FileInputStream("src//info1.properties"); //方式2:使用类的加载器 //此时默认的相对路径是当前module的src目录 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties"); pros.load(is); //获取配置文件中的信息 String name = pros.getProperty("name"); String password = pros.getProperty("password"); System.out.println("name = " + name + ", password = " + password); }