首页 > 技术文章 > java类加载机制

hhhshct 2018-11-16 17:58 原文

  前言 

  讲java类加载机制,免不了要涉及到JVM,根据JVM规范,JVM把内存划分成了如下几个区域:

  1.方法区(Method Area)

  2.堆区(Heap)

  3.虚拟机栈(VM Stack)

  4.本地方法栈(Native Method Stack)

  5.程序计数器(Program Counter Register)

  方法区存放了要加载的类的信息(如类名、修饰符等)、静态变量、构造函数、final定义的常量、类中的字段和方法等信息。运行时常量池(Runtime Constant Pool)也是方法区的一部分,用于存储编译器生成的常量和引用

  堆区是GC最频繁的,也是理解GC机制最重要的区域。堆区由所有线程共享,在虚拟机启动时创建。堆区主要用于存放对象实例。

   虚拟机栈占用的是操作系统内存,每个线程对应一个虚拟机栈,它是线程私有的,生命周期和线程一样,每个方法被执行时产生一个栈帧(Statck Frame),栈帧用于存储局部变量表、动态链接、操作数和方法出口等信息,当方法被调用时,栈帧入栈,当方法调用结束时,栈帧出栈。

  本地方法栈用于支持native方法的执行,存储了每个native方法的执行状态。本地方法栈和虚拟机栈他们的运行机制一致,唯一的区别是,虚拟机栈执行Java方法,本地方法栈执行native方法。在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将虚拟机栈和本地方法栈一起使用。

  程序计数器是一个很小的内存区域,不在RAM上,而是直接划分在CPU上,程序猿无法操作它,它的作用是:JVM在解释字节码(.class)文件时,存储当前线程执行的字节码行号,只是一种概念模型,各种JVM所采用的方式不一样。字节码解释器工作时,就是通过改变程序计数器的值来取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术区完成的。

  正题

  java类加载过程大的方面可以分为三个阶段:加载、连接、初始化,其中连接又可细分为验证、准备、解析三个部分,JVM就是按照上面的顺序一步一步的将字节码文件加载到内存中并生成相应的对象的。首先将字节码加载到内存中,然后对字节码进行连接,连接完毕之后再进行初始化工作。

  1、加载阶段

  类的加载由类加载器执行,指的是将类的.class文件中的二进制数据读入内存中,将其放在运行时数据区域的方法区内,然后在内存中创建java.lang.Class对象(并没有明确规定是在java堆中,对于HotSpot虚拟机来说,Class对象比较特殊,虽然是对象,但是存放在方法区内),用来封装类在方法区的数据结构.只有java虚拟机才会创建class对象,并且是一一对应关系.这样才能通过反射找到相应的类信息.我们上面提到过Class这个类,这个类我们并没有new过,这个类是由java虚拟机创建的。

  jvm自带有三种类加载器:

  1)根类加载器,使用c++编写(BootStrap),负责加载rt.jar

  2)扩展类加载器,java实现(ExtClassLoader)

  3)应用加载器,java实现(AppClassLoader) classpath

  这三种类加载器的关系是自上而下的,类的加载采用双亲委派机制,听着很高大上,其实很简单。比如A类的加载器是AppClassLoader(其实我们自己写的类的加载器都是AppClassLoader),AppClassLoader不会自己去加载类,而会委ExtClassLoader进行加载,那么到了ExtClassLoader类加载器的时候,它也不会自己去加载,而是委托BootStrap类加载器进行加载,就这样一层一层往上委托,如果Bootstrap类加载器无法进行加载的话,再一层层往下走。

  2、验证阶段

  验证阶段主要做了以下工作:

  1)将已经读入到内存类的二进制数据合并到虚拟机运行时环境中去。

  2)类文件结构检查:格式符合jvm规范-语义检查:符合java语言规范,final类没有子类,final类型方法没有被覆盖

  3)字节码验证:确保字节码可以安全的被java虚拟机执行.

  4)二进制兼容性检查:确保互相引用的类的一致性.如A类的a方法会调用B类的b方法.那么java虚拟机在验证A类的时候会检查B类的b方法是否存在并检查版本兼容性.因为有可能A类是由jdk1.7编译的,而B类是由1.8编译的。那根据向下兼容的性质,A类引用B类可能会出错,注意是可能。

  3、准备阶段

  java虚拟机为类的静态变量分配内存并赋予默认的初始值.如int分配4个字节并赋值为0,long分配8字节并赋值为0。  

  4、解析阶段

  解析阶段主要是将符号引用转化为直接引用的过程。比如 A类中的a方法引用了B类中的b方法,那么它会找到B类的b方法的内存地址,将符号引用替换为直接引用(内存地址)。

  5、初始化阶段

  主动初始化的6种方式:

  1)创建对象的实例:我们new对象的时候,会引发类的初始化,前提是这个类没有被初始化。

  2)调用类的静态属性或者为静态属性赋值

  3)调用类的静态方法

  4)通过class文件反射创建对象

  5)初始化一个类的子类:使用子类的时候先初始化父类

  6)java虚拟机启动时被标记为启动类的类:就是我们的main方法所在的类

  同时我们需要注意下面几个点:

  1)在同一个类加载器下面只能初始化类一次,如果已经初始化了就不必要初始化了;

  2)在编译的时候能确定下来的静态变量(编译常量),不会对类进行初始化;

  3)在编译时无法确定下来的静态变量(运行时常量),会对类进行初始化;

  4)如果这个类没有被加载和连接的话,那就需要进行加载和连接;

  5)如果这个类有父类并且这个父类没有被初始化,则先初始化父类.

  6)如果类中存在初始化语句,依次执行初始化语句.

  初始化步骤(无父类):

  类的静态属性-->类的静态代码块-->类的非静态属性-->类的非静态代码块-->构造方法

  初始化步骤(有父类):

  父类的静态属性-->父类的静态代码块-->子类的静态属性-->子类的静态代码块-->父类的非静态属性-->父类的非静态代码块-->父类构造方法-->子类非静态属性-->子类非静态代码块-->子类构造方法

  扩展问题

  1、为何要双亲委派机制?

  判断两个类相同的前提是这两个类都是同一个加载器进行加载的,如果使用不同的类加载器进行加载同一个类,也会有不同的结果。我们可以自定义一个类加载器来测试一下

package classload;

import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader {

    public static void main(String args[]) throws ClassNotFoundException,IllegalAccessException, InstantiationException {
        ClassLoader loader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {

                String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                InputStream inputStream = getClass().getResourceAsStream(fileName);
                if (inputStream==null)
                    return super.loadClass(name);
                try {
                    byte[] bytes = new byte[inputStream.available()];
                    inputStream.read(bytes);
                    return defineClass(name,bytes,0,bytes.length);

                } catch (IOException e) {
                    e.printStackTrace();
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object ob =loader.loadClass("classload.MyClassLoader").newInstance();
        System.out.println(ob instanceof classload.MyClassLoader);
    }
}

  运行代码,可以发现我们通过自己的类加载器加载的类所创建的对象并不是"classload.MyClassLoader"的一个实例,为什么?因为jvm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加载器加载的,而我们却指定了自己的加载器,当然加载出来的类就不相同了。如果没有双亲委派机制,会出现什么样的结果呢?比如我们在rt.jar中随便找一个类,如java.util.HashMap,那么我们同样也可以写一个一样的类,也叫java.util.HashMap存放在我们自己的路径下(ClassPath).那样这两个相同的类采用的是不同的类加载器,系统中就会出现两个不同的HashMap类,这样引用程序就会出现一片混乱。

  2、如何定义自己的类加载器

  所以我们定义自己的类加载器,通常是这样做的:(1)继承ClassLoader  (2)重写findClass()方法  (3)调用defineClass()方法

package classload;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

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=getBytes(name);
                
                if(classDate==null){}
                
                else{
                    //defineClass方法将字节码转化为类
                    return defineClass(name,classDate,0,classDate.length);
                }
                
            } catch (IOException e) {
                
                e.printStackTrace();
            }
            
            return super.findClass(name);
        }
        //返回类的字节码
        private byte[] getBytes(String className) throws IOException{
            InputStream in = null;
            ByteArrayOutputStream out = null;
            //这里我们可以拿到类路径下以外的java文件,使用我们自定义类加载器进行加载,而默认的类加载器只能加载类路径下的.class文件
            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;
        }
}

  3、为什么要使用自定义的类加载器

  自定义类加载器的作用:jvm自带的三个加载器只能加载指定路径下的类字节码。如果某个情况下,我们需要加载应用程序之外的类文件呢?比如本地D盘下的,或者去加载网络上的某个类文件,这种情况就可以使用自定义加载器了。

  4、思考下下面程序的运行结果,

package classload;

public class TestClassLoad {
    public static void main(String args[]) {
        System.out.println(FinalTest.x);
        Singleton singleton = Singleton.getSingleton();
        System.out.println("counter1="+Singleton.counter1);
        System.out.println("counter2="+Singleton.counter2);
    }
}

class FinalTest {
    public static final int x = 6 / 3;
    static {
        System.out.println("FinalTest static block");
    }
}

class Singleton {
    private static Singleton singleton = new Singleton();
    public static int counter1;
    public static int counter2 = 0;

    private Singleton() {
        counter1++;
        counter2++;
    }

    public static Singleton getSingleton() {
        return singleton;
    }

}

  结果如下:

2
counter1=1
counter2=0

  至于为什么,可以在初始化阶段找到答案。

  参考网址:https://www.jianshu.com/p/b6547abd0706

          https://blog.csdn.net/briblue/article/details/54973413

  

 

 

 

推荐阅读