JVM学习笔记【JVM架构、class文件格式、方法区、类加载器(双亲委派)、编译解释执行】

目录

  • 前言
  • Class文件
  • 方法区
  • 类加载
    • 双亲委派
  • 编译解释执行

前言

  • 前端编译,将java文件编译成class文件。
  • 我们可以拿着这个文件到各种平台的jvm上运行,这就是java所谓的跨平台的语言。
  • 而jvm却也因此可以称为跨语言的平台,因为jvm面对的是class文件,而不是java文件,这意味着任何语言不管kotlin还是scala等,只要能被编译成class文件,jvm就能运行。所以jvm和java可以说没什么关系。

简单的看下jvm的整体架构(取自网络)
JVM学习笔记【JVM架构、class文件格式、方法区、类加载器(双亲委派)、编译解释执行】_第1张图片

Class文件

  • 既然jvm面对的是class文件,就需要简单看下class文件的内容到底是什么,那是怎样的格式。
  • 其实,class文件本身是一个二进制文件,里面的内容全是二进制的,但是它有固定的格式,比如最开始的两个字节表示模数(java是cafe babe,像其他文件比如jpg、png、txt等都有它们各自的模数,我们通过模数可以知道这是个什么文件),接下来的两个字节表示小版本号,再接下两个字节表示主版本号,我们可以根据这两个版本号可以知道目前我们使用的java版本是多少。还有其他的,比如类名、父类名、常量池信息、接口数、接口列表,方法数、方法列表等信息。
  • 具体哪部分对应的是类名,哪部分对应的是接口列表,查官网文档即可。
  • 一般我们使用jclasslib这个插件,可以可视化这些信息。而不用汇编、或二进制、十六进制去表现这些信息。也许你可能遇到jclasslib显示乱码,解决方案

方法区

方法区是逻辑上的概念,其落地实现在jdk1.8之前叫永久代,jdk1.8及其之后叫元空间metaspace
其保存在着被加载过的每一个类的信息;

类加载

三个阶段

  1. 加载loading
  2. 链接linking(准备、验证、解析)
  3. 初始化initalizing
  • 加载阶段,就是将class文件加载到内存中,这里就要使用类加载器了。
  • 链接阶段,分为三个小阶段(准备、验证、解析) ,准备阶段是检查class文件格式是否正确,验证阶段为静态变量赋默认值,一般为0,null,false等,解析阶段将符号引用替换为直接引用。
  • 初始化阶段,为静态变量赋我们给的值,执行静态代码块。

有四种加载器,分别是:

  1. 启动类加载器,BootstrapClassLoader
  2. 扩展类加载器,ExtClassLoader
  3. 应用类加载器,AppClassLoader

图示(以下内容都为jdk1.8版本)
JVM学习笔记【JVM架构、class文件格式、方法区、类加载器(双亲委派)、编译解释执行】_第2张图片

  • 启动类加载器,可以加载rt.jar或charset.jar等lib里的类
  • 扩展类加载器,则是可以加载ext文件夹下的类
  • 注意,hot spot是懒加载,这表示不会一次性将加载器可以加载的类全都加载到内存中,而是使用需要用的时候才会进行加载。

图示 jdk1.8:
JVM学习笔记【JVM架构、class文件格式、方法区、类加载器(双亲委派)、编译解释执行】_第3张图片

  • 而应用类加载器则是加载在你项目里的输出文件夹下,即你项目编译好后的class文件存放的位置。

图示 java web 的项目
JVM学习笔记【JVM架构、class文件格式、方法区、类加载器(双亲委派)、编译解释执行】_第4张图片
图示 普通java项目
JVM学习笔记【JVM架构、class文件格式、方法区、类加载器(双亲委派)、编译解释执行】_第5张图片

双亲委派

在讲自定义类加载器之前,先说一下双亲委派机制。

  • 有一个类需要加载,此时应用类加载器先看看该类有没有加载过,若没有,则先委托器父加载器(即扩展类加载器)加载,扩展类加载器也是同样的步骤,先看看该类有没有加载过,若没有,则先委托器父加载器(即启动类加载器)加载。
  • 启动类加载器为顶级父加载器,他不会进行委托,先看看有没有加载,若没有,则尝试加载,然后发现需要加载的类在它搜索的范围即(lib文件夹下)没有,则让扩展类加载器自己加载,而扩展类加载器也没有在它搜索的范围即(/lib/ext文件夹下)搜索到该class文件,则让应用类加载器自己加载,应用类加载器在自己搜索的范围内找到则加载,若每找到则抛异常(ClassNotFount)
  • 需注意的是父加载器,不是父类,它们之间没有继承关系,只不过有一个成员变量parent指向了其它类加载器,表示它是自己的父加载器。
  1. 自定义类加载器(所有类加载必须是ClassLoader的子类,或者其子类的子类等,嗯,启动类加载器是c++写的,不做讨论)

如何自定义类加载器,看一下ClassLoader源码

//当我们调用ClassLoader该方法时,传入类的完全限定名,将进入其重载方法。
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return this.loadClass(name, false);
}

//类加载器通过这个方法完成双亲委派模型机制的实现
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized(this.getClassLoadingLock(name)) {
    //首先看看是否已经加载过该类了
        Class<?> c = this.findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();

            try {
                if (this.parent != null) {
		//若没有加载过,先交给其父加载器加载
                    c = this.parent.loadClass(name, false);
                } else {
                    c = this.findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException var10) {
            }

            if (c == null) {
                long t1 = System.nanoTime();
                //若走到这一步,说明需要本加载器去加载,findClass方法是真正的加载类的方法
                c = this.findClass(name);
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }

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

        return c;
    }
}

//自定义类加载器,需要做的就是继承ClassLoader类,重写这个方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
  • 看看AppClassLoader是如何重写findClass方法,以及如何指定父加载器?
    (嗯,看源码的话,AppClassLoader是Launcher的静态内部类,ExtClassLoader也是,所以打印它们的名称时会有个 ‘ $ ’,如sun.misc.Launcher$AppClassLoader@18b4aac2
AppClassLoader(URL[] urls, ClassLoader parent) {
	//super构造方法指定parent
    super(urls, parent, factory);
    ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
    ucp.initLookupCache(this);
}
  • 嗯,AppClassLoader继承了URLClassLoader,而URLClassLoader则是实现了findClass,而AppClassLoader没有重写它。所以看看URLClassLoader是怎么重写ClassLoader的findClass方法的。
protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                //找路径
                    String path = name.replace('.', '/').concat(".class");
                    //加载至内存,就需获取文件流一样
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                        //自定义类加载器,重写方法的时候,必须调用该方法defineClass,它的作用是根据加载到内存中的数据,生成class对象。
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

注意defineClass有许多重载,看看其最终执行的defineClass

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

编译解释执行

接下来讨论以下java的编译执行和解释执行。

我们可以设置jvm纯编译执行(启动慢,因为要编译,但执行快),纯解释执行(启动快,但执行慢,因为要一条一条解释),混合执行(综合效率高,具体是将热点代码即时编译保存着,需要的时候直接执行即可,而出现次数少的代码一条一条解释执行即可,因为java解释器解释执行的速度比直接执行慢不了太多。)
怎么判断热点代码:
有个计数器,当每段代码在一段时间内执行的频率高过阈值,则进行即时编译。

你可能感兴趣的:(#,JVM,java,jvm,学习笔记,类加载器,class)