JVM结构图
黄色的是所有线程共享数据,存在垃圾回收。
灰色的是线程之间数据私有,不存在垃圾回收。
类的加载时机
- 创建类的实例。例如:new Person()
- 使用到类的静态成员时。
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
- 初始化某个类的子类时,new子类先加载父类。
- 直接使用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
刚刚学习没多久,有不对的地方,还望各位指点!