JVM底层原理介绍

JVM结构图

在这里插入图片描述
黄色的是所有线程共享数据,存在垃圾回收。
灰色的是线程之间数据私有,不存在垃圾回收。

类的加载时机

  1. 创建类的实例。例如:new Person()
  2. 使用到类的静态成员时。
  3. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
  4. 初始化某个类的子类时,new子类先加载父类。
  5. 直接使用java.exe命令来运行某个主类。
    以上五种情况的任何一种,都可以导致JVM将一个类加载到方法区。

类加载器

虚拟机自带的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):用于加载系统类库<JAVA_HOME>\bin\rt.jar目录下的class。 (C++编写的)
  • 扩展类加载器(Extension ClassLoader):用于加载扩展类<JAVA_HOME>\lib\ext目录下的class。 (Java编写的)
  • 应用程序类加载器(Application ClassLoader):也叫系统类加载器,用于加载自定义的类和第三方的jar。

自定义的类加载器:

  • Java.long.ClassLoader的子类用户可自定义加载方式,例如:(Custom ClassLoader)。

三个类加载器的关系:

  • 虽然AppClassLoader的父加载器是ExtClassLoader
  • ExtClassLoader的父加载器是BootstrapClassLoader
  • 但是他们没有子父类继承关系,他们有一个共同的爹–>ClassLoader。
public class Demo {
    public static void main(String[] args) {
        //获取AppClassLoader
        ClassLoader classLoader = Demo.class.getClassLoader();
        System.out.println(classLoader);

        //获取ExtClassLoader
        ClassLoader classLoader1 = Demo.class.getClassLoader().getParent();
        System.out.println(classLoader1);

        //获取BootstrapClassLoader
        ClassLoader classLoader2 = Demo.class.getClassLoader().getParent().getParent();
        System.out.println(classLoader2);
    }
}

双亲委派机制

下图展示了"类加载器"的层次关系,这种关系称为类加载器的"双亲委派模型"。
在这里插入图片描述

  • “双亲委派模型"中,除了顶层的启动类加载器外,其余的类加载器都应当有自己的"父级类加载器”。
  • 这种关系不是通过"继承"实现的,通常是通过"组合"实现的。通过"组合"来表示父级类加载器。
  • "双亲委派模型"的工作过程:
    • 某个"类加载器"收到类加载的请求,它首先不会尝试自己去加载这个类,而是把求交给父级类加载器。
    • 因此,所有的类加载的请求最终都会传送到顶层的"启动类加载器"中。
    • 如果"父级类加载器"无法加载这个类,然后子级类加载器再去加载。

双亲委派机制的好处

双亲委派机制的一个显而易见的好处是:Java的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如:java.lang.Object。它存放在rt.jar中。无论哪一个类加载器要加载这个类,最终都是委派给处于顶端的"启动类加载器"进行加载,因此java.lang.Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有"双亲委派机制",如果用户自己编写了一个java.lang.Object,那么当我们编写其它类时,这种隐式的继承使用的将会是用户自己编写的java.lang.Object类,那将变得一片混乱。

做一个演示:
这里我编写了一个类,包名是java.lang,类名是String。
但是看到异常中的错误提示说的是找不到main方法,分析一下原因:
图中的String是我们自定义的类,加载时会从AppClassloader往上找,先会询问ExtClassLoader中有没有这个类,显然它没有String这个类,又会向上找父级加载器BootStrapClassLoader,显然这里面可以加载java.lang.String,但是它里面的这个String是官方提供的类,里面没有main方法,所以会报这个异常。
在这里插入图片描述
总结:每个自定义的类在加载时,都会一级一级向上询问父加载器中有没有这个类,父类加载器中有的话就用父类加载器中的类,如果实在找不到就用你自定义的这个类,这样就保证了Class只会加载一次,防止了我们的代码与源代码冲突,这就是双亲委派机制。

沙箱安全机制(了解)

参考(改变ing)的博客:
https://blog.csdn.net/qq_30336433/article/details/83268945

本地方法栈

用于运行带有native的方法。

带有native的方法只有声明,没有实现,它的实现是C语言编写的。

带有native的方法不是java官方编写的,是C语言编写的代码,我们可以简单的理解为,带有native的方法就是调用了第三方函数库或者叫做调用了C语言函数库来实现功能的。

PC寄存器(程序计数器)

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址),由执行引擎读取下一条命令,占内存空间非常小,可以忽略不计。
它是当前线程所执行的字节码的行号指示器,通过不断的改变这个计数器的值来选取下一条要执行的字节码指令。
如果执行的是native的方法,那么这个计数器是空的。

总结:类似于指针,意思就是当前方法执行完,下一个该执行那个方法,就需要用pc寄存器来做标记,实质上pc寄存器存储的就是下一个要运行的方法的地址。

方法区

供各个线程运行时的内存区域,它存储了每个类的结构信息,例如运行时期的常量池、方法数据、构造函数、普通方法等这些的字节码内容,方法区就是存储了类的结构信息,也可以称为模板信息。

注意:方法区只是一个规范,它在不同的虚拟机里面实现是不一样的,最经典的就是永久代(PermGen space)和元空间(Meta space)。
实例变量在堆内存中,与方法区无关。

总结:方法区存储了一个类的结构信息,也就是Class(大Class),就是类的模板信息。

堆和栈

先了解几个概念:
堆管存储,栈管运行。
程序 = 框架 + 业务逻辑。
队列:先进先出,后进后出。
栈:先进后出,后进先出。

栈内存

栈中存储着:8中基本数据类型 + 对象的引用变量(等号左边的对象引用) + 方法都是在栈内存中分配着。

方法在栈内存中叫栈帧
在java层面就是叫方法

StackOverflowError 栈内存溢出(错误)

堆内存

凡是new出来的都在堆内存。

OutOfMemoryError (Java heap space)堆内存溢出

堆的结构和GC以及GC算法请参考上篇文章:
https://blog.csdn.net/weixin_45216092/article/details/105196959

刚刚学习没多久,有不对的地方,还望各位指点!

发布了41 篇原创文章 · 获赞 48 · 访问量 4923

猜你喜欢

转载自blog.csdn.net/weixin_45216092/article/details/105226704