带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器

编程语言

演化:

机器语言->编程语言->高级语言(java,c++,Go,Rust等)

面向过程–面向对象-面向函数

java是一种面向对象、静态类型、编译执行,有VM(虚拟机)/GC和运行时、跨平台的高级语言。重点:VM(虚拟机)/GC(Garbage Collector)和运行时、跨平台。

带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器插图1

跨平台步骤字节码文件被虚拟机加载(类加载器)加载到内存中,转换成具体的对象

带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器插图3

字节码

结构:

Java byteCode由单字节(byte)指令构成,理论上最多支持256个操作码(opcode)。实际上java只使用了200左右的操作码,其他留给了调试操作。

根据指令的性质大概分为四大类:

1.栈操作指令,包括与局部变量交互的指令,

2.程序流程指令,

3.对象操作指令,比如方法调用的指令,

4.算数运算以及类型转换的指令,

运行步骤:

带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器插图5

JVM是一个基于栈的计算机,每个线程都有独属于自己的线程栈(JVM Stack),用语存储栈帧。每次调用方法就会自动创建一个线程栈。栈帧是由操作数栈、局部变量表以及一个class引用组成,class引用中又包含着我们使用的常量池

操作demo:https://juejin.cn/post/7141206840456511496/

类加载器

带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器插图7

类生命周期的七个步骤:

1.加载:找到class文件;

2.验证:验证字节码文件格式是否正确、依赖是否完备;

3.准备:静态字段、方法表;

4.解析:符合解析为引用;

5.初始化:构造器,静态变量赋值,静态代码块;

6.使用

7.卸载

前五步是我们通常所说的类加载过程,其中2、3、4可以合在一起称为-链接:

1 找到class文件,读出来2 验证格式,解析字段方法,所有符号转化为实际引用3 类相关初始化

类的加载时机:

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生):

1.3.1 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。

1.3.2 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。

1.3.3当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

1.3.4 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;

1.3.5 当使用 JDK.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;

以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:

通过子类引用父类的静态字段,不会导致子类初始化。

通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。

常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

总结:显式,隐式隐式,子类父类,实现类和接口,反射,动态调用显式,main方法,new,静态字段和方法

三类加载器和特点:

带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器插图9

1.启动类加载器(BootstrapClass Loader)

  • 这个类加载使用C/C++语言实现,嵌套在JVM内部

  • 它用来加载JAVA的核心库(JAVA_HOME/jre/lib/rt.jar,resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类

  • 并不继承自Java.lang.ClassLoader,没有父加载器

  • 加载扩展类和应用程序类加载器,并指定为它们的父类加载器

  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类

  • 启动类加载器不像其他类加载器有实体,它是没有实体的,JVM将C++处理类加载的一套逻辑定义为启动类加载器。因此,启动类加载器是无法被Java程序调用的。

2.扩展类加载器(Extension Class Loader)

  • java语言编写,由sun.misc.Launcher$ExtClassLoader实现

  • 派生于ClassLoader类

  • 父类加载器为启动类加载器

  • 从Java.ext.dirs系统属性所指的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

public static void main(String[] args) {    ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();    URLClassLoader urlClassLoader = (URLClassLoader) classLoader;    URL[] urls = urlClassLoader.getURLs();    for (URL url : urls) {        System.out.println(url);    }}

3.应用程序加载器(系统类加载器,System Class Loader/App Class Loader)

  • java语言编写,由sun.misc.Launcher&AppClassLoader实现

  • 派生于ClassLoader类

  • 父类加载器为扩展类加载器

  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库

  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载

  • 通过ClassLoadergetSystemClassLoader()方法可以获得到该类加载器

public static void main(String[] args) {    String[] urls = System.getProperty("java.class.path").split(":");    for (String url : urls) {        System.out.println(url);    }    System.out.println("================================");    URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();    URL[] urls1 = classLoader.getURLs();    for (URL url : urls1) {        System.out.println(url);    }}

4.用户自定义类加载器

在Java的日常应用程序开发中,类加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

1、开发人员可以通过继承抽象类java.lang.classLoader类的方式,实现自己的类加载器,以满足一些特殊的需求

2、在JDK2.0之前,在自定义类加载器时,总会去继承classLoader类并重写loadclass ()方法,从而实现自定义的类加载类,但是在JDK2.0之后已不再建议用户去覆盖loadclass ()方法,而是建议把自定义的类加载逻辑写在findclass ()方法中

3、在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass ()方法及其获取字节码流的方式,这样会让自定义类加载器编写更为简单一些。

带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器插图11

双亲委派

双亲委派机制的原理:

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。

如果父类的加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终达到顶层的启动类加载器。

如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制。

优点:

避免类的重复加载,确保一个类的全局唯一性

保护程序安全,防止核心API被随意篡改

缺点:

  • 无法做到不委派

  • 无法做到向下委派

在某些场景下双亲委派制过于局限,所以有时候必须打破双亲委派机制来达到目的。例如:SPI机制,这个SPI机制涉及到打破双亲委派机制,工作中没有涉及到就不细说了,感兴趣的同学可以自己研究下。

双亲委派在JVM中的实现代码:

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) {                    // this 是AppClassLoader, this.parent是ExtClassLoader                    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;    }}

这一期的课程大概就讲了这么多吧,说实话看完还是好多记不住和不理解,也是反复记忆并且查了好多资料才知道,所以不理解很正常,没有接触过就能一遍看懂的一般都是高级及以上了,慢慢看就可以了。看一点就是进步。

下期这周末写,大概是内存模型和JMM的相关知识,小伙伴可以先复习下,然后查漏补缺。

创作不易,如果这篇文章对你有用,请点个赞谢谢(ω)!

带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器插图13
举报/反馈

原创文章 带你手撸万元java进阶课程:jvm基础知识、字节码、类加载器,版权所有
如若转载,请注明出处:https://www.itxiaozhan.cn/202211150.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注