JVM 内存结构
先来一张内存结构图
那么什么叫做Java虚拟机呢?
我们学习的时候,应该都知道Java语言能够一次编译,到处运行的特点,其中这种平台无关性关键就在于Java虚拟机,他是一个可执行Java字节码的虚拟机进程。
组成部分
ClassLoader:(类加载器)类装载子系统,JVM启动的时候,将字节码加载到JVM中。
RunTime Data Area:(运行时数据区)如上图,将内存分为不同的区域,分别实现不同的功能。
Excution Engine:(执行引擎)不仅负责执行class文件中的字节码指令,还会调用GC操作。
Native Interface:(本地接口)融合不同开发语言的原生库(主要为C和C++)为Java所用。
接下来我们分区域进行分析,首先是ClassLoader -----> 类加载器
类加载器的加载过程 (主要的就为5个部分,原为7个部分,不过最后两个部分我觉得并没有什么,比较好理解)
- 加载:在硬盘上查找并通过IO读入字节码文件至JVM虚拟机和方法区中,同时在堆中创建Class对象
- 验证:校验字节码文件的正确性 ( 例如 class 文件的标识 cafe)
- 准备:为类的静态变量分配内存,并将其初始化为默认值,此阶段仅仅只为静态类变量,(static修饰的变量)分配内存空间,并且设置该变量的初始值,(比如static num = 5 ,在这里只会将其初始化为0,5的值将会在显示初始化的时候复制)对于final static 修饰的变量,编译的时候就会分配,不会分配实例变量的内存空间。
- 解析:把类中的符号引用转化为直接引用
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
符号引用和直接引用
类加载过程中,在解析阶段,JVM会将二进制常量池内的符号引用替换为直接引用。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以试任何形式的字面量,只要使用时能无歧义的定位到目标位置就行。(存放在方法区中的运行时常量池中)
**直接引用:**直接引用就是可以直接指向目标的指针、相对偏移量或者时是一个能间接定位到目标的句柄。
我们都知道,在程序运行过程中,程序会将java 文件转化为.class文件,将.class文件加载到内存中,将其放在运行时数据区的方法区内。这个过程就是类加载过程。那么是怎么被加载的呢? 这里就运用了双亲委派模型
首先一共由三大类加载器
-
**启动类加载器(BootStrapClassLoader):**是最顶层的类加载器,主要加载核心类库
-
**扩展类加载器(ExtClassLoader):**加载<java_HOME>\lib\ext目录下的jar文件
-
**应用程序类加载器(AppClassLoader):**负责加载用户路径下的类加载器
Launcher是classloader的入口,扩展类加载器和应用程序类加载器是一个静态内部类(并不是我们想象中认为的父类加载器(继承下来的),源码中只是存在一个指针有存放上一级的关系而已,有兴趣搜一下Launcher类类去看一下,更好的理解哦),继承于URLclassLoader,间接继承classLoader。
什么是双亲委派模型?
简单的说,就是当一个类需要被加载的时候,会先去查看是否已经被加载过了,如果没有被加载,则会请求委派给父类,第一层父类就是应用程序类加载器AppClassLoader,检查这个类是否已经加载,没有加载,则继续请求委派给父类,接下来就是扩展类加载器,同样,继续直到最顶配的启动类加载器,当所有的父类都无法完成这个加载请求的时候,才会交给子加载器尝试加载
有什么优先:沙箱安全机制 往上委派的理由
总所周知,java.lang.Object类是所有类的父类,程序在运行期间会把java.lang.Object类加载到内存当中,假如java.lang.Object能够被我们自定义类加载器去加载的话,那么JVM中就会存在多份Object的Class对象,而且这些对象是不兼容的。
所以双亲委派模型可以保证java核心类库的类型安全
借助双亲委派模型,我们java核心类库的类必须是由我们的启动类加载器加载的,这样可以确保我们核心类库只会在jvm中存在一份,这样就不会给自定义类加载器去加载我们核心类库的类。
一个小小的问题 ------> JVM是如何判定两个类是相同的?
主要还是判断是否是同一个类加载器加载出来的,即便是相同全限定类名的类,通过不同的类加载器加载出来的都是不一样的class对象
RunTime Data Area ------> JVM的重点就在于运行时数据区,也就是内存区
内存区又可分为线程共享区和私有区 栈管运行,堆管存储
线程共享区:
- 方法区:存放类加载后产生的字节码信息,常量,静态数据。栈都有一个运行时常量池的引用
- 堆:通俗的说就是存放对象的信息,是GC的主要区域,在物理逻辑上是内存不连续的。
线程私有区:
- 虚拟机栈:每一个线程都会由单独的虚拟机栈,虚拟机栈用来存放局部变量的信息,每一个虚拟机栈都会同步创建一个栈帧,用于存储 局部变量表、操作数栈、动态链接、方法出口。
- 本地方法栈:与虚拟机栈的功能相同,主要提供给本地方法
- 程序计数器:每一个线程独有的,如果执行的是java代码,通俗点理解就是记录当前线程执行到哪一行,记录下来。 如果执行的是Native方法,计数器的值为空;该内存区域是唯一一个不会出现OOM情况的区域。
栈帧的内存结构
局部变量表:存放一组变量值的存储空间,用于存放方法参数和局部变量的信息
操作数栈:用于计算的临时数据区
动态链接:符号引用在运行过程中转化为直接引用,类加载过程中的解析过程。 符号引用存在于方法区中的运行时常量池 ,每一个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,目的就是为了支持方法在调用过程中的动态链接。
方法出口: 在方法完成之后,都需要返回最初方法被调用时的位置,程序才能正常秩序,所以会在栈帧中存储方法返回的地址,用来帮助程序返回。