Java基础回顾 —虚拟机
虚拟机作用:
- 为应用程序屏蔽底层操作系统的细节,因为不同系统底层API不同,虚拟机就用来处理这些底层的细节,从而为程序提供一个统一的接口。
- 为应用程序提供必要的运行时的支持,包括基本类型和操作符、对象模型、Unicode支持、动态链接、垃圾回收器、内存模型和访问控制。
-
ClassLoader类加载器:
java虚拟机与程序的生命周期:
如下情况下,java虚拟机将结束生命周期:- 执行了System.exit()方法。
- 程序正常执行结束。
- 程序在执行过程中遇到了异常或错误而异常终止。
- 由于操作系统出现错误而导致Java虚拟机进程终止。
-
类的加载、连接与初始化
加载:查找并加载类的二进制数据 。
连接: 负责把类的二进制数据合并到JRE中。- 验证:确保被加载的类的正确性。
- 准备:为类的静态变量分配内存,并将其初始化为默认值。
- 解析: 把类中的符号引用转换为直接引用。
初始化:为类的静态变量赋予正确的初始值。
类的静态变量赋值方式:一声明类变量时直接赋值;二是使用静态初始化块为类变量指定初始值。 -
java程序对类的使用方式可分为两种:
主动使用
被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化他们。 -
初始化时机:主动使用(六种):
- 创建类的实例:
通过new方式、通过反射方式、通过反序列化方式。 - 访问某个类或接口的静态变量,或者对该静态变量赋值。
- 调用类的静态方法。
- 使用反射方式来强制创建某个类和接口对应的Class对象。eg:Class.forName(“Person”),如果系统还未初始化Person类,则这句话将会初始化Person类,并返回java.lang.Class对象。
- 初始化一个类的子类。
- Java虚拟机启动时被标明为启动类的类。
tips:
- 对于一个final类型的类变量,如果该类变量的值在编译时就能确定,Java编译器会在编译时直接将这个类变量出现的地方替换为它的值,那么程序使用这个类变量时,是不会导致该类初始化的。但是如果final修饰的类变量在编译时不能确定下来,而是要在运行时才确定该变量的值,则这种方式程序访问类变量,会导致类被初始化。
2)当使用ClassLoader的loadClass方法来加载某个类时,该方法只会加载该类,并不会执行类的初始化;使用Class的forName()静态方法才会导致强制初始化该类。
- 创建类的实例:
-
类的加载指将类的.class文件中的二进制数据读到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区的数据结构。
-
JVM提供了三种类加载器:
- 根类加载器:C++编写,负责加载Java的核心类。
- 扩展加载器:java编写,负责加载JRE的扩展目录,可以为Java扩展核心类以外的新功能,只要将自己的JAR包放入JAVA_HOME/jre/lib/ext路径即可。
- 系统加载器:java编写,负责在JVM启动时加载来自Java命令的-classpath选项,java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。
-
用户自定义加载器需要继承java.lang.ClassLoader
-
类的初始化步骤:
- 假如这个类还没有被加载和连接,那么先进行加载和连接。
- 假如类存在直接的父类,并且 这个父类没有被初始化,那就先初始化直接的父类。
- 假如类中存在初始化语句,那就依次执行这些初始化语句。
-
编译期常量不会引起类的初始化,运行期常量则会引起类的初始化。
-
类的初始化时机:
虚拟机初始化一个类时,要求它的所有父类都已经被初始化(不适用于接口):
一个父接口不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。 -
只有当程序访问的静态变量或静态方法确实在当前类或当前接口中定义时,才可以认为是对类或接口的主动使用。(当子类没有重写父类的变量,而程序通过子类访问父类的变量,不会初始化子类,而直接初始化父类)
-
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
-
类的加载机制主要有三种:
- 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用的其他Class也将由该类的加载器负责载入,除非显示使用另一个类加载器来载入。
- 父亲委托机制:父类委托,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。各个加载器按照父子关系形成树形结构,除了根类加载器以外,其余的类加载器都有且只有一个父加载器。
- 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区查找该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,并存入缓存区中。这也是为什么修改Class后,需要重启JVM,重启之后所做的修改才会生效。
-
定义类加载器: 能加载一个类,该加载器以及其子加载器都称作:初始类加载器。
如果生成一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器就将成为该类加载器的父加载器。 -
命名空间:由该加载器及所有父加载器所加载的类组成。
-
创建并使用自定义的类加载器:
JVM中除了根类加载器,其余加载器就是继承自ClassLoader,可以通过扩展ClassLoader的子类,并重写ClassLoader所包含的方法来实现自定义的类加载器。
ClassLoader中三个关键的方法:- loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据名称来加载类,系统就是调用该方法来获取指定类对应的Class对象。
- findClass(String name):根据指定的类名来查找类。
要实现自己的ClassLoader,则可以通过重写上面两个方法来实现,通常推荐findClass()方法,而不是重写loadClass()方法。 - final defineClass(String name, byte[] b, int off, int len)方法负责将指定类的字节码读入字节数组byte[] b内,并把它转换为Class对象,该字节码可以来源于文件、网络等。此方法是final的不可以重写。
loadClass()的执行步骤: - 用findLoadedClass(String name)来检查是否已经加载类,如果已经加载则直接返回。
- 在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。
- 调用findClass(String name)方法查找类。
所有说重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;重写loadClass()方法则实现逻辑更为复杂。
ClassLoader的普通方法: - findSystemClass(String name):从本地文件系统装入文件。它在本地系统中寻找类文件,如果找到了则使用defineClass()方法将原始字节转换成Class对象,以将该文件转换成类。
- static getSystemClassLoader():这是一个静态方法,用于返回系统类加载器。
- getParent():获取该类加载器的父类加载器。
- resolveClass(Class<?> c):链接指定的类。类加载器可以使用此方法来链接类c。
- findLoadedClass(String name):如果此Java虚拟机已加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。该方法是Java类加载缓存机制的体现。
tips: 一般是先自定义的类加载器,可以实现以下常见功能: - 执行代码钱自动验证数字签名;
- 根据用户提供的密码解密代码,从未可以实现代码混淆器来避免编译*.class文件。
- 根据用户需求来动态加载类。
- 根据应用需求把其他数据以字节码的形式加载到应用中。
-
URLClassLoader可以从本地文件系统获取二进制文件来加载类,也可以从远程主机上获取二进制文件来加载类。