java系列 JVM类加载和内存模型

前言

本文我们会着重讲解JVM的类加载机制以及我们会通过源码去分析类加载机制和如果自定义类加载,还有如何打破双亲委派等,当我们理解了类加载机制之后就会进入JVM的下一个环节那就是内存模型,我们也会讲到内存模型中栈的执行机制,还有内存模型中有哪些东西以及都是什么作用,学习这些理论知识也不是一无是处他在我们学习下一章如何进行JVM调优之前这些理论知识可以说是必备的要不你调什么优,那就让我们一起来看吧! 接下来我们会根据下面这个图进行展开讲解

image

从上述图可以看出我们jvm一共分为类装载 , 运行时数据区 , 字节码执行引擎三大块,每一块具体什么作用以及什么原理,也就是我们本文着重要讲解的东西,大致知识点我先给大家罗列一下

  • 类装载子系统
    • 类加载到内存中都有那些步骤
    • 有哪些类加载器
    • 什么是双亲委派机制
    • 如何自定义自己的类装载器
    • 如何打破双亲委派机制
    • 为什么要打破双亲委派机制,如Tomcat
  • 运行时数据区(内存模型)
      • 存放的什么数据
      • 与方法区和栈有什么关联
    • 栈(线程)
      • 栈执行原理
    • 本地方法栈
      • 本地方法栈就是用来执行本地方法专用的一块内存空间,本地方法也就是native修饰的用c++实现的方法
    • 方法区(元空间)
      • 存放那些数据
    • 程序计数器
      • 程序计数器有什么用
      • 为什么会有程序计数器
  • 字节码执行引擎
    最后我们会做一个小结来叙述当一个项目启动以后系统都做了哪些操作,将我们上述学到的进行一个串联!

    类装载子系统

    image

当执行java命令时

java test.class

java.exe 会调用底层的jvm.dll文件(C++实现的类库相当于java的jar包的意思)去创建一个java虚拟机,然后再创建一个引导类加载器,然后会使用该引导类加载器去加载使用java实现的Launcher类,该类会去加载初始化其他的类加载器如etx加载器,app加载器.根据加载器管理的区域进行加载class文件这也就是类加载子系统的职能!

类加载到内存中经历那些步骤

一个类如何加载到内存中呢,他经历了那些步骤呢?

image

加载 >> 验证 >> 准备 >> 解析 >> 初始化

加载

在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,特殊的就是launcher类,他是c++使用bootstrap加载器加载的,也是项目启动加载的第一个java类

验证

校验字节码文件的正确性,当我们java程序生成class文件后,是可以进行编辑的,也就会导致class文件不一定是正确的,可能我们会使用记事本打开class文件修改一些东西,导致class文件格式不正确,所以jvm在加载class文件的时候会进行验证字节码文件是否有错误!

准备

当字节码文件没有问题之后就会读取该字节码文件,第一步读取的就是字节码文件中声明的静态变量并给这些静态变量分配内存,并赋予默认值. 注意 : final修饰的会直接赋于真实的值,而静态变量会先赋于默认值

解析

符号引用替换为直接引用,该阶段会把一些静态方法的符号引用替换为指向数据所存内存的指针或者句柄等;

大致意思就是你可以通过javap -v 去汇编一个class文件生成一个指令文件

javap -v demo.class

image

javap指令输出的数据就是demo.class反汇编生成的指令,内存就会根据这些指令一行一行的执行这个class文件,具体怎么执行后续我会专门出一个文章来讲解,我们这里只需要明白这里的每一个指令都是符号引用,在jvm看来这些数据都是符号就是红框框中的数据,解析以后就会把静态方法中这些符号引用转换成直接引用也就是会把这些符号都转换成这些数据在内存中的指针或者地址,大致就这个意思,更详细就需要深抛c++代码去理解这些概念了!

初始化

对类的静态变量初始化为指定的值,执行静态代码块!

有哪些类加载器以及类加载器如何初始化的

我们从上述可知在jvm虚拟机启动以后C++会通过bootstrap类加载器去加载Launcher类,那我们怎么知道Launcher会初始化什么类加载器,这些在源码里面都可以看见,如下

image

类如何加载到jvm内存中我们提到了,他会经历初始化,初始化就是会对静态变量进行赋值并运行静态代码块,那Launcher类在加载到jvm内存中的时候,就会执行new Launcher,那就会执行Launcher方法里面的代码,Launcher方法中我们可以看见,调用了extclassloader和appclassloader,那我们想一下,这俩方法肯定会创建两个类加载器吖,不妨我们看一下其中一个方法的源码extclassloader

image

具体Extclassloader方法做了什么,上图我们知道了var1的值是什么就是ext类加载器负责加载那些路径的class文件,你也可以点getExtDirs()方法进去看一下,会有一个路径

image

然后他会将这些路径作为参数传给ExtClassLoader方法,而ExtClassLoader方法会通过父类的方法(URLclassloader)去设置Ext类加载器负责加载那些路径的class文件,最后会通过Classloader类去配置这个类加载器的父类加载器是谁,不过这里是null因为ext类加载器的父类加载器是bootstrap,是由c++实现的所以java这里是不会显示的!

image

一个类加载器的初始化也就是创建一个classloader对象并配置上他的父加载器并告诉他负责加载那些路径的class文件,就可以了.这里我们讲这些是为了最后我们自定义类加载的时候做铺垫

从上述源码中我们知道Lanucher会初始化两个类加载器,一个是ext类加载器一个app类加载器,同时我们也可以自己去创建类加载器,所以一共有4种类加载器

  • bootstrap类加载器(引导类加载器)
  • extclassloader
  • appclassloader
  • 自定义加载器

    bootstrap类加载器(引导类加载器)

    也就是最初c++实现的根加载器,用来加载java最核心的类库,具体他会加载以下路径的class文件,如上述我们讲到的launcher(在rt.jar包中)

image

可以看出来他加载的主要就是lib目录下的class文件,比较著名的就是rt.jar包了,比如java.lang包他就在rt.jar包中

extclassloader类加载器(扩展类加载器)

主要加载lib.ext包下的class文件

image

appclassloader类加载器(应用类加载器)

主要加载我们项目自己写的那些class文件,也就是我们的项目差不多百分之95的类都是使用这个类加载器进行加载的

image

image

类加载器之间的关系

上述三个类加载器都是都是系统初始化出来的,那他们三个关系是什么样的呢,这里我们就又要引入一个新的东西就是系统初始的类加载器是那个

image

可以看出来系统默认类加载器是app类加载器,他的父类加载器是ext,而ext的父类加载器是null,为什么是null在上面看ext类加载器创建的源码的时候也说了,他父类加载器是C++写的,java看不见,这里我们没有自定义类加载器所以就没打印,其实自定义的类加载器的父类加载器是app类加载器,后面我们会在自定义类加载器的时候看一下源码.

那有的小伙伴就疑惑了为什么这四个类加载器会建议关联呢,那是因为"双亲委派机制"那什么是双亲委派机制呢

什么是双亲委派机制

image

双亲委派机制大致意思就是当一个class文件要加载到内存中的时候,自定义类加载器会先去查看自己之前有没有加载过该class文件,如果加载过那自定义类加载器就直接把该class文件读取到内存中了,如果之前没有加载过的话就会委托他的父类加载器(AppClasssLoader)去进行加载,AppClassloader和自定义加载器一样也是会看一下自己是否加载过该class文件如果有直接读取到内存如果没有就委托他父类加载器这样子直到Bootstrap类加载器,如果bootstrap类加载器也没有加载过该class文件那boot类加载器就会去他管辖的类路径去找这个class文件,找见了就会通过io读取该class文件并加载到内存中,如果没有就委托他的子类加载器去他管辖的区域找,找到了就加载没找到就委托他的子类加载器,直到找见该class文件,然后加载进内存;

双亲委派机制源码

image

上面这个方法就是双亲委派机制的整体代码,后续我们要打破双亲委派机制也是通过重写该方法实现的,

自定义一个类加载器

自定义加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法,一个是loadClass(String,boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要重写findClass方法

问题1 : 那有的小伙伴就疑问了为什么findClass是空方法那ext和app类加载器可以读取class文件呢?

因为我们自定义类加载器继承的是ClassLoader类,而ext和app类加载器他继承的是URLClassLoader,而URLClassLoader是重写了findClass方法的

image

image

那接下来我们就自定义一个类加载器吧

package com.demo.order;

import sun.misc.Resource;

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;

public class MyClassLoaderTest {

    static class MyClassLoader extends ClassLoader {

        private String classPath;

        //myclassloader的构造方法
        public MyClassLoader(String classPath){
            this.classPath = classPath;

        }

        //使用fileInputStrerm把class文件读成二进制流文件存到字节数组中
        private byte[] loadByte(String name) throws Exception{

            name = name.replaceAll("\\.","/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte [] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;

        }

        //重写findClass方法
        protected Class<?> findClass(String name) throws ClassNotFoundException{

            try {
                byte[] data = loadByte(name);
                return defineClass(name,data,0,data.length);//defineClass是ClassLoader中的方法,他会调用很多本地方法将这个class文件读取到jvm内存中
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }

        }

        //测试
        public static void main(String[] args) throws Exception {

            //初始化构造方法
            MyClassLoader myClassLoader = new MyClassLoader("D:/test");

            Class<?> clazz = myClassLoader.loadClass("com.demo.order.demo2");

            Object obj = clazz.newInstance();

            Method sout = clazz.getDeclaredMethod("demo", null);

            sout.invoke(obj,null);

            System.out.println(clazz.getClassLoader().getClass().getName());

        }
    }
}

image

当如果我们项目有同名的demo2类,那他会怎么样呢

image

如上述两张图可以看出来,自定义类加载器的父类加载器是app类加载器,也会执行双亲委派机制,那我们如何去打破双亲委派机制呢,其实也很简单,上面我们介绍双亲委派机制的时候应该看过双亲委派机制的源码了,我们也是只需要把双亲委派机制的方法进行重写就可以了

如何打破双亲委派机制

重写ClassLoader的loadClass方法即可

image

我们来试一下

image

看是不是双亲委派机制我们就给他重写成功了呀,当我们自定义加载器没有加载过object.class的话也不会让父加载器去加载了,而是直接自己去自己负责的class文件路径中寻找去了,这样子也就打破了双亲委派机制了,我们也就可以根据以上案例去根据业务重写双亲委派机制了!

为什么要打破双亲委派机制

这里我们就讲一个Tomcat案例,如果我们Tomcat里面运行了两个war包也就是两个java项目一个java项目是依据spring4,一个java项目是依据spring5,比如说,我是说比如,spring4和spring5中有一个class文件同名同路径但是内容不同,那类加载在加载这个类的时候是不是就会保存到自己已加载过的类的一个集合中呀,比如第一次加载的是spring5中的一个class文件,第二次是spring4来加载这个class文件,那类加载器一看自己已加载过的类发现有这个类同名同路径,就直接返回了,那么问题就来了,是不是spring4这个项目加载了spring5的class文件啊,那错误也就油然而生了,所以tomcat就不会遵守双亲委派机制,就要打破双亲委派机制,就是自己war包就由自己的加载器去加载不要委托给父类加载器,防止同名同路径的类文件读取错误

运行时数据区(JVM内存模型)

这一块也就是jvm最重要的一块了,因为jvm调优主要就是对这一块进行优化,那我们要调优就要先明确当JVM类加载器将一个class文件加载到JVM内存中以后,会发生什么,从而去了解到JVM内存中由那些东西构成并起到什么样的作用.

image

接下来我们就根据以下代码块结合上图来捋一下,当代码进入jvm内存中,都发生了什么

image

JVM内存运行机制

当我们运行main方法之后,类加载就会讲main方法存在的类加载进jvm内存中,在加载的准备阶段,就会给静态变量在方法区分配空间并执行静态代码块(在这里要注意一下,静态代码块不包含main方法,因为main方法是一个程序的入口),然后再初始化阶段给这些静态变量进行赋值,随后将class文件加载到jvm的方法区中,并在堆内存中生成一个对应的对象

image

然后字节码执行引擎就会根据二进制文件执行我们的项目,这里我们可以使用javap来对我们的程序class文件进行反汇编,让我们清楚字节码执行引擎是如何执行我们的代码的

javac demo.java
javap -c demo.class > demo.txt

image

从上述反汇编的文件中可以清晰看见数据结构和我们的demo一样,有f1方法 下面也有 main方法,但是其中的短句我们却看不太明白,其实很简单只要对应着我发布的JVM指令手册去看就可以了,这也是当class文件进入方法区内存以后,main方法就会运行,然后在栈中开辟一个内存块,一个线程对应一个栈内存,然后压入main方法栈帧,那为什么要一个方法对应一个栈帧呢,大家可以想一下,我们一个方法中的局部变量是不是都只能在当前方法中使用,那当我们把一个方法当做一个栈帧,然后栈帧中存放该方法的局部变量,然后当我们这个方法不使用的时候是不是就可以直接把这个栈帧给他清掉,那这个方法中存放的那些变量也就可以被同步清理掉了,所以这就引出来,栈帧中会存放我们当前方法中的局部变量,其中还有 操作数栈 , 动态链接 , 方法出口.也就会形成以下图

image

然后我们就可以根据上面反汇编出来的文档进行一步一步的解析,看一下main方法是如何在内存中执行的,我们先看main方法的第一个指令

image

我们只需要复制这个指令 'new' 然后打开我分享的JVM指令手册,然后按 CTRL+F , 然后粘贴进行查找

image

主要是查找指令,也就是前面有无序序号标记的,可以看见new的意思就是创建一个对象,并将其引用值压入栈顶,后面的#3 其实也是有对应值的这个反汇编显示不出来,你也可以使用javap -v demo.class去直接展示汇编内容,可能会展示出来#3对应的数据小伙伴们可以自己试一下;后面的#3也就是跳转到#3对应行号进行执行 , 那我们这里闭着眼睛也可以知道这不就new一个demo对象么,后面注释也有对象的路径,关于main方法指令下面我们就不一一展示了,下面无非也就是将f1方法压入栈顶,主要我们要研究f1里面的逻辑是如何在栈里面工作的,当f1压入栈顶以后,也就形成了下面的情况

image

然后我们看f1方法的第一句指令是什么,然后我们再去jvm指令手册里面去查指令的含义

image

image

将单字节的常量值推至栈顶,那也就是将后面的10常量推到栈顶,这里我们要注意,这里的栈顶也就是栈帧中操作数栈的栈顶

image

然后第二行指令 :

istore_1

image

对于非静态函数,第一变量是this,那第二变量是不是也就是我们方法中的第一个变量a吖,那就会在局部变量中开辟一块空间存放a变量,然后再将栈顶的10存弹出栈,存到a变量上,如图

image

然后我们在看第3个和第4个指令,这个就和上面一样,把20常量压入栈顶,然后再开辟在局部变量中开辟空间给b然后常量20弹出栈,赋值给b

bipush        20
istore_2

image

然后我们在看第5个指令

iload_1

image

上诉我们分析过第一个int变量是this,那第二个int型变量就是a么,a的变量是10,那就是再把常量10压入栈顶

image

然后再看第6个指令,和上面一样,就是将第3个本地变量压入栈,也就是b,b的常量是20

iload_2

image

然后看第7个指令

imul

image

将两个栈顶的int数值弹出栈,然后相乘,然后再将结果压入栈顶,这里要注意,JAVA栈也是遵循的FILO算法的先进后出,也就是会先将20弹出,然后再将10弹出,然后相乘等于200,然后再把常量200入栈

image

上述差不多就是栈的一个运行机制,后面的指令就不一一给大家演示了,大家可以根据自己条件去操作一下,其实在我们这些步骤之中还有一个非常重要的东西我们没有提到过那就是程序计数器,每一个线程对应一个栈,也同时每一个栈都会一个程序计数器,他记录着这个线程执行的位置,如果把我们上面反汇编文件每一条指令前的数字当做一个行号

image

那我们刚才执行的第7个指令也就是行号8了,注意 : 程序计数器他记录的值是即将执行的指令行号,那也就是在执行行号8指令的时候即将执行的指令是行号9,那程序计数器也就是记录的 9 了

image

那我们看行号9是什么指令,上述我们也查过这个指令,也就是把常数10压入栈顶,那当执行这个行号9的时候那程序计数器的值应该是多少呢,肯定小伙伴们就知道了呀,那肯定是行号9的下一个指令的行号11吖

bipush 10

image

经过上述一顿操作那我们前言提到的问题也都有答案了,接下来我们就对我们本文章做一个问题汇总

总结

image

  1. 类加载到内存中都有哪些步骤
    加载 -> 验证 -> 准备 ->解析 ->初始化
  2. 有哪些类加载器
    • bootstrap类加载器 - 也就是根加载器,用C++实现的
    • ext类加载器 - 用来加载lib/ext包下的class文件
    • app类加载器 - 用来加载用户自己写的class文件,也是最常用的,几乎项目中百分之95以上文件都是他加载的
    • 自定义类加载器 - 我们可以通过继承ClassLoader类然后重写他的findclass方法来自定义一个加载,然后通过反射来执行加载的类方法
  3. 什么是双亲委派机制
    双亲委派机制就是当需要加载一个class文件的时候会根据以下顺序进行委派 自定义类加载器 -> app类加载器 -> ext类加载器 -> bootstrap类加载器 , 也就是当加载一个class文件的时候会先让自定义类加载器去查看自己是否加载过该类如果加载过就直接使用自定义加载器进行加载,如果没有自定义加载器或者自定义加载器没有加载过那他就会委托他的父加载器app类加载器进行加载,app同理也是会查自己是否加载过该类,如果没有加载过就委托他的父类加载器etx类加载器去加载,也是同理ext类加载器去查看自己是否加载过该类如果加载过就加载返回,如果没有也是委托他的父类加载器bootstrap类加载器去加载,如果bootstrap类加载器也没加载过就直接去她管辖的包下去找是否有该类的class文件也就是lib包下,如果有就进行加载返回如果没有就委托他的子类加载器ext去加载,ext类加载器也是同上,他会去自己管辖的包下也就是lib/ext包下去找是否有该类的class文件,有的话加载返回没有就委托他的子类加载器app类加载器加载,app类加载器也是回去自己管辖的包下也就是你的项目包去找该类class文件,有就返回,没有就委托他的自定义类加载器,如果都没有找到class文件就会返回错误 class找不到错误,
  4. 为什么要设计双亲委派机制
    我们可以设想一下如果我们项目中写了一个java.lang.String这么的一个类文件,然而和java核心lib包下的String类文件同路径同名,那如果不设计双亲委派机制的话他会不会就直接使用我们的app类加载器去加载了,然后就会执行那个盗版的String类文件吖,那安全也就毫无可言了,那如果设计了双亲委派机制,那这些核心包就算你自己项目中有,那类加载器也不会去加载而是会委托给java的核心类加载器bootsrtap类加载器去加载,对吧!!!
  5. 如何打破双亲委派机制
    继承ClassLoader类然后重写他的loadClass方法,即可,但是如果你要使用这个去加载同名同路径的java核心包的话也是不可以的,那小伙伴就有疑问了为啥呀,我直接打破他直接使用我自己的类加载器去读到内存中不可以么,哈哈哈这就要提到一个java的修饰符protected,大家感兴趣可以去了解一下,
  6. 为什么要打破双亲委派机制
    为什么要打破双亲委派机制上面我已经汇总过了,大家可以直接在大纲中跳转去看,这里就不写了
  7. 堆存放的都是什么数据,与方法区和栈有什么关系以及方法区中都存放那些数据
    存放的都是我们的实例对象,然后栈中会存放内存地址去读取我们堆中的这些实例对象,当一个类加载到内存中以后会把该类的二进制文件存到方法区中,比如静态变量还有类信息都会在方法区中存储和堆差不多了,但是有和堆不一样,所以他还有另一个名字叫'非堆',同时也就是说,方法区的大小也就关系着你能运行多大的项目
  8. 程序计数器是什么有什么作用
    每一个线程程序计数器都给他开辟一个空间去记录这个线程即将执行指令的位置,那为什么要有程序计数器呢,大家可以设想一下如果你一个线程A在执行一个方法中的指令的时候执行到一半,另一个比较快的线程B也来执行了,那你线程A是不是就会暂停然后等线程B执行完以后你在执行,那如果没有程序计数器是不是线程A就不知道自己执行到哪里了就会重新执行,那肯定就不合理了对吧,所以需要程序计数器去记录线程A他即将执行的指令位置,同时也就映射出来就是线程A必然会执行完当前指令才会暂停,不会一个指令执行到一半然后给线程B让路!
  9. 字节码执行引擎
    上面我去解析反汇编文件的时候那一个一个指令就是字节码执行引擎去执行的,同时这里要提到一点,就是程序计数器的记录的即将执行的代码位置是谁在修改的,没错就是字节码执行引擎修改的,因为指令执行到哪里也就只有他知道,所以他还有一个职责就是在执行指令的同时要去修改程序计数器的值

通过以上文章描述,我们对jvm也有一个大致的了解了,当大家看明白了以上文章之后我们就要进入下一章了,也是我们用以上这么多内容做铺垫的东西,那就是 JVM如何调优 ,下一章我就会着重讲解我们企业中一般经常面临的JVM异常的处理方式,同时还有如何调优以及会展示一些案例让大家好理解,大家就拭目以待吧!

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇