深入理解JVM虚拟机

一、Compile Once,Run Anywhere如何实现

首先使用javac命令将.java文件编译成字节码文件也就是.class文件,之后交给JVM虚拟机去执行,从而转换成特定平台的执行指令,这也是为什么一份相同的.class文件可以在不同的操作系统上运行的原因,下图是一张java项目从编译到执行的流程图:
在这里插入图片描述

二、JVM如何加载.class文件

下图为java虚拟机的运行结构,分别由Class Loader、Execution Engine、Native Interface和Runtime Data Area组成,首先由Class Loader会读取特定格式的class文件,然后交给Execution Engine来对其解析,解析完成之后在通过Native Interface融合一些第三方类库,最后放到Runtime Data Area去运行即可;

Class Loader:依据特定格式,加载class文件到内存中;
Execution Engine:对命令进行解析;
Native Interface:融合不同语言的开发库为Java所用;
Runtime Data Area:JVM内存空间结构;
在这里插入图片描述

三、问类从编译到执行一共需要几步?

假设现在有一个Demo.java文件,那它从编译到执行总共只需要三步即可:

1.编译器将Demo.java文件编译成Demo.class字节码文件;
2.ClassLoader将字节码转换为JVM中的Class(Demo)对象;
3.JVM利用Class(Demo)对象实例化为Demo对象;

四、ClassLoader到底是啥?

ClassLoader在Java中有这非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。他是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。

五、ClassLoader种类

BootStrapClassLoader:C++编写,加载核心库java.*
ExtClassLoader:Java编写,加载扩展库javax.*
AppClassLoader:Java编写,加载程序所在目录
自定义ClassLoader:Java编写,定制化加载

六、自定义ClassLoader实战

理论讲的在多,还是不懂啥是ClassLoader,该怎么用,那么接下来博主带领大家编写自己人生中的第一个ClassLoader(是不是第一无所谓0.0),让你感受Java真正的魅力:

首先创建一个.java文件,放置在项目以外,然后将该文件编译成.class字节码,下图为.java文件内容:

public class ClassLoaderDemo {
	//.java文件内容只需要一个static代码块即可,当该类在进行实例化时会自动执行代码块中的内容;
    static{
        System.out.println("Hello, I'm loaded by a custom classloader");
    }
}

字节码文件地址如下图,项目在RocketMQ中(不要问为什么在这里面,我也不知道):
在这里插入图片描述
接着就是编写我们自己的ClassLoader啦(有点小激动):

package com.intretech.classloader;

import java.io.*;

/**
 * 自定义ClassLoader
 * 主要实现两个方法即可:
 * 第一个是findClass方法,用于加载.class字节码文件
 * 第二个是defineClass方法,用于定义Class,当前自定义ClassLoader使用默认实现的方式即可
 */
public class MyClassLoader extends ClassLoader{

    private String name;
    private String path;

    public MyClassLoader(String name,String path){
        this.name=name;
        this.path=path;
    }

    /**
     * 寻找需要加载的.class字节码文件
     * @param name .class字节码文件的名称
     * @return
     */
    @Override
    public Class findClass(String name){
        //加载.class字节码文件
        byte[] b=loaderClassData(name);

        //使用ClassLoader默认实现好的定义Class方法即可
        return defineClass(name,b,0,b.length);
    }

    /**
     * 加载.class字节码文件
     * @param name .class字节码文件的名称
     * @return
     */
    private byte[] loaderClassData(String name) {
        name = path+name+".class";
        InputStream in=null;
        ByteArrayOutputStream out=null;
        try{
            in = new FileInputStream(new File(name));
            out = new ByteArrayOutputStream();
            int i=0;
            while ((i =in.read())!= -1){
                out.write(i);
            }
            return out.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                in.close();
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

自定义ClassLoader写好了,接下来咱们来使用他吧:

package com.intretech.classloader;

/**
 * 自定义ClassLoader测试使用类
 */
public class MyClassLoaderTest {
    
    public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        //创建MyClassLoader对象,name随便填,只是个名称,path填需要加载的.class文件所在地址
        MyClassLoader myClassLoader = new MyClassLoader("myClassLoader", "F:\\YQ\\HolyGrail\\");

        //通过findClass定义出ClassLoaderDemo类
        Class classLoaderDemo = myClassLoader.findClass("ClassLoaderDemo");

        //实例化对象,并且会执行static代码块中的内容;即会在控制台打印"Hello, I'm loaded by a custom classloader"
        classLoaderDemo.newInstance();
    }

}

很简单吧,其实就是先通过findClass方法将.class文件加载成byte[],然后交给defineClass方法去执行得到对应的类,得到类之后就能为所欲为了0.0;

七、ClassLoader的双亲委派机制

加载一个类是有规则的,不是一通乱加载,首先是自底向上的,先从Customer ClassLoader查找是否加载过该类,有的话直接返回就好了,如果没有,那就交给他的上级,也就是App ClassLoader去查找,以此类推,一直到最顶层的Bootstrap ClassLoader,如果都没有加载过(好家伙,新来的),这时候就开始另一种方式了,也就是图中自项向下去尝试加载,首先是Bootstrap ClassLoader(因为现在球在它这嘛,所以当然是他先了),Bootstrap ClassLoader会去rt.jar中或者是指定的jar(这个位置可以自定义哦,方式就是启动jar包的时候加上-Xbootclasspath “路径”)去查找这个类,如果找到了这个类,那就加载完返回即可,如果没有,那就交给下级,去它们自己负责的路径里面查找有没有这个类,以此类推,如果说最终都没有找到这个类,好了,别启动了,直接给老子报异常,ClassNotFoundException,老老实实去解决吧。
在这里插入图片描述
说了一大堆,可能有些小伙伴没有听懂,没有关系,接下来咱们通过一段代码来看看ClassLoader到底是怎么加载的,是不是和上图的方式一致的,看下博主到底有没有在瞎说(也不是没有可能,所以下面一段代码认真看哦):

 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 		//加锁
        synchronized(this.getClassLoadingLock(name)) {
        	//先让当前ClassLoader去查找,是否有加载过该类
            Class<?> c = this.findLoadedClass(name);
            //没有的话,那就进来吧你
            if (c == null) {
                long t0 = System.nanoTime();

                try {
                	//先判断是否有上级,也就是判断当前的ClassLoader是不是Bootstrap ClassLoader
                    if (this.parent != null) {
                    	//上级不为空,好办,让上级去查找
                        c = this.parent.loadClass(name, false);
                    } else {
                    	//上级为空了,说明是Bootstrap ClassLoader,那就专门调用orNull方法了
                    	//因为如果在没有找到说明就没有加载过嘛,当然就得返回null了
                        c = this.findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException var10) {
                }
				//因为上面第一种方式没有找到该类,接下来开始第二种方式了
                if (c == null) {
                    long t1 = System.nanoTime();
                    //通过自项向下的方式去尝试加载该类了
                    c = this.findClass(name);
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }

            if (resolve) {
                this.resolveClass(c);
            }
            
            return c;
        }
    }

这个注释加的可以吧,这下应该知道博主没有在瞎说了吧!0.0

八、loadClass和forName的区别

这两个家伙都是显式加载类的方式,你们如果不认识,那你们一定认识他们的亲兄弟——new,new是隐式加载类的方式,new出来的实例可以直接使用,而loadClass和forName加载的类需要在通过newInstance方法得到对应的实例才可使用,言归正传,那loadClass和forName到底有什么区别呢,咱们先了解一下类的装载过程(也有人叫加载过程,一样的):
如下图,类的装载过程分为三步,loadClass装载类时只会到第二步,而forName会到第三步,这就是他们的区别,也就是loadClass不会初始化,而forName会。
在这里插入图片描述
可能你们又认为博主在乱说,接下来上证据:

首先上的是loadClass源码,可以发现,resolve默认传的false,所以不会去执行resolveClass方法,这个方法从注释上来看就知道是要链接该类的意思,所以,loadClass在加载一个类时,只会进行到链接,根本到不了初始化;

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    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;
        }
    }

/**
     * Links the specified class.  This (misleadingly named) method may be
     * used by a class loader to link a class.  If the class <tt>c</tt> has
     * already been linked, then this method simply returns. Otherwise, the
     * class is linked as described in the "Execution" chapter of
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @param  c
     *         The class to link
     *
     * @throws  NullPointerException
     *          If <tt>c</tt> is <tt>null</tt>.
     *
     * @see  #defineClass(String, byte[], int, int)
     */
    protected final void resolveClass(Class<?> c) {
        resolveClass0(c);
    }

我们在来看下forName的源码:
重点关注initialize这个参数,它代表着是否要初始化,默认传的是true,所以我们可以清楚的判断出来forName加载类时是会进行初始化的;
在这里插入图片描述
有些小伙伴这时候会提问了,为什么需要有这两种方式,直接弄一种初始化的不就好了吗?(好问题),这是因为这两种方式都各有大用,比方说你在加载jdbc驱动时,你就需要去加载源码中的静态代码块,这时你就需要使用forName,loadClass在Spring IOC中则被大量使用,因为Spring IOC为了加快加载速度,使用了大量的懒加载,所以loadClass在加载速度方面的优势就提现出来了,所以我们可以知道一个道理:存在即合理;

九、Java内存模型

咱们先来看看一张老图,请各位小伙伴先思考一下,Java内存模型指的是下图中的哪个部分(等待10秒),聪明的小伙伴应该已经找到了,就是Runtime Data Area(你找对了吗);
在这里插入图片描述
上图仅仅是个开胃菜,想要真正搞懂Java内存模型还需要继续深入,本章使用JDK8的Java内存模型来讲解(其实后面的JDK9,JDK10…都是类似的),我们可以从两个角度去理解,一个是线程的角度,另一个就是存储的角度,咱们先从线程的角度来see see;

如下图所示,线程可以分为线程私有和线程共享,像程序计数器、虚拟机栈、本地方法栈属于线程私有的,每个线程对应一个,而MetaSpace(元空间)、常量池和堆就属于线程共享的了,所有线程一起使用,看到这,详细大家都有同一个感觉就是:“懂了,但没完全懂”,没关系,博主接下来为各位小伙伴解释一下它们到底是个什么鬼;

程序计数器(Program Counter Register):当前线程所执行的字节码行号指示器(逻辑计数器),通俗点说,就是记录当前线程运行到第几行代码了,如果需要执行下一行代码,那就改变计数器的值就可以了,因为cpu在特定时间只能处理一个线程(所谓的多线程其实是cpu在不停的切换线程执行,速度快到让你认为是在一起执行,但其实实质上还是单线程,除非你是多核),所以为了记录每个线程的执行位置,程序计数器和线程都是一对一关系即“线程私有”,并且线程技术器只对Java方法计数,如果是Native修饰的方法计数器的值为Undefined,因为计数器只几率行号,所以根本不用担心内存泄露问题哦!

虚拟机栈(Stack):Java方法执行的内存模型,包含多个栈帧,栈帧用于存储局部变量表,操作栈,动态链接和方法出口等信息,当调用方法结束了,栈帧就会被删除,局部变量表包含方法执行过程中的所有变量,操作数栈包括入栈、出栈、复制、交换、产生消费变量的操作,大部分jvm字节码都把时间消耗在操作数栈的操作上,栈是一个后进先出的数据结构,除了入栈和出栈,栈帧不能直接被操作;

本地方法栈:和虚拟机栈是类似是,不同的是本地方法栈是作用于有native修饰的方法,比如上文说的forName方法使用的就是本地方法栈;

MetaSpace(元空间):所有线程共享,存储类的加载信息;

堆(Heap):所有线程共享,主要是用来存放对象实例,同时也是GC管理的主要区域;
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值