文章目录
什么是JVM
JVM本质上是一个运行在计算机上的程序,他的职责是运行java字节码文件。
JVM的功能
解释和运行
对字节码文件中的指令实时的解释成机器码,让计算机执行。
内存管理
自动为对象,方法等分配内存。
自动的垃圾回收机制,回收不再使用的对象。
即时编译
对热点代码进行优化,提升执行效率。
常见的JVM
字节码文件的组成
基本信息`
- 魔数:java字节码文件中,将文件头称为magic魔数 java class文件固定格式为0xCAFEBABE。
- 主副版本号:主副版本号是指编译字节码文件的JDK版本号
作用:版本号的作用主要是判断当前字节码文件的版本和运行时的JDK是否兼容。 - 访问标示:public final等等。
- 父类和接口。
常量池
内容:保存了字符串常量,类或接口名,字段名,主要在字节码指令中使用。
作用:避免相同的内容重复定义,节省空间。
字段
当前类或接口声明的字段信息。
方法
当前类或接口声明的方法信息,字节码指令。
操作数栈:
是临时存放数据的地方。
局部变量表:
是存放方法中局部变量的位置。
属性
类的内部属性,比如源码的文件名,内部类的列表等。
JVM的组成
类加载器
定义
是java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
分类
一类是java代码实现的,一类是java虚拟机底层源码实现的。
dk8以及之前的版本中默认的类加载器有如下几种:
虚拟机底层实现(c++):
启动类加载器 Bootstrap 加载java中最核心的类。
启动类加载器是由Hotspot虚拟机提供的,使用C++语言编写的类加载器。
默认加载java安装目录/jre/lib下的类文件,比如rt.jar,tools.jar,resources.jar
通过启动类加载器去加载用户jar包:
放入jre/lib下进行扩展(不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即使放进去由于文件名不匹配的问题也不会正常的被加载)。
使用参数进行扩展(推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展)。
java:
扩展类加载器Extension 允许扩展java中比较通用的类。
默认加载java安装目录中/jre/lib/ext下的类文件。
通过扩展类加载器去加载用户jar包:
放入jre/lib/ext下进行扩展(不推荐,尽可能不要去更改JDK安装目录中的内容) 。
使用参数进行扩展(推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加原始目录)。
应用程序类加载器Application:加载应用使用的类,即加载自己写的类文件和导入第三方jar包中的类。
jdk8之后的类加载器:
JDK9引入了module的概念,类加载器在设计上产生了很多变化。
BootClassLoader仍然存在,使用java编写,java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
扩展类加载器被替换成了平台类加载器(Platform Class Loader),平台类加载器的存在更多是为了与老版本的设计方案进行兼容,自身没有特殊的逻辑。
类加载器的双亲委派机制
双亲委派机制的核心是解决一个类到底由谁加载的问题。
作用:
保证类加载的安全性。
避免类被重复加载。
作用机理
打破双亲委派机制的方法
1.自定义类加载器:
自定义类加载器并且重写loadClass方法,并且将双亲委派机制的代码去除。
实现自定义类加载器,它的父类为AppClassLoader加载器。
2.线程上下文类加载器:
比如JDBC和JNDI等。
3.Osgi框架的类加载器
OSGI模块化框架,它存在同级之间的类加载器的委托加载,OSGI还使用类加载器实现了热部署的功能。
类的生命周期
1.加载:
第一步:类加载器根据类的全限定名通过不同的渠道以二进制的方式获取字节码信息。
第二步:类加载器加载完类之后,java虚拟机会将字节码中的信息保存的到方法区中(InstanceKlass)(c++语言写的)。
第三步:java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.class对象(jdk8之后静态字段放在堆区)。
2.连接(验证->准备->解析):
验证:验证内容是否满足《java虚拟机规范》
主要有四部分:
文件格式验证,比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前java虚拟机的要求。
元信息验证,比如类必须要有父类(super不能为空)。
验证程序指令的语义,比如方法内指令执行中跳转到不正确的位置。
符号引用验证,例如是否访问了其他类中private的方法等。
准备:给静态变量赋初值,不加final在初始化时赋值,加final在准备时赋值。
解析:将常量池中的符号引用替换成指向内存的直接引用。
3.初始化:
初始化阶段会执行静态代码块中的代码,并为静态变量赋值。
初始化阶段会执行字节码文件中clinit部分的字节码指令。
clinit在特定的情况下不会出现:
无静态代码块且无静态变量赋值语句。
有静态变量的声明,但是没有赋值语句。
静态变量使用final关键字,这类变量会在准备阶段进行初始化。
数组中创建不会导致数组中元素的类进行初始化。
final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化。
初始化触发:
直接访问父类的静态变量,不会触发子类的初始化方法。
子类的初始化clinit调用之前,会先执行父类的clinit的初始化方法。
4.使用
5.卸载
运行时数据区域(JVM管理的内存)
作用:负责管理JVM使用到的内存,比如创建对象和销毁对象。
线程不共享(位于线程内部):
程序计数器:也叫PC寄存器,每个线程会通过程序计数器记录当前要执行字节码指令的地址。
作用:1.控制程序指令的执行。
2.在多线程执行情况下,java虚拟机需要通过程序计数器CPU切换前解释执行到那一句指令并继续解释运行。
java虚拟机栈:采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存。
栈帧的组成:
局部变量表:是存放方法中局部变量的位置,是一个数组,数组中的每一个位置称为槽(slot),long和double占两个槽,其他类型占一个槽。
保存的内容有:实例方法的this对象,方法的参数,方法体中声明的局部变量。
操作数栈:是栈帧中虚拟机在执行指令时用来存放临时数据的一块区域。
帧数据:
动态链接:保存了编号到运行时常量池的内存地址的映射关系。
方法出口:是指方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址,所以在当前栈帧中,需要存储此方法出口的地址。
异常表的引用:异常表存放了代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到字节码的指令位置。
本地方法栈(会产生内存溢出):
存储的是native本地方法的栈帧,在Hotspot虚拟机中,java虚拟机栈和本地方法栈实现上使用了同一个栈空间。
线程共享(位于Jvm内存):
方法区:
存放基础信息的位置,线程共享,主要包含三部分内容:
1.类的元信息:
保存了所有类的基本信息。
2.运行时常量池
保存了字节码文件中的常量池内容。
3.字符串常量池
保存了字符串常量。
堆:一般java程序中堆内存是空间最大的一块内存区域,创建出来的对象都放在堆上。
堆中需要关注的三个值: (建议将total和max设置的值相同)
1.used:当前已经使用的堆空间。
2.total:java虚拟机已经分配的可yong堆内存。
3.max:虚拟机可以分配的最大堆内存。
堆 - 设置大小(虚拟机参数设置见下方):
要修改堆的大小,可以使用虚拟机参数 -Xmx(max最大值)和-Xms(初始的total)
语法:-Xmx值 -Xms值
单位:字节(默认),必须是1024的倍数,k,m,g
限制:Xmx必须大于2MB,Xms必须大于1MB。
虚拟机参数的设置:
其他的参数依次类推。
执行引擎(主要介绍垃圾回收器)
作用:将字节码文件中的指令解释成机械码,同时使用即时编译器优化性能。
包含即时编译器,解释器,垃圾回收器等。
垃圾回收器
分类
自动垃圾回收(java):
自动根据对象是否使用由虚拟机来回收对象:
优点:降低程序员实现难度,降低对象回收bug的可能性。
缺点:程序员无法控制内存回收的及时性。
手动垃圾回收(c/c++):
由程序员编程实现对象的删除
优点:回收及时性高,由程序员把控回收的时机。
缺点:编写不当出现悬空指针,重复释放,内存泄漏等问题。
方法区和堆的回收
方法区的回收:
判断一个类可以被回收,需要同时满足下面三个条件
1.此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
2.加载该类的类加载器已经被回收。
3.此类对应的java.lang.Class对象没有在任何地方被引用。
手动触发方法区回收:
如果需要手动触发垃圾回收,可以调用System.gc()方法。
堆回收:
判断堆上的对象是否可以回收:
java对象是否可以回收,是根据对象是否被引用来决定的,如果被引用了,说明对象还在被使用,不能被回收。
判断方法:
常见的有两种方法:
引用计数法:
引用计数法会为每一个对象维护一个引用计数器,当对象被引用时+1,取消引用时-1。
可达性分析法:
可达性分析将对象分为两类,垃圾回收的根对象(GC root)(一般不能回收)和普通对象,对象和对象之间存在引用关系
哪些对象称为GC Root对象:
线程Thread对象。
系统类加载器加载的java.lang.Class对象,一般静态对象会被这类对象关联到。
监视器对象,用来保存和同步锁synchronized关键字持有的对象。
本地方法调用时使用的全局对象。
垃圾回收算法
- 核心思想:
1.找到内存中存活的对象。
2.释放不再存活对象的内存,使得程序能够再次利用这部分空间。- 算法分类:
标记-清除算法:
分为两个阶段:
1.标记阶段,将所有存活的对象进行标记。java中使用可达性算法,从GC Root开始通过引用链遍历出所有存活对象。
2.清除阶段,从内存中删除没有被标记也就是非存活对象。
- 算法分类:
优势:实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
缺点:1.碎片化问题。
2.分配速度慢。
复制算法:
核心思想为:
1.准备两块空间From空间和To空间,每次在对象分配阶段,只能使用其中一块空间。
2.GC阶段开始,将GC Root搬运至To空间。
3.将GC Root关联的对象搬入To空间。
4.清理From空间,并把名称互换。
优势:吞吐量高,不会发送碎片化。
缺点:内存使用率低。
标记-整理算法:
也叫标记压缩算法,是对标记清理算法中容易产生内存碎片问题的一种解决方案。
有两个阶段:
1.标记阶段,将所有存活的对象进行标记。java中使用可达性算法,从GC Root开始通过引用链遍历出所有存活对象段。
2.整理阶段,将存活的对象移动到堆的一端,清理掉非存活对象的内存空间。
优势:内存使用率高,不会发生碎片化。
缺点:整理阶段的效率不高。
分代GC:
分代垃圾回收将内存区域划分为):
年轻代(新生代)(Young区):
存放存活时间比较短的对象,包含伊甸园区(Eden),幸存区(S0,S1)。
老年代(Old区):存放存活时间比较长的对象。
回收的方法:
Minor GC(Young GC):
Full GC:
当年轻代和老年代内存都满了,对整个堆进行回收。
垃圾回收器
垃圾回收器的组合关系:
1.1.Serial(年轻代):
它是一个单线程串行回收年轻代的垃圾回收器(复制算法)。
1.2.SerialOld(老年代):
它是Serial垃圾回收器的老年代版本(标记整理算法)。
适用于CPU比较匮乏的情况。
2.1.ParNew(年轻代):
本质上是对Serial在多CPU下的优化,使用多线程进行垃圾回收(复制算法)。
2.2.CMS(老年代):
CMS关注的是系统的暂停时间,允许用户线程和垃圾回收器在某些步骤中同时执行,减少了用户线程等待的时间(标记清理算法)(浮动垃圾)。
3.1Parallel Scavenge(年轻代):
是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点(复制算法)。
3.2Parallel Old(老年代):
是为Parallel Scavenge收集器设计的老年代版本,利用多线程并发收集(标记整理算法)。
G1垃圾回收器:
JDK9之后默认的垃圾回收器是G1垃圾回收器,将Parallel Scavenge和CMS两种垃圾回收器的优点融合。
垃圾回收器的选择:
JDK8及之前:
ParNew + CMS(关注暂停时间),Parallel Scavenge + Parallel Old(关注吞吐量),G1(JDK8之前不建议,较大堆并且关注暂停时间)。
JDK9之后:
G1(默认)。
本地接口
作用:调用本地已经编译的方法,比如虚拟机中提供的c/c++方法。