类的加载连接和初始化

  1. 加载: 查找并加载类的二进制数据

  2. 链接: 包括验证,准备和解析类的二进制数据

    • 验证: 确保加载类的正确性。
    • 准备: 为类的静态变量分配内存,将其初始化为默认值(也就是0)
    • 解析: 将类的符号引用变成直接引用 [1]
  3. 初始化: 把类的静态变量赋予正确的初始值。

类的加载

类的加载是指把类的.class文件中的二进制数据读入到内存中,把它存放在方法区中。然后再去装载一个java.lang.Class对象,用来封装类在方法区的数据结构。通俗的来讲就是把代码加载到内存中

类的加载是由虚拟机自带的加载器来完成的,但是我们也可以自己去定义。用java.lang.ClassLoader类的子类的实例。

类加载器允许某个类将要被使用时预先加载它。如果预先加载过程中遇到了.class的错误。那么类加载器在首次主动使用这个类的时候报错(LinkageError)

类的链接

类的验证

类的验证主要验证一下内容:

  • 类文件格式, 看看后缀是否符合
  • 语义检查 看看是否符合java语义(例如把一个String给int或final是否有子类)
  • 字节码验证,字节码代表java方法。
  • 二进制兼容验证。查看这个类引用的另一个类的方法的=在另一个类中是否存在。

类的准备

为类的静态变量分配内存,并设置成0(boolean是false,char是’\u0000’)

类的解析

符号引用上面已经解释过了,其实也就是这个类引用的其他类或其他类的成员变量和方法等。因为这些类在编译阶段并没有加载,所以虚拟机也不知道要到哪里去找这些方法,所以先弄一个符号代表这个方法。

类的初始化

静态变量的初始化有两种途径。(1) 声明时直接赋值 (2) 在静态代码块中初始化。

初始化并不是直接初始化。如果没有加载和链接,那么先加载和链接。如果父类没有初始化,那么先初始化父类。

虚拟机只有首次启用一个类的时候才会初始化它。也就是说,创建对象实例,或者访问使用静态变量,还有是某个正在初始化类的父类时都会初始化它。

此外,当final类型的静态变量,如果能直接计算出值,那么会当成常量,不会导致初始化。反之,会导致初始化。

类的加载器

类加载和Linux进程生成类似,都是先有一个根加载器,然后其他类加载器只有一个父加载器。父加载器不是加载自己,而是加载子类,但是是子类请求父类加载自己。

有三类自带的加载器:

  • 根加载器,负责加载一些核心库,例如java.lang.*
  • 扩展加载器,它的父加载器是根加载器。他从java.ext.dirs系统属性指定的目录中加载扩展。或者从JDK的jre\lib\ext中加载扩展
  • 系统加载器:也叫应用类加载器。它的父加载器是扩展加载器。他从classpath环境变量或者子系统属性java.class.path中加载类,它是用户自定义加载器的默认父加载器。

类加载的过程

例如要加载一个类,首先请求父类加载器代为加载,父类再向它的父类代为加载…。一直到根加载器,如果根加载器不能加载,那么就让扩展加载器加载,如果不能加载…。直到找到一个可以加载的。如果所有加载器都不能加载,那么返回ClassNotFoundException。

成功加载那个类的加载器叫定义类加载器。

这种机制是为了安全考虑,因为在这种严密的机制下,用户自定义的加载器不可能取代由父加载器完成的任务。

这里加载器并不一定是和类一样的父子关系。一对父子加载器可能是同一个类的两个实例。

命名空间

每个类加载器都有自己的命名空间,命名空间由该加载器和所有父加载器所加载的类组成。在同一个命名空间中,不可能出现名字(包括包名)完全相同的两个类。不同的命名空间中就有可能出现。

运行时包

同一加载器加载的属于相同包的类组成了运行时包。包名相同不一定默认访问级别可以访问,必须要组成运行时包才可以访问默认访问级别。

创建用户自定义加载器

首先介绍自定义类的应用场景:

(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。

(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。

(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。

要扩展自己的类加载器,只需扩展java.lang.Classloader类,瑞啊后覆盖findClass(String name)方法。该方法根据参数指定类的名字,返回对应Class对象的引用。


import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class MyClassLoader extends ClassLoader
{
public MyClassLoader()
{

}

public MyClassLoader(ClassLoader parent)
{
super(parent);
}

protected Class<?> findClass(String name) throws ClassNotFoundException
{
File file = new File("D:/People.class");
try{
byte[] bytes = getClassBytes(file);
//defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
}
catch (Exception e)
{
e.printStackTrace();
}

return super.findClass(name);
}

private byte[] getClassBytes(File file) throws Exception
{
// 这里要读入.class的字节,因此要使用字节流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);

while (true){
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}

这段代码是扒下来的

URLClassLoader加载类

在java.net包中,提供了URLClassLoader类,它可以从网上下载类。可以直接使用这个类作为自定义加载器。

构造方法: URLClassLoader(URL[] urls)//父加载器是系统加载器

URLClassLoader(URL[] urls, ClassLoader parent)//指定父加载器

类的卸载

java自带类加载器所加载的类是永远不会被卸载的。而用户自定义的类加载器可以被卸载。


  1. 1.符号引用就是在编译阶段,虚拟机并不知道所有引用类的地址(因为还没有加到内存中),就用一个符号表示地址。而直接引用就是真实的地址。