JVM的类加载过程以及双亲委派模型详解
jvm 的主要组成部分
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
jvm 运行时数据区的组成
方法区:
①方法区主要用来存储已被虚拟机加载的类信息(构造器,接口定义)、常量、静态变量和运行时常量池等数据。
②该区域是被线程共享的。
③方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
虚拟机栈:
虚拟机栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。
8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
①每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息。
②虚拟机栈是线程私有的,它的生命周期与线程相同。
③局部变量表里存储的是基本数据类型、returnAddress类型(指向一条字节码指令的地址)和对象引用,这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置。4.局部变量所需的内存空间在编译器间确定。
④操作数栈的作用主要用来存储运算结果以及运算的操作数,它不同于局部变量表通过索引来访问,而是压栈和出栈的方式
⑤每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接.动态链接就是将常量池中的符号引用在运行期转化为直接引用。
本地方法栈
本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。
堆
java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
程序计数器
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM(程序申请内存过大,虚拟机无法满足我们,然后自杀了)情况的区域。
程序在虚拟机中的执行过程
首先是类加载器来加载class文件得到Class模板,放到方法区中(类的信息(构造器,接口定义)、常量、静态变量和运行时常量池等数据),根据class模板来实例化对象的时候,会把对象放在堆中(可以提一下堆分代,垃圾回收策略,垃圾回收算法,内存泄漏原因),根据对象调用方法时,会将方法压到栈中(8种基本类型的变量+对象的引用变量+实例方法),native方法会被压入到本地方法栈中,由jvm向操作系统发送指令,由执行引擎解释命令发送给操作系统,操作系统会调用本地方法接口,用本地方法库,执行本地方法。栈中的方法按照后进先出的顺序出栈,由程序计数器来指向下一个出栈的方法,栈中没有垃圾回收,他们随着线程的执行完毕被释放。
类加载的双亲委派模型
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
类加载器分类:
启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
扩展类加载器(Extension ClassLoader):负责加载<java_home style="box-sizing: border-box; -webkit-tap-highlight-color: transparent; text-size-adjust: none; -webkit-font-smoothing: antialiased; outline: 0px !important;">\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;</java_home>
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
自定义类加载器
他们之间如图所示是自上向下的关系。
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,再从上向下让子加载器尝试去加载类。
那么我们如何去验证这一说法呢?
我们写一个简单地小程序:
然后编译这个java文件,生成class文件。
我们把这个文件放在启动类加载器可以加载到的地方新建目录classes:D:\Program Files\Java\jdk1.8.0_161\jre\classes
然后将程序的修改:
再次编译,并将生成的classes文件放在扩展类启动器可以加载到的地方新建文件夹classes:D:\Program Files\Java\jdk1.8.0_161\jre\lib\ext\classes
最后,我们再次修改
生成的class文件就放在当前目录下。
那么当我们执行这个class文件的时候出现的结果是什么呢?
这说明,并没有加载我们当前目录下的class文件,而是用了启动类加载器扫描范围内的那个文件。
进一步验证,我们删掉D:\Program Files\Java\jdk1.8.0_161\jre\classes下的文件
再次运行结果:
结果变为bbb,说明当启动类加载器没找到class文件,由扩展类加载器加载了。
扩展类加载器范围内的文件也删掉呢?
终于加载到了当前文件夹下的class文件
面试题:
在自己的代码中,可以创建一个java.lang.String对象吗?如果可以,这个对象是否可以被类加载器加载?
可以创建,但是不能被加载到。
因为,双亲委派模式会保证父类加载器先加载类,就是BootStrap(启动类)加载器加载jdk里面的java.lang.String类,而自定义的java.lang.String类永远不会被加载到
以上是 JVM的类加载过程以及双亲委派模型详解 的全部内容, 来源链接: utcz.com/z/344342.html