Java虚拟机/JVM
2021-04-12 17:44:02 0 举报
AI智能生成
JVM相关内容
作者其他创作
大纲/内容
编译
从.java文件变成.class文件的过程就是前端编译,由JVM完成,JVM是解释和编译执行同在的系统,
且内置了两个编译期,存在不同的优化和编译速度的差别
且内置了两个编译期,存在不同的优化和编译速度的差别
Java 语言的编译分为两种前端编译和后端编译
前端编译
前端编译就是指通过.java文件编译生成.class文件的过程,主要的就是javac
后端编译
后端编译是借助JIT将.class文件转化为机器码的过程
class文件结构
Java 做为解释型语言,解释器解释的对象就是 Class文件结构,也就是字节码文件,文件是按照8位字节为基础单位的二进制流,并且按照规定的格式严格排列
目前仅直到文件以COFFOEBABY开头,主次版本然后就是常量池
其他暂时不做深入了解,等到看CGLIB的时候可以顺带研究一下字节码的格式
GC/垃圾收集
垃圾收集器
G1
ZGC
CMS
第一款实际意义上的并发收集器(和用户线程同时进行),并且采用的是标记-清除算法
CMS的执行流程
初始标记
枚举根节点,此阶段需要STW
并发标记
重新标记
处理在并发标记其间改变的引用,也需要STW
并发清除
CMS的缺点
无法清理浮动垃圾
因为和用户线程并行,所以难免的无法处理浮动垃圾的问题
CPU敏感
和用户线程并发,是多线程作业的,总会占用一些CPU的执行时间,增加线程上下文切换的耗时
内存敏感
和用户线程并行的,如果在GC时用户线程分配内存失败,则会直接进入 Serial Old 的处理,这是非常严重的问题
CMS提供另外的参数配置在老年代占比达到n时就开启GC
内存碎片化
因为采用的标记清除所有碎片化无法避免,CMS提供了另外的参数配置n次Major GC后进行一次整理,默认是每次都会整理
Serial
新生代单线程垃圾收集器,效率很高且不会有浮动垃圾,但是需要STW
ParNew
Serial 的多线程版本,一样需要STW,但是是多线程同时进行,速度会快一点
Serial Old
老年代单线程垃圾收集器,就是Serial的老年代版本
Parallel Old
ParNew的老年代版本
Parallel Scavenge
吞吐量优先收集器
相关算法
垃圾收集算法
标记清除(Mark-Sweep)
标记所有存活(或者不存活)的对象,清除掉无用对象
主要问题就是严重的内存碎片
标记整理(Mark-Compact)
在标记清除的基础上会再整合剩余的对象
速度会比标记清理慢一大截,但是没有内存碎片的问题
复制(Copying)
将堆空间分为两块,平时使用其中一块,GC时将存活对象统一移动到另外一块
内存空间的利用率较低,并且复制的速率不高
分代垃圾收集算法
根据对象的生存周期的不同,将内存划分为多个区域,例如新生代/老年代/永久代,分别采用不同的垃圾收集算法
卡表(Card Table)
Minor GC的频率远大于Major GC,为了避免新生代的GC频繁扫描老年代(也就是全堆扫描),所以出现了卡表
卡表就是一个BitMap,每位表示特定大小的老年代内存空间是否有指向新生代的引用,以此来节省扫描时间
垃圾判定算法
引用计数法
保存对某个对象的引用数,为0表示对象已被废弃,可以回收,但存在循环引用的问题
Redis中采用了该类型算法,
可达性分析算法
以制定的GC ROOT 作为起点,逐层遍历直到标记所有可以到达的对象,不可达对象即为垃圾
GC ROOT
JVM栈和本地方法栈中变量
常量,静态变量
三色标记法
将所有的对象划分为白,黑,灰三色,分别表示未扫描的潜在垃圾,已扫描的活跃对象,未扫描的活跃对象
算法初始将GC Root的对象标记为灰色,对灰色进行扫描,将引用的对象标记为灰色,扫描结束后灰色节点变为黑色,继续对新增的灰色节点扫描,直到堆中没有灰色对象,清理白色对象
纯粹的三色标记法在和用户线程并发时会存在漏标或者错标的问题,需要结合内存屏障保证标记的正确性
引用
引用定义
对象的数据数值表示的另外一块区域的内存起始地址
引用类型
强引用(Strong Reference)
以 = new Object 创建的就是强引用
JVM的GC机制保证强引用对象不会被JVM的内存回收
软引用(Soft Reference)
比强引用弱,保存有用但非必须的对象
JVM的GC 会在内存溢出/内存不足前将其回收
弱引用(Weak Reference)
同样是描述非必须对象,比软引用更弱
GC时不论空间是否足够都会回收该对象,可以参考 ThreadLocal 的实现
虚引用(Phantom Reference)
最弱的引用,对GC不产生任何实质性的影响
定义虚引用的唯一作用就是在对象回收时通过finalize或者ReferenceQueue获得通知
执行子系统
执行子系统就是执行Class文件代码的子系统,Hotspot的实现中包含了解释执行和编译执行两种方式
解释执行
解释执行就是由解释器执行的过程
编译执行
编译过程是根据即时编译器产生的本地代码执行的过程
Java 虚拟机栈
Java虚拟机栈是实现方法调用和方法执行的关键,栈以FILO的形式操作,方法调用的时候入栈,调用结束之后弹出
局部变量表
该区域保存方法内的局部变量,包含方法参数
如果是非静态方法,该区域首个为this指针,指向当前的实例对象
!这里也能看出来 this 关键字的原理,是在方法调用的时候在局部变量表中填充的一个变量
局部变量表以 Solt 为单位分配内存,double 和 long 可能需要两个 Solt
另外这里也是 GC ROOT 的目标之一,在战帧 - 局部变量表的对象肯定都是当前存活的对象
方法返回地址
就是调用方法的地方,退出方法的方式有异常和正常返回两种,都需要返回到方法调用时的执行点继续执行
操作数栈
Java的解释执行引擎的主要工作区,以LIFO的形式操作
例如在执行加法运算的时候会先将两个操作数入栈,然后调用加法的字节码指令出栈并相加,入栈结果,或继续赋值给变量
动态链接
Java 支持动态语音特性,可能只有在运行期才能知道方法调用的真实目标方法是,所以此时只能保存符号引用
方法调用
Class 文件中所有的方法名都会保存在Class 文件常量池,一些可以在编译期就确定的方法会在转为Class文件时就转化为直接引用,这个就是静态解析,而动态解析的则是在编译期无法确定的方法
Java的方法调用支持重写和重载
重写
将父类的方法重新编写
重载
重名的方法只有参数列表不一样
返回值不一样无法区分两个方法
重写的实现
重写方法的调用实现基于动态单分派,属于动态解析的一种,对应的字节码指令为 invokevirtual
动态分派
方法只有在执行期间才能确定调用目标,实际中会在方法调用时候获取方法栈帧的第一个参数(this)的实际类型,而不同的调用可能会有不同的实际类型,所以调用的方法也会不同,如果当前方法没有则会往父类对象继续调用
虚方法表
JVM 会在方法执行的时候确定实际的方法地址,如果每次在动态分派的时候都需要上搜索类的元数据就很麻烦,所以建立了虚方法表
每个类在初始化的时候就创建了虚方法表,该表包括了所有该类的方法,包括从父类继承的
单分派
根据调用者这单个宗量来决定调用的方法
重载的实现
重载方法基于静态多分派,属于静态解析的一种
静态分派
JVM的静态分派实现是根据方法参数列表来的,并且是根据参数的静态类型而非实际类型
多分派
根据方法的参数列表进行匹配,多个方法参数就表示多个宗量,所以是多分派
内存管理
内存管理的概念
JVM 是运行在真实的操作系统上的,启动时会直接获取部分实际内存,在Java中创建对象就需要通过 JVM 去分配真实的内存地址,而且在对象死亡之后也需要JVM去回收这部分内存
内存分区
线程私有空间
线程私有的空间不会在线程间共享,没有并发问题,并且线程空间随着Java线程的启动和结束而创建或销毁
程序计数器(Program Counter Register)
当前所执行的字节码的行号指示器,字节码解释器就是通过这个值来获取当前需要执行的字节码
字节码的行号指示器的意思就是,当执行 Native 方法的时候不会分配程序计数器
JVM栈
Java方法运行时候的栈结构,执行Java方法时会将方法包装为栈帧,调用时会先将栈帧入栈,执行完毕之后出栈,最底下为Main方法
栈帧
局部变量表
存储局部变量,包括基本类型,对象引用以及方法的返回地址
操作数栈
主要用于保存方法执行过程中的中间结果,常说的字节码解释器是基于栈的,这里的栈就是指操作数栈
操作数栈只需要标准的出栈和入栈操作,例如两数相加会先将两数压入栈
动态链接
在Class文件中包含了类中包括方法在内的所有符号引用,在类加载的时候就会进入到JVM的运行时常量池中,而动态链接就是符号引用表示这些方法
返回地址
方法返回地址,也就是指向方法调用者的下一条指令地址
该区域存在 StackOverflowError 以及 OutOfMemoryError
Native方法栈
Java中Native方法运行时空间,和JVM栈也大致相同
线程私有
JVM内共享空间
堆
JVM中占比最大的一块内存空间,几乎所有的对象都会在这里分配内存,是GC的主要活动场所
根据GC
方法区
和堆一样属于线程共享的区域,存放加载的Class信息,常量,静态变量以及即时编译后的代码
在1.7之后的JVM实现中,方法区逐渐被拆解,Class信息被移动到了元空间,静态变量以及常量都被移动到了堆中
即时编译后的代码去向不明
元空间
在1.8出现的一片内存区,存放被加载的Class信息
属于直接内存,并不受JVM的管制,空间理论上取决于物理机的上限
直接内存/堆外内存
由Native区域直接分配得不由JVM管理的内存区域,可以通过类似DirectByteBuffer对象作为引用进行操作
因为不由JVM管理,所以内存的回收也必须显式操作
对象的内存结构
对象头
MarkWord
包含HashCode,GC年龄,锁信息等
在Synchronized的同步过程中,MarkWord用来保存偏向线程id,也会被拷贝到栈空间作为锁依据
类型指针
指向当前实例对象所属的类对象
数组长度
仅仅数组对象有,可以O(1)复杂度获取数组的长度
实例数据
对齐填充
一个对象的整体大小必须是8的整数倍
常量池
Java中的常量池可以分为两种:Class文件常量池以及运行时常量池,都是保存常量的地方
Class文件常量池
可以使用Javap查看字节码,类中的变量,方法名等都会被保存在同一块区域,而字节码以类似#1形式指定
运行时常量池
运行时常量池就是JVM在运行其间用于保存定义的常量的内存空间
Class文件常量池中的数据在加载之后就会进入运行时常量池
和Class文件常量池相比,运行时常量池可以通过 String.internt 方法动态新增内容
类加载子系统
Java中的类加载过程,就是指查找并加载Class文件,经过验证,解析变为运行时Class类的
类加载流程
加载
加载的主要任务就是找到.class文件,以二进制流的形式加载进虚拟机,并生成Class类对象
在加载一个类的时候除了先判断缓存,还会将其交给父类加载器加载,只有父类加载失败才会由自己加载
Class文件可以来自任何地方,Jar包、本地甚至是网络
连接
验证
验证流程主要保证Class文件的格式规范性,以及安全性
文件格式验证
该过程的对象就是加载初期的二进制流,对其进行分析,判断是否满足Class文件的规范
该阶段过后,所有的操作目标都变为了内存中的Class对象(运行时数据结构)
JVM的解释器就要求Class文件按照规范严格排列
元数据验证
元数据指的就是类元数据,包含类名,方法名等等
该阶段的验证主要针对类
字节码验证
该阶段的验证主要是类方法的执行流程,判断其语义是否正确
符号引用验证
符号引用转化为直接引用前的验证,验证符号引用指向的真实数据是否存在
准备
正式为`类变量`分配内存空间,并初始化为系统初值,例如int -> 0
类变量实质被 static 修饰的变量,并且不包含 final 关键字,常量此时应该从Class文件常量池,转移到运行时常量池
结合这个过程可以很好的理解 static 的语义,被 static 修饰的变量从属于类对象,被所有的实例对象共享,而没有被修饰的则从属于某个实例对象
解析
解析是将符号引用转化为直接引用的过程,在这之前会有符号引用验证
初始化
通过对static初始化块的手机组成<clinit>方法,正式`类变量`的过程
JVM 中唯一标识一个类对象的不仅仅是类名和地址,还有加载它的ClassLoader,也就是说不同的ClassLoader 可以多次加载同一个 Class文件
基本的类加载器
Bootstrap ClassLoader
由C++实现,最底层的类加载器,并不存在父加载器,负责加载<JAVA_HOME>/lib 下的依赖
Extension ClassLoader
由sun公司实现的扩展,父类为Bootstrap,负责加载 <JAVA_HOME>/lib/ext 目录下的依赖jar包
Application ClassLoader
负责加载 ClassPath 下的所有依赖包
双亲加载模式
双亲加载模式是在 java.lang.ClassLoader#loadClass 方法中实现的
优点
1. 类随着类加载器的不同有了一定的层次性,比如 java.lang.String 被 Bootstrap 加载,就说明他是最底层的类
2. 保证了核心类的安全,例如java.util.String常规途径不可能替换掉原有类
破坏双亲加载模式其实只需要覆盖 java.lang.ClassLoader#loadClass 就好
!!!Java 中默认使用当前类的加载器加载调用的类
并发实现
Java内存模型
Java 内存模型的出现是为了屏蔽不同硬件系统之间的差异性,让Java在各个平台之间的内存访问效果一致
Java 内存模型规定了 Java 的主内存和工作内存,主内容所有线程共享,但工作内存为线程私有,所有对象的修改只能在工作内存,并且不同线程之间无法互相访问对方的内存空间,所以线程之间的通信只能通过主内存
内存交互
JMM 规定了以下8种原子操作,从JVM层面保证其原子性,LOCK/UNLOCK/LOAD/READ/USE/ASSIGN/STORE/WRITE
对一个变量进行LOCK操作,会直接清空工作内存,使用时会再次从主内存获取
对一个变量UNLOCK时会主动将变量同步到主内存
相关命令
JMAP
JMAP 主要用来查看JVM中的各类内存对象
JMAP -histo <PID> | head -n NUM
可以用来查看当前JVM中占用内存最大的 NUM 个 对象
JMAP -heap <PID>
可以查看当前的堆内存分布情况,在内存报警的时候可以快速知道在哪个区,或者用来查看堆的分配比例
主要该命令可能会触发一次FULL GC
JSTACK
该命令主要用来查看Java中个线程的情况
查看CPU占用最大的线程
TOP 查看CPU 占用最高的进程
TOP -Hp <PID> 查看占用最大的线程
找之前需要将线程id转为16进制
JSTACK <PID> | grep
JSTATE
JSTATE 可以很方便的查看JVM相关的统计信息
收藏
0 条评论
下一页