文章目录

  • JVM结构图
  • 类的加载
  • 类的加载时机
  • 类加载器
  • 双亲委派机制
  • 双亲委派机制的好处
  • 沙箱安全机制(了解)
  • 本地方法栈
  • PC寄存器(程序计数器)
  • 方法区
  • 堆和栈
  • 栈内存
  • 堆内存
  • **堆的结构和GC以及GC算法请参考上篇文章:** []()


JVM结构图

JAVA底层实现机制是什么意思 jvm底层机制_类加载器


黄色的是所有线程共享数据,存在垃圾回收。
灰色的是线程之间数据私有,不存在垃圾回收。

通过类装载子系统把class装载到运行时数据区。
类装载方式有两种 :

  • 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm 中。
  • 显式装载, 通过class.forname()等方法,显式加载需要的类。

类的加载

1.装载:查找和导入class文件;

2.连接:(细分为:检查,准备,解析)

  • 检查:检查载入的class文件数据的正确性;
  • 准备:为类的静态变量分配存储空间;
  • 解析:将符号引用转换成直接引用(可选的)

3.初始化:初始化静态变量,静态代码块。

注意: 在Java中类的加载是动态的,它不会一次性记载所有类然后运行,而是先把保证程序能运行的基类先加载到JVM中,其他类则是在需要时再加载,这样就加快了加载速度,而且节约了程序运行过程中内存的开销。

类的加载时机

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

类加载器

作用:将class文件加载进内存。

虚拟机自带的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):用于加载系统类库<JAVA_HOME>/jre/bin/rt.jar目录下的class。 (C++编写的)
  • 扩展类加载器(Extension ClassLoader):用于加载扩展类<JAVA_HOME>jre/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底层实现机制是什么意思 jvm底层机制_加载_02

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

双亲委派机制的好处

双亲委派机制的一个显而易见的好处是: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方法,所以会报这个异常。

JAVA底层实现机制是什么意思 jvm底层机制_java_03


总结:每个自定义的类在加载时,都会一级一级向上询问父加载器中有没有这个类,父类加载器中有的话就用父类加载器中的类,如果实在找不到就用你自定义的这个类,这样就保证了Class只会加载一次,防止了我们的代码与源代码冲突,这就是双亲委派机制。

沙箱安全机制(了解)

沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。

本地方法栈

用于运行带有native的方法。

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

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

PC寄存器(程序计数器)

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

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

方法区

1.7之叫做永久代,1.7以叫做元空间,方法区逻辑上是堆的一部分,但是实际堆中只有年轻代和老年代。

永久代:存储一个类的结构信息,运行时期的常量池,方法数据,构造,成员变量等。
元空间:存储一个类的结构信息,运行时期的常量池、方法数据、方法代码、符号引用等。

二者都是JVM规范中方法区的实现,不过最大的区别是元空间并不在虚拟机中,而是使用本地内存。

常量池:类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式的。

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

实例变量在堆内存中,与方法区无关。

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

堆和栈

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

栈内存

运行普通方法的地方。
单线程的情况下,第一个方法会压栈执行,如果在该方法内又调用了另外一个方法,第二个方法会进入栈在第一个方法的上面,直到第二个方法执行完毕弹栈后,第一个方法才会得到执行,当所有方法弹栈之后,该程序就执行完毕了。

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

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

StackOverflowError 栈内存溢出(错误)

堆内存

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

OutOfMemoryError (Java heap space)堆内存溢出


拓展:内存泄漏问题
当长生命周期的对象持有短生命周期的对象的引用,就很可能发生内存泄漏。尽管短生命周期的对象已经不再需要,但是长生命周期的对象一直持有它的引用导致其无法被回收。

例如:1. 一个map中我们存储了多个对象,该map一直被其他对象所引用,但是map中的个别对象很长时间未被引用,由于该对象在map中,这个对象一直被map引用,不会被回收。

例如:2. 如果一个外部类的实例对象的方法返回了一个内部类的实例对象, 这个内部类对象被长期引用了,即使那个外部类实例对象不再被使 用,但由于内部类持久外部类的实例对象,这个外部类对象将不会 被垃圾回收,这也会造成内存泄露。