JVM底层原理介绍

JVM结构图

JVM底层原理介绍
黄色的是所有线程共享数据,存在垃圾回收。
灰色的是线程之间数据私有,不存在垃圾回收。

类的加载时机

  1. 创建类的实例。
  2. 类的静态变量,或者为静态变量赋值。
  3. 类的静态方法。
  4. 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
  5. 初始化某个类的子类。
  6. 直接使用java.exe命令来运行某个主类。
    以上六种情况的任何一种,都可以导致JVM将一个类加载到方法区。

类装载器

虚拟机自带的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):用于加载系统类库<JAVA_HOME>\bin目录下的class,例如:rt.jar。 (C++编写的)
  • 扩展类加载器(Extension ClassLoader):用于加载扩展类<JAVA_HOME>\lib\ext目录下的class。 (Java编写的)
  • 应用程序类加载器(Application ClassLoader):也叫系统类加载器,用于加载当前应用的classpath下的所有类,包括我们自定义类的加载器。

自定义的类加载器:

  • Java.long.ClassLoader的子类用户可自定义加载方式。

双亲委派机制

下图展示了"类加载器"的层次关系,这种关系称为类加载器的"双亲委派模型"。
JVM底层原理介绍

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

双亲委派机制的好处

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

做一个演示:
这里我编写了一个类,包名是java.lang,类名是String。
但是看到异常中的错误提示说的是找不到main方法,由此就可以想到,类加载器是从上往下的,先到最顶层的Bootstrap启动类加载器去找这个对应的类,找到了就用,找不到就去下一个类加载器找,从上往下依次查找,直到找到为止。我们都知道虚拟机中自带的String类是在java.lang包下,刚好跟该例子的包名类名完全符合,当从上往下进行查找时,在启动类加载器的时候就已经找到了String类,而不是我们自定义的这个String类,所以它会去执行虚拟机里自带的这个String类,但是里面没有main方法,所以就会报这个异常。
JVM底层原理介绍
总结:每个类在加载时都会到最顶端的启动类加载器中先查找,找到就用,找不到去下一个加载器中找,如果实在找不到就用你自定义的这个类,这样就保证了我们自己写的代码不会去污染java的源代码,这就是双亲委派机制。
每一个类的加载都要从上往下找,从最上面的启动类加载器开始,这就是jvm的沙箱安全机制

本地方法栈

用于运行带有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算法!

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