zoukankan      html  css  js  c++  java
  • JVM总括四-类加载过程、双亲委派模型、对象实例化过程

    JVM总括四-类加载过程、双亲委派模型、对象实例化过程


    目录:JVM总括:目录

    一、 类加载过程

    类加载过程就是将.class文件转化为Class对象,类实例化的过程,(User user = new User(); 这个过程是对象实例化的过程);

    一个.class文件只有一个Class对象(字节码对象),可以有无数个对象(例如:new User(););

    1、Load:

      将编译后的.class文件以二进制流的方式加载到JVM内存中,并转化为特定的数据结构,用到的就是classLoad二类加载器。这个过程中校验cafe babe魔法数常量池文件长度是否有父类等。

      其实加载阶段用一句话来描述就是:把代码数据加载到内存中。

    2、Link:

      分为验证、准备、解析三步。

      验证:更为详细的验证,比如:final是否规范(二次赋值不规范)、static是否合理(静态方法必须引用静态变量)、类型是否正确

      准备:当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。

        内存分配的对象。Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。

        在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

        比如下面的代码在准备阶段,只会为 age 属性分配内存,而不会为 website 属性分配内存。

        public static int age= 3;

        public String website = "www.beijingdesigner.com";

        例如下面的代码在准备阶段之后,age的值将是 0,而不是 3。

        public static int age= 3;

        但如果一个变量是常量(被 static final 修饰)的话,那么在准备阶段,属性便会被赋予用户希望的值。例如下面的代码在准备阶段之后,number 的值将是 3,而不是 0。

        public static final int number = 3;

        这其中的道理我们想下就明白了。

        两个语句的区别是一个有 final 关键字修饰,另外一个没有。被final修饰的常量,是不允许再被改变的。所以初始化就是3了。

      解析:把类中的符号引用转化为直接引用,完成内存结构布局。(符号引用转化为直接引用:例如:test1() { test2(); },这里test1调用test2方法就是符号引用,但实际test2()通过一个指针指向test2方法的内存地址,这个指针负责调用,它就是直接引用)。

    3、Init:

      执行类构造器<clinit>,递归初始化父类的静态代码块执行构造函数。(先执静态变量,再执行行静态代码块)。

    任何小写的class定义的类都有一个魔法属性:class,

    int j;
    Class<Integer> integerClass = int.class;
    Class<Integer> integerClass1 = Integer.class;

    二、类加载的原则:双亲委派模型

      类加载是怎么确定类文件的位置呢?

      1、Bootstrap:最高级的类加载器,装置最核心的类,如:Object、System、String;

      2、Extension ClassLoader:JDK9之前的类加载器,以后的为Platform ClassLoader,加载系统的扩展类;

      3、Application ClassLoader:应用类加载器,主要加载用户自定义CLASSPATH路径下的类。

    类加载器并非继承关系,只是以组合的方式服用父加载器功能,符合优先原则。

    其中第二、第三层为java实现,用户可以自定义类加载器,第三层为C++实现,所有为null:

    ClassLoader classLoader = Son.class.getClassLoader();
    ClassLoader parent = classLoader.getParent();
    ClassLoader parent1 = parent.getParent();
    System.out.println(classLoader);
    System.out.println(parent);
    System.out.println(parent1);
    ----------------------------------- sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@5acf9800
    null

    获取Bootstrap加载的类库:

    URLClassPath bootstrapClassPath = Launcher.getBootstrapClassPath();
    URL[] urLs = bootstrapClassPath.getURLs();
    for (URL url : urLs){
         System.out.println(url.toExternalForm());
    }
    --------------------------------------------------------- file:
    /D:/jdk-8u151-windows-x64/JDK/jre/lib/resources.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/rt.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/sunrsasign.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/jsse.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/jce.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/charsets.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/lib/jfr.jar file:/D:/jdk-8u151-windows-x64/JDK/jre/classes

    Bootstrap加载的路径可以追加,在JVM增加启动参数,不过不建议修改:

    Xbootclasspath/D:/test/src

     自定义类加载器的情况:

    1、隔离加载类:框架中吧类加载到不同的环境中

    2、修改类加载方式:除了Bootstrap,其他类并非一定要加入。

    3、扩展加载源:比如加载数据库;

    4、防止源码泄露:可以对类进行加密,那加载的时候需要自定义加载器对类进行解密加载。

    自定义ClassLoder,继承ClassLoader,重写findClass(),调用defineClass():

    public class UserClassLoader extends ClassLoader {
    
        private String classpath;
    
        public UserClassLoader(String classpath) {
            this.classpath = classpath;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte [] classDate=getDate(name);
                if(classDate==null){
                    throw new FileNotFoundException();
                }
                else{
                    //defineClass方法将字节码转化为类
                    return defineClass(name,classDate,0,classDate.length);
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return super.findClass(name);
        }
        //返回类的字节码
        private byte[] getDate(String className) throws IOException{
            InputStream in = null;
            ByteArrayOutputStream out = null;
            String path=classpath + File.separatorChar +className.replace('.',File.separatorChar)+".class";
            try {
                in=new FileInputStream(path);
                out=new ByteArrayOutputStream();
                byte[] buffer=new byte[2048];
                int len=0;
                while((len=in.read(buffer))!=-1){
                    out.write(buffer,0,len);
                }
                return out.toByteArray();
            }
            catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            finally{
                in.close();
                out.close();
            }
            return null;
        }
    
    }
    UserClassLoader

    调用,其中D:\wp目录下为.class文件,里面有个Son.class

    //自定义类加载器的加载路径
    UserClassLoader myClassLoader=new UserClassLoader("D:\wp");
    //包名+类名
    Class sonClass=myClassLoader.loadClass("com.Son");
    
    if(sonClass!=null){
        Object obj=sonClass.newInstance();
        System.out.println(sonClass.getClassLoader().toString());
    }

    三、对象实例化

    从字节码、执行步骤两个方面分析

    1、字节码方面:

      Object object = new Object();,通过javap -verbose -p查看对象创建的字节码指令:

       

      (1)new:如果找不到Class对象就进行类加载,然后分配内存(本类路径上所有的属性都分配),其中对象的引用也是个变量也占内存(4个字节),这个指令执行完毕会把对象的压入虚拟机栈顶。

      (2)dup:在栈顶复杂引用,如果<init>有参数,把参数压入操作栈,两个引用,压入栈底的用来赋值或保存到局部变量表中,栈顶引用作为句柄调用相关方法。

      (3)invokespecial:调用对象实例化方法,通过栈顶方法调用<init>方法(也就是调用构造方法)。

    2、执行步骤

      (1)确认类元信息是否存在:接到new指令时,在metaspace检查类元信息是否存在,没有就在双亲委派模式下进行类加载,生成Class对象。

      (2)分配对象内存:首先计算对象占用空间大小(成员变量是引用变量就分配4个字节大小的变量空间),在堆中划分内存空间给新对象(分配空间需要进行同步操作,如:cas)。

      (3)设定默认值:成员变量设置不同形式的0值;

      (4)设置对象头:设置对象的哈希码、锁信息、对象所属的类元信息,设置取决于JVM。

      (5)执行init方法:初始化成员变量,执行代码块,调用类的构造方法,把堆对象首地址赋值给引用变量。

     四、思考:ClassLoader.loadClasshe和Class.forName区别

    1 //1
    2 Class<Son> sonClass = Son.class;
    3 //2
    4 Class<?> aClass = Maint.class.getClassLoader().loadClass("com.Son");
    5 //3
    6 Class<?> bClass = Class.forName("com.Son");

    代码2:

            其实这种方法调运的是:ClassLoader.loadClass(name, false)方法
            参数一:name,需要加载的类的名称
            参数二:false,这个类加载以后是否需要去连接(不需要linking)

    代码:3:
            其实这种方法调运的是:Class.forName(className, true, ClassLoader.getCallerClassLoader())方法
            参数一:className,需要加载的类的名称。
            参数二:true,是否对class进行初始化(需要initialize)
            参数三:classLoader,对应的类加载器

    其中1、2都是将.class文件加载到JVM中,得到Class对象,但是还没有连接(Link)静态代码块不会执行;
    代码3得到Class对象并且已经完成初始化<clinit>,静态代码块会执行。( Class.forName("com.mysql.jdbc.Driver");//通过这种方式将驱动注册到驱动管理器上)。

    1、2、3都不会执行对象的构造函数

     五、思考:<clinit>和<init>区别

    <clinit>是类(Class)初始化执行的方法,<init>是对象初始化执行的方法(构造函数);

     详情见:JVM思考-init和clinit区别

  • 相关阅读:
    Laravel 5.5 创建全局公共函数
    Mysql性能优化四:分库,分区,分表,你们如何做?
    Mysql性能优化三:主从配置,读写分离
    Mysql性能优化二:索引优化
    Mysql性能优化一:SQL语句性能优化
    Windows Server 2008 R2(x64) IIS7+PHP5(FastCGI)环境搭建
    centos7使用Gogs搭建Git服务器
    CentOS虚拟机和物理机共享文件夹实现
    Flask目录
    MySQL目录
  • 原文地址:https://www.cnblogs.com/java-zzl/p/9899856.html
Copyright © 2011-2022 走看看