JVM之JVM的体系结构
一、jdk的组成
jdk:jdk是java开发工具包,是sun microsystems针对java开发员的产品。jdk中包含jre(在jdk的安装目录下有一个名为jre的目录,里面有两个文件夹bin和lib,在这里可以认为bin里的就是jvm,lib中则是jvm工作所需要的类库,而jvm和 lib和起来就称为jre)和一堆java工具(javac/java/jdb等)和java基础的类库(即java api 包括rt.jar)。
java runtime environment(jre):是运行基于java语言编写的程序所不可缺少的运行环境。也是通过它,java的开发者才得以将自己开发的程序发布到用户手中,让用户使用。jre中包含了java virtual machine(jvm),runtime class libraries和java application launcher,这些是运行java程序的必要组件。
jvm(java virtual machine):就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。
二、jvm的位置
jvm就是运行在操作系统之上的一个软件
三、jvm体系结构
jvm的组成:
- 类加载子系统 class loader
- 运行时数据区 jvm 内存模型
- 执行引擎
四、类加载子系统
======================类加载器=======================
类加载器(classloader):负责加载class文件(classs文件在文件开头有特定的文件标识),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构;classloader只负责加载class文件的加载,至于它是否可以运行,则由execution engine决定。
1、bootstraploader(引导类加载器):类加载器也是java类,他们也需要类加载器加载进入内存,显然必须要有第一个不是java类的类加载器,来完成这个工作,这个正是bootstrap。负责加载存放在d:\program files (x86)\java\jdk1.7.0_79\jre\lib下,或被-xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被bootstrap classloader加载);启动类加载器是无法被java程序直接引用的;rt.jar 里面的类的加载器都是bootstraploader。
2、extension classloader(扩展类加载器):该加载器由sun.misc.launcher$extclassloader实现,它负责加载d:\program files (x86)\java\jdk1.7.0_79\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。ext 目录下所有的类的加载器都是extension classloader
3、application classloader(应用程序类加载器):该类加载器由sun.misc.launcher$appclassloader来实现,它负责加载用户类路径(classpath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
====================jvm类加载机制==============
全盘负责:当前线程的类加载器负责加载某个class时,该class所依赖的和引用的其他class也将由该类加载器负责载入,除非显示使用classloader.loadclass()指定类加载器来载入
父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。所以我们在开发中尽量不要使用与jdk相同的类(例如自定义一个java.lang.system类),因为父类加载器中已经有一份java.lang.system类了,它会直接将该类给程序使用,而你自定义的类压根就不会被加载。
双亲委派模型:
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,
只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制:
- 1、当appclassloader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器extclassloader去完成。
- 2、当extclassloader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给bootstrap classloader去完成。
- 3、如果bootstrap classloader加载失败(例如在$java_home/jre/lib里未查找到该class),会使用extclassloader来尝试加载;
- 4、若extclassloader也加载失败,则会使用appclassloader来加载,如果appclassloader也加载失败,则会报出异常classnotfoundexception。
双亲委派模型意义:
- -系统类防止内存中出现多份同样的字节码
- -保证java程序安全稳定运行
==================类的加载过程======================
类的加载过程:jvm将javac编译好的class字节码文件加载到内存中,并对该数据进行验证、解析和初始化、形成jvm可以直接使用的java类,最终回收(卸载)的过程。
字节码(.class)文件来源:
- – 从本地系统中直接加载
- – 通过网络下载.class文件
- – 从zip,jar等归档文件中加载.class文件
- – 从专有数据库中提取.class文件
- – 将java源文件动态编译为.class文件
1、加载:加载阶段其实就是jvm通过一个类的全限定名来获取其定义的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构且在java堆中生成一个代表这个类的java.lang.class对象,作为对方法区中这些数据的访问入口。在该阶段我们开发人员可以干预,例如:我们可以指定类加载器来加载该字节数组或者自定义类加载器来加载。
2、链接:将java类的二进制代码合并到jvm的运行状态中的过程
- a、验证:验证是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- b、准备:该阶段是在方法区中为类变量(static变量)分配内存并设置类变量初始值。例如:public static int flag=1;该阶段初始化值为0。
- c、解析:虚拟机将常量池中的符号引用替换为直接引用的过程。(直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄)
3、初始化:初始化为类的静态变量赋予正确的初始值,jvm负责对类进行初始化,主要对类变量进行初始化。
- 初始化阶段就是执行类构造器<clinit>()的过程,类构造器<clinit>()是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先初始化其父类。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
- 当访问一个java 类的静态域时,只有正真申明这个域的类才会被初始化。
4、使用:程序使用jvm加载的类
5、卸载
- 执行了system.exit()方法
- jvm垃圾回收机制触发回收
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致java虚拟机进程终止
五、运行时数据区
1、方法区(method area):方法区是各个线程共享的内存区域;方法区用于存储已被虚拟机加载的类的模板信息、常量、静态变量等;虽然java虚拟机规范把方法区描述为堆的一部分,但是他还有个别名叫做non-heap(非堆),目的应该是与java堆区分开来;根据java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出outofmemoryerror 异常;相对而言,垃圾收集在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样永久存在了。这区域的内存回收目标重要是针对常量池的回收和类型的卸载。
方法区只是一个规范:
- 在hotspot虚拟机上开发、部署程序我们把方法区称为“永久代”(permanent generation);
- 他虚拟机(如 bea jrockit、ibm j9 等)来说是不存在永久代的概念的。
- hotspot虚拟机在jkd.8中已经没有方法区的概念了,他使用元空间代替该区域
2、pc寄存器(程序计数器):每个线程都有一个程序计数器,是线程私有的;就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,既将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记;它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果执行的是一个native方法,那这个计数器是空的;用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出oom错误
本地方法栈(native stack):与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为native方法服务。(栈的空间大小远远小于堆)
3、虚拟机栈(vm stack)
栈也叫栈内存,主管 java 程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就仔放,对于栈来说不存在垃圾回收问题,只要线程结束该栈就释放,生命周期和线程一致,是线程私有的。8种基木类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈的运行原理:栈中的数据都是以栈帧(stack frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法( method )和运行期数据的数据集,当一个方法a被调用时就产生了一个栈帧 fl ,并被压入到栈中, a方法又调用了b方法,于是产生栈帧 f2 也被压入栈,b方法又调用了c方法,于是产生栈帧 f3 也被压入栈,执行完毕后,先弹出 f3 栈帧,再弹出 f2 栈帧,再弹出 fl 栈帧 以此类推, 遵循“先进后出” / “后进先出”原则。每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体jvm的实现有关,通常在 256k~1024k 之间, 1m 左右。
jvm栈的特点:
- 局部变量表所需的内存空间在编译期间完成内存分配。当进入一个方法时,这个方法需要在帧中分配多大的内存空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
- 在java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将抛出*error异常(栈溢出);如果虚拟机栈可以动态扩展(现在大部分java虚拟机都可以动态扩展,只不过java虚拟机规范中也允许固定长度的java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出outofmemoryerror异常(没有足够的内存)。
4、本地方法栈(native method stacks):与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地native方法服务‘;在虚拟机规范中对本地方法栈中的使用方法、语言、数据结构并没有强制规定,因此具体的虚拟机可以*实现它。甚至有的虚拟机(例如sun hotspot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。本地方法栈也会抛出*error和outofmmemoryerror异常。
5、java堆(java heap):是java虚拟机管理内存中的最大一块;java堆是所有线程共享的一块内存管理区域。此内存区域唯一目的就是存放对象的实例,几乎所有对象实例都在堆中分配内存。这一点在java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配,但是随着jit编译器的发展与逃逸技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也不是变的那么“绝对”了。详解请学习我的:jvm之堆的体系结构
上一篇: 5 分钟全面掌握 Python 装饰器
下一篇: Jupyter book简单配置