Java类的加载

JVM的位置

JVM是运行在操作系统之上的,它与硬件没有直接的交互


JVM体系结构

类加载器ClassLoader

- 负责加载class文件,class文件在**文件开头有特定的文件标示**,将class文件字节码内容加载到内存中,
  并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于他是否可以运行,则由Execution Engine决定
		特定标示:0X cafe babe

类加载器的分类

image-20210106132410789

  • 虚拟机自带的加载器

    • 引导类加载器(Bootstrap) C++

    负责加载jre/lib目录下的核心类库,比如rt.jar,charsets.jar

    并不继承自java.lang.ClassLoader,没有父加载器

    出于安全考虑,启动类加载器只加载包名为java,javax,sun等开头的类

    加载扩展类和应用程序类加载器,并指定为他们的父类加载器

    • 扩展类加载器(Extension) Java

    负责加载jre/lib/ext目录中的JAR类包

    • 应用程序类加载器(AppClassLoader) Java也叫系统类加载器,加载当前应用的classpath的所有类

    负责加载用户自定义路径下的class字节码文件

  • 用户自定义加载器

    • Java.lang.ClassLoader的子类,用户可以定制类的加载方式

除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。

不同类加载器看似是继承关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用。


类的加载分类: 显示加载 vs 隐式加载

class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式

  • 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中

命名空间

什么是类的唯一性?

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

命名空间

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

借助这一特性,我们可以在大型应用中运行同一个列的不同版本

类加载的过程

加载:

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

连接:

  • 验证(Verify):

    • 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
    • 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证
  • 准备(Prepare):

    • 为类变量(static)分配内存并且设置该类变量的默认初始值

      private static int a = 1;
      //这个阶段会先把a赋值为默认值0
      
    • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化

    • 这里不会为实例变量分配初始化,类变量会分配在方法去中,而实例变量是会随着对象一起分配到堆中

  • 解析(Resolve):

    • 将常量池中的符号引用转换为直接引用的过程
    • 事实上,解析操作往往会伴随着JVM在执行完初始化之后在执行
    • 符号引用就是一组符号来描述所引用的目标.
    • 解析动作主要针对类或接口/字段/类方法,接口方法,方法类型等.

初始化:

  • 初始化阶段就是执行类构造器方法<clint>()方法的过程
  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
  • 构造器方法中指令按语句在源文件中出现的顺序执行

双亲委派机制

image-20210106142348694

例如:

package java.lang;

public class String{
    public static void main(String[] args){
        System.out.println("helloword");
    }
}

运行结果会是什么呢?

> 在类 java.lang.String 中找不到main方法.

为什么?

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父亲去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父亲加载器反馈自己无法完成这个请求的时候,子类加载器才会尝试自己去加载.

- 例如:本例中自己写了一个java.lang.String,会先交给--->Application ClassLoader判断一下有没有加载过

- 没有加载过则Application ClassLoader向上委托 交给--->Extension ClassLoader判断有没有加载过

- 没有加载过则Extension ClassLoader再向上委托 交给--->Bootstrap ClassLoader,

- Bootstrap ClassLoader再没有父加载器,于是开始判断rt.jar包里有没有这个java.lang.String这个类
		发现有就返回rt.jar包里的String
		如果说没有的话,再由下一层Extension ClassLoader判断自己加载的包里有没有这个xx.class
			如果说有的话返回这个包里的这个xx.class
			如果说没有的话,再由下一层Application ClassLoader判断断自己加载的包里有没有这个xx.class
				如果说有的话返回这个包里的这个xx.class
				没有的话报异常

流程如下图:

双亲委派的好处与弊端

好处:

  1. 比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象,保证被加载类的唯一性
  2. 自己写的Java.lang.String.class类不会被加载,可以防止核心API库被随意篡改

弊端:

  • 这个方式虽然说结构比较清晰,使各个ClassLoader的职责非常明确,但是同样会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类.

  • 通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类.按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题.比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定了一个工厂方法,用于创建该接口的实例,而该接口和工厂方法都在启动类加载器中.这时,就会出现工厂方法无法创建由应用类加载器加载的应用实例的问题

因此,由于在Java虚拟机规范中并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式

ClassLoader源码

loadclass()源代码如下:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //首先,在缓存中判断是否已经加载过同名的类
            Class<?> c = findLoadedClass(name);
            
            //没有加载过同名类的话
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //先获取当前类加载器的父类加载器
                    if (parent != null) {
                        //如果存在父类的话,则调用父类加载器进行类的加载
                        c = parent.loadClass(name, false);
                    } else {
                        //可能没有父类加载器也可能父类加载器是引导类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				
                //当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
                if (c == null) {
                    long t1 = System.nanoTime();
                    //调用当前class的findClass()方法去寻找
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

在loadClass()方法中实际上就实现了双亲委派机制.

loadClass()中调用的findClass()源代码如下:

protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    //得到二进制class文件
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}
  • 该方法是在URLClassLoader中实现的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委派模式.

  • 需要注意的是: ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的.

  • 一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findCLass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象>

其中调用的defineClass()方法源码如下:

    private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // 检查包是否已经加载过
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // 开始读取文件并定义Class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }
  • 该方法是用来将byte字节流解析成JVM能够识别的Class对象,通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接受一个类的字节码,然后转换为byte字节流创建对应的Class对象

  • defineCLass()方法通常与findCLass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findCLass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象>

自定义类加载器的实现方式:

  • Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类
  • 在自定义ClassLoader的子类时,常见做法有两种
    1. 重写loadClass()方法 --- 会破会双亲委派机制
    2. 重写findClass()方法 --- 不会破坏双亲委派机制(推荐)

自定义类加载器

代码实现:

/**
 * @PROJECT_NAME: myTest
 * @DESCRIPTION:
 * @USER: 罗龙达
 * @DATE: 2021/2/23 23:17
 */
public class LoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader loader = new MyClassLoader("d:/");
        Class clazz = loader.loadClass("demo1");
        
        System.out.println("加载此类的类的加载器为" + clazz.getClassLoader().getClass().getName());
        //加载此类的类的加载器为load.MyClassLoader
        
        System.out.println("加载此类的类的父类加载器为" + clazz.getClassLoader().getParent().getClass().getName());
        //加载此类的类的父类加载器为sun.misc.Launcher$AppClassLoader

    }
}

class MyClassLoader extends ClassLoader {
    private String pathName;


    public MyClassLoader(String pathName) {
        this.pathName = pathName;
    }

    public MyClassLoader(ClassLoader parent, String pathName) {
        super(parent);
        this.pathName = pathName;
    }



    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {

        //获取字节码文件的完整路径
        String fileName = pathName + className + ".class";
        //获取一个输入流
        BufferedInputStream bis = null;
        //获取一个输出流
        ByteArrayOutputStream baos = null;

        try {
            bis = new BufferedInputStream(new FileInputStream(fileName));
            baos = new ByteArrayOutputStream();

            //具体读入数据并写出的过程
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            //获取内存中的完整的字节数组的数据
            byte[] bytes = baos.toByteArray();
            //调用defineClass(),将字节数组的数据转换为Class的实例
            Class clazz = defineClass(null, bytes, 0, bytes.length);
            return clazz;
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}

Class.forName()与ClassLoader.loadClass()

  • 前者是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个Class对象.该方法在将Class文件加载到内存的同时,会执行类的初始化.
  • 后者是一个实例化方法,需要一个CLassLoader对象来调用此方法.该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.

Java沙箱安全机制

沙箱是一个限制程序运行的环境

沙箱机制就是将Java代码限定在虚拟机特定的运行范围内,并且严格限制代码对本地系统资源访问.通过这样的措施来保证对代码的有限隔离,防止对本地系统资源造成破坏

系统资源包括如CPU,内存,文件系统,网络.不同级别的沙箱对这些资源访问的限制也可以不一样

当前最新的安全机制实现引入了域(Domain)的概念

虚拟机会把所有代码加载到不同的系统域和应用域.系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问.虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission).存在于不同域中的类文件就具有了当前域的全部权限,

posted @ 2021-02-27 20:40  longda666  阅读(114)  评论(0)    收藏  举报