JVM----基础

我们执行一个类

首先javac命令编译这个类(对编译原理我们不需要做深入了解)

在java命令启动虚拟机对.class文件进行加载和执行

类加载或类初始化

  当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

                                                                   

一、类加载过程

1.加载    

  将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构(包含类的所有的信息),在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,就是Class对象会指向这个方法区中的数据,并且如果外部程序只能通过Class对象来进行访问。这个过程需要类加载器参与。也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象

  类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

  通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源

    从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
    从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
    通过网络加载class文件。
    把一个Java源文件动态编译,并执行加载。
  类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

2.链接

  当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

  (1)验证

    验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

       四种验证做进一步说明:

    文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。

    元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。

    字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。

    符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。

  (2)准备

    类准备阶段负责为类的静态变量(static)分配内存(这些内存都在方法区中分配),并设置默认初始值。

    比如public static int a=3; 在准备阶段a=0(设置默认值),a=3是在初始化的时候。

  (3)解析   

    将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。

    符号引用(抽象):a---->b,表示a会去定位到b。但是不知道b的具体位置

    直接引用(具体):a(b的位置在地球-->中国-->北京-->........)

    一个类中包含了很多常量(final修饰的常量,在编译期就已经加载到了该类的常量池中。(实在不理解,常量池难道不是运行时才开辟的吗?)),比如类名,方法名,参数名a,变量名b和c,“aa”和1,类型名称void String public都是属于常量。在方法区中每一个类都有一个常量池。这个常量池放置了符号引用,但是需要将符号引用替换成直接引用之后,类才具备初始化的条件。

public class Demo01 {
    public void test(int a){
        String  b = "aa";
        int c = 1;
    }
} 

3.初始化

  初始化阶段是执行类构造器 <clinit>() 方法的过程。类构造器 <clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。(初始化是为类的静态变量赋予正确的初始值
  当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化虚拟机会保证一个类的<clinit>0方法在多线程环境中被正确加锁和同步。
  当访问一个Java类的静态域时,只有真正声明这个域的类才会被初始化。

public class Demo {
    public static void main(String[] args) {
        System.out.println(Test.a);
    }
}
class Test{
    //赋值动作a=1和静态语句块合并成<clinit>()方法,并执行<clinit>()方法
    public static int a=1;
    static{
        System.out.println("静态代码块");
        a = 2;
    }
    public Test(){
    }
}

二、类的第一次加载时机(如果类已经加载到内存了,就不会再加载了,除非该类被释放)

  类的主动引用

    创建类的实例,也就是new一个对象
    访问某个类或接口的静态变量,或者对该静态变量赋值(该字段不能被final修饰)
    调用类的静态方法
    反射(Class.forName("com.zy.Xxx"))
    初始化一个类的子类(会首先初始化子类的父类)
    JVM启动时标明的启动类,即文件名和类名相同的那个类(当虚拟机启动,java Hello,则一定会初始化Hello类。说白了就是先启动main方法所在的类)

  类的被动引用

    引用常量(final)有以下情况(对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化),对这段解释我个人比较信服的(staitc final修饰的字段编译阶段会被加载到常量池中,我是不太理解的)

System.out.println(Test.a); //此时Test.a 就会被替换成常量了

    通过数组定仪类引用,不会触发此类的初始化

Test[] tests = new Test[10];

    当访问一个静态域时,只有真正声明这个城的类才会被初始化,比如A类继承了B类,B类有一个静态字段b,A.b,此时只初始化B类,A类不初始化。

三、类加载器

  (1)类加载器作用

    将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口。

    类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

  (2)类缓存

    标准的Java SE 类加载器 可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过,JVM垃圾收集器可以回收

  (3)类加载器的层次结构(树状结构)

    (引导类加载器|根类加载器)bootstrap class loader

      它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar(rootDir)里所有的class,由C++实现,不是ClassLoader子类)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

    (扩展类加载器)extensions class loader

      它负责加载JRE的扩展目录,lib/ext或者由java.ext.*.jar系统属性指定的目录中的JAR包的类(rootDir由Java语言实现,父类加载器为引导类加载器,java中代码获取不到,获取值为null。

      由sun.misc.Launcher$ExtClassLoader实现

    (应用程序类加载器|系统类加载器)system class loader

      被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

      该类加载的路径(rootDir)就是我们的classPath路径

      由sun.misc.Launcher$AppClassLoader实现

    java.class.ClassLoader类介绍    

      作用:

        -java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。
        -除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文性和配置文件等。

      相关方法        

        getParent0返回该类加载器的父类加载器。
        loadClass(String name)加载名称为name的类,返回的结果是java.lang.Class类的实例。
        findClass(String name)查找名称为name的类,返回的结果是java.lang.Class类的实例。
        findLoadedClass(String name)查找名称为name的已经被加载过的类,返回的结果是java.lang.Class类的实例。
        defineClass(String name,byte]b,int off,int len)把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。这个方法被声明为final的。
        resolveClass(Class<?>c)链接指定的Java类。
        对于以上给出的方法,表示类名称的name参数的值是类的二进制名称。需要注意的是内部类的表示,如com.example.Sample$1和com.example.Sample$Inner等表示方式。

    public static void main(String[] args) {
        System.out.println(ClassLoader.getSystemClassLoader());
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());

        System.out.println(System.getProperty("java.class.path"));
    }

四、类加载机制:

  双亲委派机制(注意只是代码控制的类加载顺序方式)

    双亲委派机制,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

    双亲委派机制的优势:采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

    比如你自己定义一个 java.lang.String类,这个类会被交到顶级加载器。他发现java.lang.String在自己的jar中,所以需要加载,就加载了自己的java.lang.String,所以下面的这个.class文件,永远不会被加载。提高核心库的安全性。

package java.lang;
public class String {
    public int hashCode(){
        return 11;
    }
}

    双亲委托机制是代理模式的一种
      -并不是所有的类加载器都采用双亲委托机制。
      tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的

  线程上下文类加载器

    线程类加载器是为了抛弃双亲委派加载链模式。

    每个线程都有一个关联的上下文类加载器。如果你便用new Thread0方式生成新的线程,新线程将继承其父线程的上下文然加载器,如果程序对线程上下文类加载器没有任何改动的话,程序中所有的线程将都使用系统类加载器作为上下文类加载器。

    public static void main(String[] args) {
        try {
            //sun.misc.Launcher$AppClassLoader@b4aac2
            System.out.println(Demo01.class.getClassLoader());
            //sun.misc.Launcher$AppClassLoader@b4aac2
            System.out.println(Thread.currentThread().getContextClassLoader());

            //设置线程上下文类加载器(需要自定义一个类加载器)
            Thread.currentThread().setContextClassLoader(new FileSystemClassLoader("C:/Users/zhengyan/Desktop/hellow/"));
            System.out.println(Thread.currentThread().getContextClassLoader());
            //通过当前线程上下文类加载器加载类
            Class<?> aClass = Thread.currentThread().getContextClassLoader().loadClass("com.zy.HelloWord");
            System.out.println(aClass);
            System.out.println(aClass.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

  TOMCAT服务器的类加载机制

    TOMCAT不能使用系统默认的类加载器。    

      如果TOMCAT跑你的WEB项目使用系统的类加载器那是相当危险的,你可以直接是无忌惮是操作系统的各个目录了。
      对于运行在Java EE容器中的Web应用来说,类加载器的实现方式与一般的Java应用有所不同。
      每个Web应用都有一个对应的类加载器实例。该类加载器也使用代理模式(不同于前面说的双亲委托机制),所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。但也是为了保证安全,这样核心库就不在查询范围之内。

    为了安全TOMCAT需要实现自己的类加载器。

      我可以限制你只能把类写在指定的地方,否则我不给你加载!(你的包名不能以java开口)

  OSGI原理(Open Service Gateway Initative)

    OSGI中每一个模块都有自己的java包和类。如果保证这些模块可以正确的运行相互,相互调用而不出错?OSGI中每一个模块都有一个单独的类加载器。每一个模块可以声明自己需要依赖的其他模块(import),可以声明导出自己的包和类(export)。例如。B模块需要使用A模块的类User,那么当B模块加载User的时候,就会去使用A模块的类加载器处理User。(谁定义谁加载

五、自定义类加载器

  自定义本地文件系统类加载器

package com.zy;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class FileSystemClassLoader extends ClassLoader {
    //rootDir:D:/java/xx  指定目录
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> aClass = findLoadedClass(name);
        if (aClass==null){
            //获取父类加载器
            ClassLoader parent = null;
            try {
                //调用父类加载器的时候,如果找不到com.zy.HelloWord就会抛出异常(父加载器的rootDir不是我们定义的C:/Users/zhengyan/Desktop/hellow/,是系统自定义的)
                parent = this.getParent();
                aClass = parent.loadClass(name);
            } catch (Exception e) {
                //e.printStackTrace();
            }
            if (aClass==null){
                byte[] classData = getClassData(name);
                if (classData==null) {
                    throw new ClassNotFoundException();
                }
                else {
                    aClass = defineClass(name, classData,0,classData.length);
                }
            }
        }
        return aClass;
    }

    private byte[] getClassData(String classname){
        //name:com.zy.User
        FileInputStream fileInputStream = null;
        try {
            String strPath = rootDir+classname.replace(".","/")+".class";
            fileInputStream = new FileInputStream(strPath);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            int len = -1;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes))!=-1){
                byteArrayOutputStream.write(bytes,0,len);
            }
            return byteArrayOutputStream.toByteArray();
        }catch (IOException e) {
            e.printStackTrace();
            return null;
        }finally {
            try {
                if (fileInputStream!=null){
                    fileInputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

class Test{
    public static void main(String[] args) {
        try {
            //创建类加载器,传入一个路径,表示加载器会去这个路径进行加载com.zy.HelloWord
            FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("C:/Users/zhengyan/Desktop/hellow/");
            FileSystemClassLoader fileSystemClassLoader2 = new FileSystemClassLoader("C:/Users/zhengyan/Desktop/hellow/");
            //加载类
            Class<?> aClass = fileSystemClassLoader.loadClass("com.zy.HelloWord");
            Class<?> aClass2 = fileSystemClassLoader2.loadClass("com.zy.HelloWord");

            System.out.println(aClass2.hashCode()==aClass.hashCode()); //同一个类被不同的加载器对象加载,jVM也认为是不同的类

            System.out.println(aClass.getClassLoader()); //当前加载器对象是"com.zy.FileSystemClassLoader@14ae5a5"

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

  自定义网络加载器

    基本代码和文件加载器是一样的。rootDir 就改写成域名,getClassData()这个方法通过new URL("x").openStream();来加载class数据  

  自定义加密解密类加载器

    将一个.class文件读取到字节数组中,取反(任何形式的加密都可以),写入到指定的文件。,类加载器加载编码的文件,对读取的字节数组进行解密操作,然后类加载器加载。


原文链接:https://blog.csdn.net/m0_38075425/article/details/81627349

原文地址:https://www.cnblogs.com/yanxiaoge/p/11626641.html