深入理解java虚拟机(JVM)------一篇过
2021/7/18 12:05:57
本文主要是介绍深入理解java虚拟机(JVM)------一篇过,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
JVM——一篇过
发展史(不知道历史的程序员不是一个好秃子)
前身:Oak语言(91年开发消费性电子产品)
java me、java se、java ee
java me:移动终端java程序
java se:桌面级应用
java ee:企业家应用,包含了许多扩展包
JDK、jre、jvm
jvm:java虚拟机是运行java程序的,是实现跨平台
jre:包括jvm和java程序所需的核心类库,主要包含lang包
jdk:提供给开发人员使用的,包含了jre、java开发工具(javac.exe、jar.exe…)
年代史
95年,1.0发布—–>98年jdk1.2即时编译器——>04年jdk1.5(泛型、枚举等)——>08年sun被oracle收购74亿——>14年1.8lambda表达式,彻底移除永久代——>18Androidjava侵权赔88亿(相当公司白送的)
自动内存管理
程序计数器
线程私有的一个字节码行号指令器,当执行native方法时,为空(唯一一个不报空指针异常)
栈(线程私有)
jvm栈
java方法调用时分配空间存储局部变量等,用完就释放
本地方法栈
执行native方法时分配空间存储变量
堆(线程共享)
存放对象实例(new 对象,静态对象,1.7加入了静态变量和常量池),也是GC管理的区域,也叫GC堆
方法区(线程共享)
存储class、常量、静态变量
jdk1.7时将字符串常量和静态变量移到堆中,jdk1.8废除方法区的概念,改用元空间来存储class
元空间
jdk1.8时出现用来代替方法区的
为什么要用元空间
传统的方法区是占用jvm的内存的,是有内存溢出的风险,但是方法区是占用本地内存,是由电脑的内存决定,所以空间很大,基本不会出现内存溢出的情况
运行时常量池
属于方法区,即在程序运行时将一些常量加载到池中,常用的一种是:String.intern()
直接内存(了解)
不是jvm运行数据区,默认和最大堆内存一样大,在jdk1.4时,引入NIO,NIO利用native方法堆外内存,可以通过DirectByteBuffer对象对直接内存操作,避免了java堆和native堆的来回复制,即Netty(基于NIO实现的)的零拷贝性质
对象的创建(new 对象)
直接上图:类加载后面会讲,别问我怎么设置对象头啥的,有的东西记住就行了,就像我tm抽你,你问为什么,不为什么,你只要知道你被打了就行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RO9qq6ys-1626579613814)(C:\Users\liuguangcheng\AppData\Roaming\Typora\typora-user-images\image-20210717130246604.png)]
内存分配的两种方式
指针碰撞
假设堆内存左边是利用了的,右边是空闲的,中间有一个指针,分配内存时,将指针向右挪动对象内存大小,当使用Serial、ParNew收集器时(空间压缩整理)用这种内存分配方式
空闲列表
使用的内存和空闲的交错在一起,就需要维护一个列表,当分配内存时,将一个足够容纳对象大小的内存分配给它就行,当使用CMS(清除)收集器时用这种方式分配内存
后面会讲到垃圾收集器!!
对象内存布局
对象头
两部分:
- hashcode、GC分代年龄、锁的信息
- 堆向指向它的类型元数据的指针
数据部分
不仅包含了自己的数据,还包含了父类继承下来的字段信息
对齐填充(非必然存在)
HotSpot虚拟机要求对象大小是8字节的整数倍,不是的话就要通过对其填充来补全
对象的访问
-
句柄访问:通过句柄池找到对象指针,在通过指针找到对象
-
直接指针:直接通过对象的指针访问,快速效率高
理解不了?看下面例子
例如:你想找个老婆,你可以找媒婆帮你找一个,你也可以直接找一个,但是呢,结婚以后突然你老婆跑了,那你怎么办,在找一个呗,这时候找媒婆的优势就来了,省时省力,直接叫她在帮你找一个,但是你自己找的老婆呢,就又要自己辛辛苦苦去找
内存溢出(OOM)和内存泄露(ML)
内存泄露:无用对象(垃圾)的内存不释放,导致这部分回收不了,一直占着
内存溢出:在程序申请内存时,内存不够,出现的一种异常,内存泄露的后果就是内存溢出
垃圾收集和内存分配策略
垃圾回收
无用对象的回收叫垃圾回收,怎么判断是不是垃圾呢?
- 引用计数算法:对象添加引用计数器,每次有对象引用改对象时,就会+1,引用失效-1,计数为0时就是可以回收的,该算法看是简单,实际却需要很多额外处理,所以基本没有虚拟机用该算法
- 可达性分析:通过“GC Roots”作为根节点集,跟据引用链向下搜索,没有在引用链上的对象就是可回收对象
四大引用
- 强引用:Object o=new Object()这种,垃圾回收永远不会回收
- 软引用:java类中有一个SoftReference来实现软引用,在内存溢出之前会被回收
- 弱引用:java提供了WeakReference来实现,垃圾回收时就会回收掉该对象
- 虚引用:PhantomReference,唯一的目的是为了在该对象被回收时收到一个通知
回收过程
当判定一个对象是不可达时不会被马上回收掉的,必须经过连个阶段
- 不可达判断为true,第一次标记
- 对象是否执行了finalize方法,如果执行就代表已经被回收过了,如果没有就放入一个F-Queue,稍后会对queue中第二次标记,将标记两次的对象由一个线程优先级低的Finalizer线程去执行finalize方法回收对象
垃圾回收算法
标记清除算法
标记需要回收的对象后,统一回收
缺点:
- 执行效率不稳定,回收对象过多效率会降低
- 内存碎片化问题
标记复制算法
将对象回收后的存活对象复制到另一个等大的内存中,将原来的空间清理,在将两块空间对换,该算法多用于新生代回收算法
缺点:对空间的浪费
优点:解决了内存碎片化的问题,且新生代朝生夕灭,存活对象少,所以效率很高
标记整理算法
将对象回收后的空隙进行整理,多用于老年代的回收算法
缺点:整理式对象移动必须全程暂停用户应用程序,否则导致指针混乱,就像你数鸭子,鸭子一直跑,着你怎么数
优点:由于老年代的对象都是老干部,都是经过风吹雨打存活下来的,所以比较少对象会被回收,所以整理的代价还是比较小的
HotSpot算法实现细节
根节点枚举
根节点枚举必须暂停所以线程,可达性分析时,并不需要1个不漏的遍历,HotSpot利用了一个OopMap数据结构来存放对象引用,在特定位置记录栈和寄存器的引用
安全点
OopMap在特定位置记录时的特点位置就是安全点,所以线程执行到安全点后就开始垃圾收集
安全区域
在某一段区域的任意地方进行垃圾收集都时安全的
垃圾回收器
Serial收集器
早期的**“单线程”(新生和老年垃圾回收时暂停所以工作)**新生代垃圾回收器,对于运行在客户端模式的虚拟机时一个不错的选择
ParNew收集器
Serial的多线程(新生代多线程)版本,但是垃圾回收时,依旧需要暂停所以工作
Parallel Scavenge收集器
是一个重吞吐量的新生代多线程垃圾回收器
Serial Old收集器
Serial的老年代版本收集器,也是一个单线程
Parallel Old收集器
多线程Parallel Scavenge的老年代版本,不能和CMS配合
CMS收集器
以最短回收时间停顿的收集器,内存足够时,使用标记清除,不足时用标记整理
过程
- 初始标记(停止工作)
- 并发标记
- 重新标记(停止工作)
- 并发清除
G1收集器
面向服务端的全堆垃圾收集器,哪块存放的垃圾最多,回收效益最大,着就是Mixed GC模式
ZGC收集器
利用染色体指针来实现低延迟垃圾回收器
染色体指针:一种可扩展的存储结构用来记录更多与对象标记、重定向过程相关的数据
实战
- 对象优先分配在Eden:大多数情况下,对象都是分配在Eden新生代的
- 大对象直接进入老年代:大对象的垃圾回收很耗费资源,所以要避免过多大对象进入新生代
- 长期存活的对象将进入老年代(默认是年龄15)
- 动态对象年龄判定:相同年龄数量大于总数一半,这些相同年龄就应该直接进入老年代,否则可能会导致新生代回收效率低下
- 空间分配担保:发生minor GC之前,会去检查老年代可用空间是否大于新生代总对象空间,大于是安全的,根据概率赌不会使老年代空间不够而发生full GC
类加载机制
特例:解析过程可能会在初始化之后(动态绑定或晚期绑定)
6种情况必须初始化
- new 、getstatic、putstatic、、invokestatic指令时
- new实例化对象时
- 定义该数组类,不会初始化
- 对类型进行反射时
- 父类为初始化时,先初始化父类
- 调用父类的静态变量,子类不会初始化
- 使用常量池的变量,不会初始化
- 1.8加入default接口实现类初始化,接口也要初始化
- 方法句柄没有初始化时
加载
- 利用非数组的类的全限类名来获取二进制字节流
- 将字节流转换到方法区(1.8之后的元空间)中
- 生成一个class
数组类加载:数组类不通过类加载器加载创建,而是直接在内存动态构造
验证
保证class文件字节流符合约束,且不会对jvm造成危害
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
对static变量初始化并赋原始值(int赋0),而真正的赋值是在构造器中赋值
解析
将常量池的符号引用替换为直接引用
初始化
执行类构造器()方法的过程
类加载器
一个类的全限类名二进制字节流放到jvm外部,让程序直接去获取需要的类
双亲委派机制
每次都会将类给父类加载器去尝试加载,加载不了才会给子类加载
顺序:启动类加载器——>扩展类加载器——>应用程序类加载器——–>自定义类加载器(必须继承抽象类ClassLoader)
被破坏过3次
- jdk1.2才提出双亲委派机制,所以1.2之前的并不符合
- 线程上下文加载器:父类加载器去请求子类加载器完成类加载
- 模块化热部署时会破坏
程序编译和代码优化
编译器
- 前端编译器:javac编译器将.java编译成.class文件
- 即时编译器:C1、C2,Graal编译器将字节码变成本地机器码
- 提前编译器:ATO编译器把程序编译成与目标机器指令想关的二进制代码
优化
前端编译器(前端优化):对代码基本无优化,但是对编码效率和使用等有比较多的优化手段(语法糖等)
即时编译器(后端优化):几乎大代码有话都放到该阶段完成,运行期优化可使一些可移植class也享受到优化
编译过程
1个准备、3个处理过程
- 准备过程:初始化插入注解处理器
- 解析填充符号表过程
- 语法、词法、填充符号表
- 插入式注解处理器处理注解过程
- 分析与字节码生成过程
- 静态信息检查、动态运行检查、解语法糖、字节码生成
泛型
Java的实现方式是类型擦除式泛型
类型擦除
- 运行期原始泛型派生对原始类型的继承关系
- 编译器直接抹除(例:ArrayList —–>ArrayList)
既然抹除了泛型,为什么反射还能取得参数化类型?
答:虽然在字节码抹除了泛型,但是在元数据还是保留了泛型信息
后端编译和优化
即时编译器
作用:将字节码中的**热点代码(多次调用的方法、多次调用的循环体)**编译成本地机器码
- C1编译器:又叫客户端编译器
- C2编译器:又叫服务端编译器
- Graal编译器:JDK10出现来替换C2的编译器,现在不清楚
热点探索
- 采样:周期性检查栈顶,经常在栈顶的方法(J9)
- 计数器:每次调用就+1(HotSpot)
热度衰减
一定时间内,调用次数不够即时编译器门槛,就会在垃圾回收时随便计数器减半
提前编译器
2分支作用:
- 本质时给即时编译器做缓存加速
- 静态提前编译(在系统空闲时编译)
即时编译器对提前编译器的优势
- 性能分析制导优化:热点代码集中优化和分配更好的资源
- 激进预测性优化:性能监控支持概率预测优化
- 链接时优化:动态链接
编译器优化技术(前沿技术)
对中间代码或机器码进行代码优化,而不是对源码优化
方法内联
将方法体替换方法调用,增加了代码量,但是减少了地址引用
逃逸分析
对象逃出原本的作用域(return 对象)
公共子表达式消除
表达式计算过,且过程变量不变,则直接使用
数组边界检查消除
1.编译期完成边界检查 2.隐式异常处理
一些普通的优化
常量迭代、算数聚合、符号合并……
Graal编译器
展望替换HotSpot成为一款高效编译、高质量输出、支持即时编译和提前编译、支持不同虚拟机的编译器、继承HotSpot的高质量优化技术
java内存模型(JMM)与线程
硬件效率与一致性
处理器的熟虑与内存的速度根本不是一个等级,这样就限制了我们的效率,所以引入了高速缓存的存储交互(但是带来了缓存一致性问题)
JMM
目的:定义程序中各种变量的访问规则
怎么解决缓存一致性问题:
volatile
- 可见性:解决内存屏障带来的不可见问题,每次读取之前会去主存中查看最新的值,判断变量是否过期,过期的话就会从主存读取最新的值,每次写操作时将缓存行数据立即写入主存,且让其他其他CPU缓存了改变量的地址无效(简单来说就是每次使用前向主内存主刷新)
- 禁止指令重排:在并发的情况下,JVM的指令重排优化可能会对变量的操作产生影响,这是就需要禁止指令重排来避免
内存屏障
由于编译器的优化和缓存的使用,导致对内存的写入操作不能及时的反应出来,也就是说当完成对内存的写入操作之后,读取出来的可能是旧的内容
long、double(64位)的非原子协议
对于64位数据类型,虚拟机会将它的读写操作分为3次32位的操作,这样就不能保证操作的原子性了
例如:两个线程同时去对long或double进行读取和修改,可能即得不到原来的值,也得不到修改后的值
实际开发:除非你明确知道该类型变量有线程竞争,否则没有必要去加volatile关键字
线程实现(了解)
Thread的所以关键方法都声明位Native,即本地方法
三种实现方式
- 内核线程实现(1:1)一般不会直接使用内核线程
- 用户线程(1:n):我们常用的Thread线程的建立即与他操作都在用户态完成,不需要内核帮助,程序设计好就不需要切换导内核态,高速低消耗
- 混合实现:内有轻量级线程作为桥梁,对状态的切换等提高很多,也可以支持大规模的用户线程并发
状态转换
- new 创建未使用
- Runable:
- Running:运行时
- Ready:准备运行,等待CPU分配时间片,阻塞状态获取导锁后就进入准备运行状态
- Waiting:无限等待被其他线程唤醒,在等待池中
- Timed Waiting:休眠,等时间过来就会苏醒,典型的sleep方法(sleep期间不会释放对象的锁)
- 阻塞:等待获取一个排他锁,等待线程被唤醒后就加入阻塞状态
线程安全和锁优化
线程安全
当一个线程操作变量时,其他的线程不会对该变量的操作造成影响
- 不可变变量:String 、枚举、final修饰的变量、java.lang.Number的部分子类(Long、Double…)
- 绝对线程安全:不管什么环境,都是线程安全的
- 相对线程安全:大部分线程安全集合类(Vector、hashMap….)等
锁优化
JDK5—->JDK1.6加入了很多的锁优化手段:适应性性自旋、锁消除、锁膨胀、轻量级锁、偏向锁
自旋锁
默认开启,在获取锁失败后,会自旋重试来获取锁
锁消除
对被检测导明确不会存在锁竞争的锁进行锁消除来提高运行效率,锁消除的根据还是逃逸分析技术(前面讲过了)
锁粗化
一串零碎的锁操作对同一个对象,加锁的范围扩展导整个操作序列的外部
轻量级锁
对象头(mark word)记录一些的对象的非数据信息,里面就包含了锁标志等,轻量级锁锁升级后会变成重量级锁
偏向锁
将mark word内的偏向模式设为1,标志位设位01,则是偏向锁状态,锁竞争会锁升级位轻量级锁
具体的锁和并发可看JUC
final修饰的变量、java.lang.Number的部分子类(Long、Double…)
2. 绝对线程安全:不管什么环境,都是线程安全的
3. 相对线程安全:大部分线程安全集合类(Vector、hashMap….)等
锁优化
JDK5—->JDK1.6加入了很多的锁优化手段:适应性性自旋、锁消除、锁膨胀、轻量级锁、偏向锁
自旋锁
默认开启,在获取锁失败后,会自旋重试来获取锁
锁消除
对被检测导明确不会存在锁竞争的锁进行锁消除来提高运行效率,锁消除的根据还是逃逸分析技术(前面讲过了)
锁粗化
一串零碎的锁操作对同一个对象,加锁的范围扩展导整个操作序列的外部
轻量级锁
对象头(mark word)记录一些的对象的非数据信息,里面就包含了锁标志等,轻量级锁锁升级后会变成重量级锁
偏向锁
将mark word内的偏向模式设为1,标志位设位01,则是偏向锁状态,锁竞争会锁升级位轻量级锁
具体的锁和并发可看JUC
这篇关于深入理解java虚拟机(JVM)------一篇过的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-14后台交互资料入门指南
- 2024-11-14如何轻松创建项目环境:新手入门教程
- 2024-11-14如何抽离公共代码:初级开发者指南
- 2024-11-14Python编程入门指南
- 2024-11-14Python编程入门:如何获取参数
- 2024-11-14JWT 用户校验:简单教程与实践
- 2024-11-14Pre-commit 自动化测试入门指南
- 2024-11-14Python编程基础
- 2024-11-14Server Action入门教程:轻松掌握服务器操作
- 2024-11-14Server Component入门教程:轻松搭建服务器组件