JVM (Java Virtual Machine)
2022-03-03 18:09:44 0 举报
AI智能生成
jvm相关的知识和重点都有标注出来,以后的面试重点也会同步更新上去,花费了我很多精力和时间去整理这个资料,喜欢的可以下载下来
作者其他创作
大纲/内容
JVM基础信息
虚拟机(Virtual Machine)
含义
就是一台虚拟的计算机,是一款软件,用来执行一系列虚拟计算机指令,虚拟机分为程序虚拟机和系统虚拟机
系统虚拟机
Visual Box、Vmware 就是系统虚拟机,他们完全是对物理计算机的仿真。提供了一个可运行完整操作系统的软件平台
程序虚拟机
典型的就是Java虚拟机,专门为执行单个计算机程序而设定,在java虚拟机中执行指令被我们成为java字节码指令
java虚拟机含义
1、java虚拟机是一台执行java字节码的虚拟计算机,拥有独立的运行机制,其运行的java字节码也未必由java语言编译而成
2、JVM平台的各种语言可以共享java虚拟机带来的跨平台性,优秀的垃圾回收器以及可靠的及时编译器
3、java的核心技术就是java虚拟机(Java Virtual Machine)所有的java程序都是运行在java虚拟机内部
java虚拟机的作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释和编译为对应平台上机器指令,每一条java指令,jvm规范中都有详细的定义(怎么操作取数、怎么处理操作数,处理结果)
java虚拟机的特点
一次编译处处运行
自动内存管理
自动内存回收
JVM架构模型
基于栈式架构
设计和实现更简单,适用于资源受限的系统
避开了寄存器的分配难题,使用零地址指令方式分配
指令流中的指令大部分都是零地址指令,其执行的过程中依赖于操作栈,指令集更小(8位),编译器容易实现
不需要硬件支持,可移植性更好,更好的实现跨平台
基于寄存器架构
典型的应用是x86的二进制指令集,比如传统pc以及安卓的Davlik虚拟机
指令集架构完全依赖硬件,可移植性差
性能优秀和执行高效
花费更少的指令(16位)执行一项操作
在大部分情况下,基于寄存器架构的指令集往往都是以1地址指令,2地址指令,3地址指令为主,而基于栈式架构的指令集却是以零地址为主
JVM生命周期
虚拟机启动
java虚拟机的启动是通过引导类加载器(Bootstrap class loader)创建一个初始类(init class)来完成,这个类是由虚拟机的具体实现指定
虚拟机执行
一个运行中的java虚拟机有着一个清晰的任务,执行java程序
程序开始执行时他才运行,程序结束时他就停止
执行一个所谓的java程序时,真正在执行的是一个叫Java虚拟机的进程
虚拟机退出
程序正常执行结束
程序在执行过程中遇到了异常或错误而异常终止
由于操作系统出现错误而导致Java虚拟机进程终止
某线程调用RunTime 类或者System类中exit方法,或者RunTime 类的halt方法,并且Java安全管理器也允许这次exit或者halt操作
除此之外,JNI(Java Native Interface)规范描述了JNI Invocation API 来加载或者卸载java虚拟机时,虚拟机退出情况
VM虚拟机
oracle 的HotSpot虚拟机
oracle 的JRockit
IBM公司J9
运行时数据区域
Java堆(Heap)
垃圾回收
概念
垃圾是指运行程序中没有任何指针指向的对象,这个对象就是需要回收的垃圾
如果不及时对内存中的垃圾进行清理,那么,这些对象所占的内存空间会一直保留到应用程序结束,被保存的空间无法被其它对象使用,甚至可能导致内存溢出
作用
1),如果不进行垃圾回收,那么内存迟早有一天会被用完,因为不断地分配内存空间而不进行回收,就好像一直生产垃圾不进行回收垃圾
2),除了释放那些没用的对象,垃圾回收也可以清除内存里的记录碎片,碎片整理将所占用的堆内存移动到堆另一端,以便 JVM将整理出内存分配给新对象
3), 没有GC 就不能保证应用程序正常执行
自动内存管理缺点
1),弱化java 开发人员在程序在程序出现内存溢出时定位问题和解决问题的能力
2),需要排查各种内存溢出,内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们必须对这些自动化的技术实施必要的监控和调节
垃圾回收算法
标记–清除算法
步骤
标记:遍历整个内存区域,对需要回收的对象打上标记
清除:再次遍历内存,对标记过的内存进行回收。
适用场景
只有小部分对象需要进行回收的,适用于老年代的垃圾回收,因为老年代一般存活对象会比回收对象要多。
优点
简单、收集速度快,但会有空间碎片,空间碎片会导致后面的GC频率增加。
缺点
效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,虽然总的可用内存是够的,但是由于太过分散,无法找到一块连续的且满足分配要求的,因而不得不再次触发一次GC。
复制算法
过程
将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。
适用场景
只有少量对象存活的场景,这也正是新生代对象的特点,所以一般新生代的垃圾回收器基本都会选择标记复制法。
优点
收集速度快,可以避免空间碎片,但是有空间浪费,存活对象较多的情况下复制对象的过程等会非常耗时,而且需要担保机制。
缺点
1、会浪费一部分空间:通过上面的图我们也不难发现,总是会有一块空闲的内存区域是利用不到的,这也造成了资源的浪费。
2、存活对象多会非常耗时:因为复制移动对象的过程是比较耗时的,这个适合不仅需要移动对象本身还需要修改使用了这些对象的引用地址,所以当存活对象多的场景会非常耗时,这也提示我们标记复制法比较适合存活对象较少的场景。
3、需要担保机制:因为复制区总会有一块空间的浪费,而为了减少浪费空间太多,所以我们会把复制区的空间分配控制在很小的区间,但是空间太小又会产生一个问题,就是在存活的对象比较多的时候,这时复制区的空间可能不够容纳这些对象,这时就需要借一些空间来保证容纳这些对象,这种从其他地方借内存的方式我们称它为担保机制。
标记–整理算法
步骤
标记:对需要回收的进行标记
整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。
适用场景
内存吃紧,又要避免空间碎片的场景,老年代想要避免空间碎片问题的话通常会使用标记整理法。
优点
相对于标记复制法不会浪费内存空间,相对标记清除法则可以避免空间碎片,但是速度比其他两个算法慢。
缺点
标记整理法是三种垃圾回收算法中性能最低的一种,因为标记整理法在移动对象的时候不仅需要移动对象,还要额外的维护对象的引用的地址,这个过程可能要对内存经过几次的扫描定位才能完成,做的事情越多那么必然小号的时间也越多。
分代收集算法
内存模型
GC类型
Minor GC工作原理
Full GC工作原理
GC日志
GC 日志开启
理解GC日志
JVM常用参数
垃圾收集器
Serial 收集器
ParNew收集器
Parallel Scavenge收集器
Serial Old收集器
Parallel Old收集器
CMS收集器
Garbage First收集器
简介
一个JVM实例只存在一个堆内存,堆也是Java 内存管理的核心区域
Java 堆区在JVM启动的时候即被创建,其空间大小也确定了,是JVM管理的最大一块内存空间
<JVM规范> 规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
所有的线程共享Java 堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer ,TLAB)
<JVM规范> 中对java 堆的描述是: 所有的对象实例以及数组都应当在运行时分配在堆上
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
在方法结束后,对中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆是GC 执行垃圾回收的重点区域
年轻代( YongGen)和老年代 (OldGen)
年轻代:声明周期较短的瞬时大小,这类对象的创建和消亡都非常迅速
老年代:生命周期非常长,在某些极端的情况下还能与JVM的生命周期保持一致
默认 -XX:NewRatio=2 表示新生占1,老年代占2,新生代占整个堆的1/3
-XX:NewRatio=4 表示新生代1,老年代占4,新生代占整个堆的1/5
几乎所有的Java 对象都在Eden中被new 创建出来的
可以使用选项 -Xmn 设置新生代最大内存大小
对象分配过程
简介
为新对象分配内存是一种非常严谨和复杂的任务, JVM的设计者们不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片
过程
1), new 的对象先放入 Eden ,此区有大小限制
2), 当Eden 的空间填满时,程序又需要创建对象,JVM的垃圾回收器会对 Eden 进行垃圾回收(Minor GC) , 将Eden 中的不再被其它对象所引用的对象进行销毁,在加载新的对象放入 Eden中
3), 然后将Eden 中剩余的对象移动到 幸存者 0区
4), 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区,如果没有回收,就会放入到幸存者1区
5), 如果再次经历垃圾回收,此时会重新放入到幸存者0区, 接着再去幸存者1区
6), 啥时候能去养老区? 可以设置次数,默认是15次 -XX:MaxTenuringThreshold=N 来进行设置
7), 在养老区,相对悠闲,当养老区内存不足时,再次触发Major,进行养老区的内存清理
8), 若养老区执行了 Major,之后依然发现无法进行对象的保存,就会产生OOM异常
总结
针对幸存者0区和1区,复制之后有交换,谁空谁是TO
关于垃圾回收,频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集
年轻代GC(Young GC/Minor GC) 触发机制
1), 当年轻代空间不足的时候,就会触发 Minor GC ,这里年轻代指的是 Eden满, Survivor 满则不会引发YGC(每次Minor GC 会清理年轻代的内存)
2), 因为java 的对象大多具备朝生夕死的特性, 所以 Minor GC 非常频繁,一般回收速度很快,这一定义即清晰又易于理解
3), Minor GC 会引发 STW(stop the world),暂停其它用户线程,等垃圾回收结束,用户线程才能恢复
老年代GC(Major GC /Full GC) 触发机制
1), 指发生在老年代的GC, 对象从老年代取消时,我们说 Major GC 或 Full GC 发生了
2), 出现 Major GC ,经常伴随至少一次的Minor GC (并非绝对)
3), Major GC 的速度一般是 Minor GC 的十倍以上,STW停留的时间更长
4), 如果 Major GC 后,内存还是不足,就报OOM异常
Full GC 触发机制
1), 调用 System.gc() 时,系统建议执行 Full GC 但是不必然执行
2),老年代空间不足
3), 方法区空间不足
4), 通过 Minor GC 后进行老年代的平均大小大于老年代的可用内存
5), 由Eden 区, Survivor S0区向 S1区复制时,对象大小2大于 To 空间可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
说明: Full GC 是开发或者调优中尽量避免的,这样暂停时间会短一些
堆空间分代思想
为什么需要把Java 堆分代?不分代就不能正常工作?
经过研究, 不同对象的声明周期不同,70%-90% 的对象都是临时对象。
1), 新生代,有Eden ,s0,s1构成
2), 老年代:存放新生代中的经过多次仍然存活的对象
为什么需要把Java 堆分代? 不分代就不能正常工作了?
其实不分代完全可以,分代的唯一理由就是优化GC性能,如果没有分代,那所有的对象都在一块,GC需要找到那些没用的对象就需要全部遍历一遍,而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放在某一个地方,当GC时,先把这块存储朝生夕死对象的区域进行回收,可以腾出很大的空间
内存分配策略 ( 或 对象提升(Promotion)规则)
如果对象在 Eden出生并经过 Minor GC 之后,仍然存活,并且能被 Surivor 容纳的话, 将被移动到 Surivor 中,将对象的年龄设置为1, 对象在 Surivor 中每经过一次 MinorGC 对象年龄就会加1,当年龄超过15岁时,就会晋升老年代
1),优先分配到Eden
2), 大对象直接存放到老年代 -- 尽量避免程序中出现过多的大对象
3), 长期存活的对象分配到老年代
4), 动态对象年龄判断
如果 Surivor 区中的相同年龄的所有对大小的总和大于 Surivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到
MaxTenuringThreshold 要求的年龄
5), 空间分配担保
为对象分配内存TLAB(Thread Local Allocation Buffer)
定义
1), 从内存模型而不是垃圾收集的角度,对Eden 区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
2), 多线程同时分配内存时, 使用TLAB 可以避免一系列的非线程安全问题,同时还能够提高内存分配的吞吐量, 因此我们可以将这种分配方式成为 快速分配策略
3), 据我所知所有 OpenJDK 衍生出来的JVM 都提供了TLAB 设计
意义
1), 堆区是线程共享区域,任何线程可以访问堆区中的共享数据
2), 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区划分内存空间是线程不安全的
3), 为避免多个线程操纵同一地址,需要使用加锁机制,进而影响分配速度
说明
1), 尽管不是所有的对象实例都能在 TLAB 中成功分配, 但JVM 确实是将TLAB 作为内存分配的首选
2), 在程序中, 开发人员可以通过设置 -XX:UseTLAB 是否开启TLAB空间
3), 默认情况下, TLAB 空间的内存非常小, 仅占有整个 Eden 空间的 1%, 当然我们可以通过选项 -XX: TLABWasteTargetPercent 设置TLAB 空间所占用 Eden空间的 百分比大小
4), 一旦对象空间分配内存失败时, JVM就会尝试通过 加锁机制来 确保数据操作的原子性, 从而直接在Eden 空间中分配内存
堆参数设置
-XX:+PrintFlagsInitial : 查看所有的参数默认值大小
-XX:+PrintFlagsFinal : 查看所有参数的最终值
-Xms : 堆初始默认值大小(默认物理内存的1/64)
-Xmx: 堆最大空间大小(默认物理内存1/4)
-Xmn: 设置新生代的大小
-XX:NewRadio: 配置新生代与老年代在堆结构的占比
-XX:SurvivoRatio: 设置新生代中 Eden 与 S0/S1空间的比例
-XX:MaxTenuringThreshold : 设置新生代垃圾的最大年龄
-XX:+PrintGCDetails : 输出详细的GC处理日志
-XX: HandlePromotionFailure : 是否设置空间内存担保
1), 发生 YGC 之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间
如果大于,此次YGC是安全的
如果小于,则虚拟机会查看-XX:HandlePromotionFailure 设置的值是否允许担保失败
如果大于则尝试进行一次YGC 这次YGC依然会有风险
如果小于 则改为一次FUllGC
2), 只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Major GC 否则进行Full GC
堆是分配对象存储的唯一选择?
在<深入理解java虚拟机>关于java 堆内存有这样一段描述:
随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化,所有的对象都分配在堆上显得不是那么绝对了
在java 虚拟机中,对象是在java堆中进行分配的,这是一个普遍的常识,但是有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis) 后发现,一个对象并没有逃逸出方法的话,那么它可能被优化成栈上分配,这样就无需堆上分配内存,也无需进行垃圾回收,这就是常见的堆外存储技术
此外,基于OpenJDK 深度定制的 TaoBaoVM ,其中创新的GCIH(GC invisible Heap) 技术实现了 off-heap ,将声明周期长的java 对象从heap中移至 heap 外,并且 GC不能管理GCIH 内部的java对象, 以此达到降低GC的回收频率和提升GC回收效率的目的
逃逸分析
概述
如何将堆上的对象分配到栈,需要使用逃逸分析手段
这是一种可以有效减少java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法
通过逃逸分析,Java HotSpot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否将这个对象分配在堆上
逃逸分析的基本行为就是分析对象动态作用域
1), 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
2), 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸,例如作为调用参数传递到其它地方中
参数设置
在JDK7以后, HotSpot 中默认开启了逃逸分析
如果使用的是较早版本,开发人员可以通过
1), -XX:+DoEscapeAnalysis 显示开启逃逸分析
2), -XX:+PrintEscapeAnalysis 查看逃逸分析的筛选结果
代码优化
1),栈上分配
JIT编译器 在编译期间根据逃逸分析的结果,发现如果没有逃逸出方法的话,就可能被优化为栈上分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,这样就无需进行垃圾回收
常见栈上分配的场景
**成员变量赋值,方法返回值,实例引用传递**
---
**如何快速判断对象是否逃逸,就看new 的对象实体是否有可能在方法外部被调用**
将堆分配转为栈上分配,如果一个对象在子程序中分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
2),同步省略
1),线程同步的代价相当高的,同步的后果是降低并发性和性能
2),在动态编译同步块的时候,JIT编译器可以借助逃逸分析**来判断同步块(synchronizer)所使用的锁对象是否只能被一个线程访问而没有被发布到其它线程**。
如果没有,那么JIT编译器在编译这个同步代码块的时候就会取消这部分代码的同步,这样就能大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁同步
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
3),分离对象或标量替换
**标量**: 指一个无法再分解成更小的数据的数据,java中基本数据类型就是标量
还可以在分解的数据就是 聚合量, java中的对象就是**聚合量**,因为他可以分为标量和聚合量
在JIT阶段, 如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个成员变量来替代,这个过程叫 **标量替换**
参数配置
-XX:+EliminateAllocations 默认开启标量替换,允许将对象打散分配在栈上
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中
总结
开发中能使用局部变量的,就不要使用在方法外定义
虚拟机栈(JVM Stacks)
背景
栈是运行时的单位,而堆是存储单位 ,即 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据,堆解决的是数据存储问题,即数据该往哪里放
定义
每个线程在创建时都创建一个虚拟机栈,其内部都会保存一个栈帧,一个栈帧对应一个java方法
生命周期
与线程一致
作用
主管Java程序运行,它保存方法的局部变量,部分结果,并参与方法的调用与返回,栈是运行时的单位,而堆是存储单位 ,即 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据,堆解决的是数据存储问题,即数据该往哪里放
优点
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM 直接对Java 栈的操作只有两个
每个方法执行,伴随着入栈和出栈
执行结束后的出栈工作
对于栈来说不存在垃圾回收问题
常见异常
outofmemoryError(内存溢出)
stackOverflowError(栈溢出)
栈的存储单位
基本介绍
每个线程都有自己的栈,栈中的数据都是以栈帧(stack frame) 以基本单位进行存储的
在这个线程上正在执行的每个方法都各自对应一个栈帧
栈帧是一块内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
运行原理
JVM 直接对Java 栈的操作只有两个,入栈 和出栈,遵循先进后出,后进先出的原理
在一条活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧是有效的,这个栈帧被称为当前栈帧
与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类
执行引擎运行的所有字节码指令只针对在当前栈帧进行操作
如果在该方法中调用了其它方法,对应的新栈帧会被创建出来,放在栈的顶端,成为新的当前栈
不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另一个线程的栈帧
如果当前方法调用了其它方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
Java 方法有两种返回函数的方式,1), 正常函数返回,使用return 指令 2), 抛出异常,不管使用哪种方式,都会导致栈帧被弹出
栈帧的内部架构
局部变量表 (Local Variables)
基本介绍
局部变量表也被称为局部变量数组或本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型,对应引用(Reference) 以及 returnA队的类型
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此 不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 code 属性的 maximum local variables 数据项中,在方法运行期间是不会改变局部变量 表的大小的
方法嵌套调用的次数由栈的大小决定,一般来说,栈越大,方法嵌套调用的次数就越多
局部变量表中的变量只在当前方法调用中有效,在方法执行时,虚拟机通过使用局部变量表来完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁
Slot 的理解
参数值的存放总是在局部变量数组的index 0开始,到数组长度-1 的索引结束
局部变量表,最基本的存储单元是Slot(变量槽)
在局部变量表里, 32位以内的类型只占用一个 solt(包括 rerurnAddress类型),64 位的类型(long和double) 占用两个 solt
1), byte ,short,char 在存储前转换为 int , boolean 也被称为int ,0表示false 非0表示true
2), long 和 double 则占据两个 solt
JVM 会为局部变量表中的每一个solt 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个solt上
如果需要访问局部变量表中一个 64bit 的局部变量值时,只需要使用前一个索引即可(比如long 或double类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this 将会存放在index 为0的solt处,其余的参数按照参数表的顺序继续添加
Slot 重复利用问题
栈帧的局部变量表中的槽位是可以重复重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
静态变量与局部变量的对比
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域进行分配
类加载的过程中,在链接Linking 中准备阶段,执行初始化,对类变量设置初始值为0, 局部变量则是初始化阶段赋予程序员对定义的变量进行初始化
和类变量不同的是,局部变量表不存在系统初始化的过程,这意外着一旦定义了局部变量则必须认为的初始化,否则无法使用
补充
在栈帧中, 与性能调优关系最为密切的部分就是前面提到的局部变量表,在方法执行时,虚拟机使用局部变量表来完成方法的传递
局部变量表的变量也是重要的垃圾回收根节点,只要局部变量表中直接或间接引用的对象都不会被回收
操作数栈 (Operand Stack)(或表达式栈)
基本介绍
每一个独立的栈帧中除了包括 局部变量表之外,还包含一个先进后出的操作数栈,也可以称为表达式栈
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数,即入栈(push)和出栈(pop){某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们在把结果压入栈
比如: 执行复制,交换,求和等操作}
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就确定好了,保存在方法的code 属性中, 为max_stack的值
栈中的任何一个元素都是可以任意的Java 数据类型
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据的访问
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中的下一条需要执行的字节码指令
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段再次验证
另外,我们说JVM 的解释引擎是基于栈的执行引擎,其中栈指的是操作数栈
常见的i++和++i区别
栈顶缓存(Top of Stack Cashing) 技术
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑(8位字符) 但完成一项操作的时候必须引入更多的入栈和出栈指令,这同时也就意味着需要更多的指令分派次数和内存读写次数
由于操作系统时存储在内存中的,因此频繁地执行内存读写操作必然会影响执行速度,为了解决这个问题,HotSpot JVM的设计者提出了栈顶缓存技术
将栈顶元素全部缓存在物理CPU寄存器(16位)中,以此降低对内存的读写次数,提升执行引擎的执行效率
动态链接 (或指定运行时常量池的方法引用)
基本介绍
每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)
在Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class 文件的常量池里
比如描述一个方法掉用了另外其他方法时,就是通过常量池中指向方法的符号引用来完成,那么动态链接的作用就是为了将这些符号引用转换为方法的直接引用
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别
方法调用
静态链接
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译器可知,且运行期间保持不变的话,这种情况下将调用方法的符号引用转换为直接引用的过程称之为 静态链接
动态链接
如果被调用的方法在编译期间无法确定下来,也就是说,只能够在程序运行期间将调用方法的符号引用转换为直接引用,由于这种引用转换的过程具备动态性,因此也被称之为动态链接
早期绑定 (Early Binding)
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期间保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是那个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
晚期绑定 ( Late Binding)
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期间根据实际的类型绑定相关的方法,这种绑定称之为晚期绑定
虚方法
非虚方法
如果方法在编译期间都确定具体的调用版本,这个版本在运行期时是不可变的,这样的方法称为 非虚方法
静态方法, 私有方法, final 方法, 实例构造器, 父类方法都是非虚方法
其它方法称之为 虚方法
虚拟机提供了一下几条方法调用指令
1), invokestatic : 调用静态方法,解析阶段确定唯一方法版本
2), invokespecial : 调用 <init> 方法,私有方法,及父类方法,解析阶段确定唯一方法版本
3), invokevirtual : 调用所有虚方法(晚期绑定)
4), invokeinterface : 调用接口方法
5), invokedynamic : 动态解析出需要调用的方法,然后执行
前 4条指令 固话在虚拟机内部,方法的调用执行不可人为干预, 而 invokedynamic 指令则支持由用户确定方法版本
invokestatic 指令 与 invokespecial 指令 调用的方法称为非虚方法,其余的(final修饰除外)称为虚方法
静态类型语言(Java)与动态类型语言区别
两者区别在于对类型语言的检查是在于编译器还是运行期间确定的,在编译器确定的就是静态类型语言,在运行时确定的就是动态类型语言
Java 语言中方法重写的本质
1), 找到操作数栈顶的第一个元素所执行的对象的实际类型,记做 C
2), 如果在过程结束,如果不通类型C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法直接引用,如果没有查找到则返回 java.lang.IllegaAccessError 异常
3), 否则,继续继承关系从下往上依次对C 的各个父类进行第2步骤的查找和验证过程
4), 如果始终没有找到合适的方法,则抛出 AbstactMehtodError异常信息
虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的话就可能会影响到执行效率
为了提高性能, JVM采用了在类的方法区建立一个虚方法表(virtual method table) 来实现,使用索引来代替查找
每个类中都有一个虚方法表,表中存放着各个方法的实际入口
虚方法表在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成后,JVM会把类的方法也初始化完毕
方法返回地址 (方法正常退出或者异常退出的定义)
简介
存放调用该方法的pc 寄存器的值
一个方法的结束,有两种 1), 正常执行完成, 2), 出现未处理的异常,非正常退出
无论通过哪种方式退出, 在方法退出后都返回到该方法被调用的位置
方法正常退出时,调用者的pc寄存器的值作为返回地址,即调用该方法指令的下一条指令地址
而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
当一个方法开始执行后,只有两种方式可以退出这个方法
1), 执行引擎遇到任何一个方法返回字节码指令(return) ,会有返回值传递给上层的方法调用者,简称 正常完成出口
2), 在方法执行的过程中遇到了异常(Exception) ,并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索匹配的异常处理器,就会导致方法退出,简称 异常完成出口
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息,例如 对程序调优调试提供支持的信息
面试题
举例栈溢出的情况?(StackOverFlowError)
通过 -Xss=255k 设置栈大小
通过调整栈的大小,就能保证不出现栈溢出?
不能保证,调整栈大小理论上只能酱烧 stackOverFlowError 出现晚一些而不能让他彻底不出现
分配的栈内存越大越好?
栈内存越大对自己JVM栈来说来可以的,可以避免发生 stackoverflowerror 异常,但是对于 方法区来说并不是好的,其它内存结构就占用内存空间减少
垃圾回收是否涉及到虚拟机栈?
不会,因为栈只涉及入栈和出栈,出栈代表回收所以不涉及GC处理
方法中定义的局部变量表是否线程安全?
具体问题具体分析
变量如果在方法内部产生内部消亡的那么他就是线程安全的,如果他不是内部产生的,或者他的实例返回到方法体外边声明周期没有结束,那么他就是线程不安全的
什么是线程安全?
就是 如果只有一个线程可以访问,则必是线程安全的,如果有多个线程操作此数据,免责此数据是共享数据,会存在线程安全问题
方法区(Method Area)
位置(在哪里)
<java 虚拟机>说明: 尽管所有的方法区在逻辑上属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩, 但对于 HotSpot JVM 而言,方法区还有一个别名交 no-Heap (非堆),目的就是要与方法区分开,所以方法区看做是一块独立于java 堆的内存空间
概述
1), 方法区 与 java 堆一样,是各个线程共享的内存区域
2), 方法区在 JVM启动的时候创建完成,并且它的实际物理内存区域和 java 堆区一样都可以是不连续的
3), 方法区的大小,根堆空间大小一样,可以选择固定大小或者可扩展
4), 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出的话 JDK8之前报 OutOfMemoryError: PermGen space , JDK8之后报 OutOfMemoryError:Metaspace
5), 关闭JVM就会释放这个区域
HotSpot 方法区
在JDK7之前,习惯上把方法区称为永久代,JDK8开始把永久代改为元空间
在JDK8后的改变完全废除了永久代的概念,改用与 JRockit ,J9 一样在本地内存中实现的元空间(Metaspace) 来代替
元空间的本质和永久代类似, 都是对JVM 规范中方法区的实现,不过元空间与永久代最大的区别在于 元空间不在虚拟机设置的内存中,而是使用本地内存
永久代,元空间 二者不只是名字变了,内部结构也调整了
根据<Java 虚拟机规范> 的规定,如果方法区无法满足新的内存分配需求时, 将抛出 OutOfMemoryError:Metaspace 异常
设置方法区/元空间 内存大小
JDK8之后, 元空间可以使用惨 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来设置元空间大小
默认依赖于平台, windows 下 -XX:MetaspaceSize 是21M; -XX:MaxMetaspaceSize是-1 即没有限制
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存,如果元数据发生溢出,虚拟机一样会抛出OOM异常
-XX:MetaspaceSize 设置初始的元空间大小,对于一个64位的服务端JVM来说,默认的21M 这就是初始的高水位线,一旦触及这个水位线,就会 Full GC 卸载那些没用的类,然后这个水位将会重置,新的高水位线取决于GC后释放多少空间,如果释放的空间不足,那么再不超过 MaxMetaspaceSize 时,适当提高该值,如果释放空间过多,则适当降低该值
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收器的日志可以观察到 FGC 多次调用,为了避免频繁GC ,建议将 -XX:MetaspaceSize 设置为一个相对较高的值
如何解决这些oom?
1), 要解决oom 异常或 heap space 的异常,一般的手段是 首先通过内存映射分析工具(jvisualvm.exe...) 对dump 出来的堆转储快照进行分析, 重点是确认呢日村中的对象是否是必要的,也就是要分析清除到底出现了内存泄漏(memory)还是内存溢出 (overflow)
2), 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链,于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的, 掌握了泄漏对象的类型信息, 以及GC Roots 引用链信息 ,就可以比较准备的定位出泄漏代码的位置
3), 如果不存在内存在泄漏,换句话说 就是内存中的对象确定都还必须存活着, 那就应当检查虚拟机的堆参数,(-Xmx 与 Xms) ,与物理机内存对比看是否还可以增大,从代码上检查是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期间的内存消耗
方法区(Method Area) 存储一些什么?
它用于存储已被虚拟机加载的类型信息, 常量 ,静态变量, 即时编译器编译后的代码缓存等
为什么需要常量池?
一个java 源文件中的类,接口,编译后产生一个字节码文件,而java 中字节码需要数据支持,通常这些数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用
常量池里面有什么?
1),数量值
2),字符串值
3),类引用
4),字段引用
5),方法引用
常量池可以看做一张表, 虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型
运行时常量池
运行时常量池 是方法区的一部分
常量表是Class 文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池
JVM为每个已加载类型(类或接口) 都维护了一个常量池,池中池中的数据项像数组一样,是通过索引进行访问的
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获取的方法或者字段引用,此时不再是常量池中的符号地址了,这里替换为真实地址--具备动态性
运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号表要更加丰富一些
当创建类或接口的运行时常量池时,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,则JVM则会排除 OOM 异常
方法区演进细节
JDK1.6之前: 有永久代,静态变量存放在永久代
JDK1.7 有永久代, 但已经逐步去永久代,字符串常量池,静态变量移除,保存在堆上
JDK1.8 之后,无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池,静态变量仍在堆
永久代为什么要被元空间替换?
随着JDK8 到来,HotSpot 虚拟机再也见不到永久代了,但这并不意味着类的元数据信息也消息了,这些数据被移动到了一个与堆不相连的本地内存区域,这个区域叫做元空间(metaspace)
为永久代设置空间大小是很难确定的
对永久代进行调优非常困难
StringTable 为什么要调整?
JDK7将StringTable 放在了堆空间,因为永久代的回收效率非常低,在 FGC的时候才会被触发,而FGC是老年代或方法区空间不足才能被触发,这就导致了 StringTable 回收效率不高, 而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足,放在堆空间,能及时回收内存
方法区的垃圾收集
<java虚拟机规范> 对方法区的约束是非常轻松的,提过可以不要求虚拟机在方法中实现垃圾收集,事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(JDK11 的ZGC收集就不支持类卸载)
这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是部分区域的回收有时又确实是必要的
方法区的垃圾收集主要回收两部分内容: 常量池中废弃的常量和不再使用的类型
方法区内常量池主要存放的两大类常量 : 字面量和符号引用
1), 类和接口的全限定名
2), 字段的名称和描述符
3),方法的名称和描述符
HotSpot 虚拟机对常量池的回收策略是很明确的, 只要常量池中的常量没有被任何地方引用,就可以被回收
回收废弃常量与回收java 堆中的对象非常类似
判断一个类是否属于不在使用的类,条件非常苛刻,需要同时满足下面三个条件
1), 该类所有的实例都已经被回收,也就是java 堆中不存在该类以及其它任何派生类实例
2), 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi ,JSP 的重加载,否则通常很难达成
3), 该类对应的 java.lang.Class对象没有任何地方被引用,无法再任何地方通过反射访问该类方法
java 虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是 被允许 而不是和对象一样 ,没有引用就必然会被回收
在大量使用反射,动态代理,CGliB , 等字节码框架,动态生成JSP 以及 ISGi 这类频繁自定义类加载器场景中,通常都需要java 虚拟机具备类型卸载能力, 以保证不会对方法区造成过大内存压力
本地方法栈(Native Method Stacks)
定义
一个 Native Method 就是一个Java 调用非java代码的接口,一个 Native method 是这样一个java 接口,该方法的实现由非Java语言实现
作用
1), 与Java 环境外交互,有时java 需要与外面的环境交互,这是本丢方法存在的主要原因
2), 与操作系统交互,通过使用本地方法,我们得以使用 Java 实现 jre 的域底层系统交互,甚至JVM的一部分C写的
3), sun的解释器是C实现的,这使得它能像一些普通的C一样与外部交互
现状
目前该方法使用的越来越少了,除非是与硬件有关的应用
程序计数器(Program Counter Register)
概述
JVM 中的程序计数寄存器(Program Counter Register), Register 的名字源于CPU 的寄存器,寄存器存储的指令相关的现场信息,CPU 只有把数据装载到寄存器才能够运行
这里,并非广义上所指的物理寄存器,或许将其翻译为 PC计数器(或指令计数器)会更加贴切(也称为程序钩子) ,并非不容易引起一些不必要的误会,JVM 的
介绍
它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域
在JVM规范中,每个线程都有它自己程序计数器,是线程私有的,声明周期与线程的声明周期一致
任何时间一个线程都只有一个方法在执行,也就是所谓的,程序计数器会存储当前线程正在执行的Java 方法的JVM指令地址 或者 如果是在执行native 方法,则是未指定值(undefind)
它是程序控制流的指示器, 分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成
字节码解释器在工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
它是唯一一个在Java 虚拟机规范中没有规定任何 outofMemoryError 情况的区域
CPU时间片
CPU 时间片即 CPU 分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片
在宏观上,我们可以同时打开多个应用程序,每个程序并不停止,同时在运行
在微观上,由于只有一个CPU,一次只能处理程序要求的一部分,如果处理公平,一种方式就是引入时间片,每个程序轮流执行
常见问题
使用pc 寄存器存储字节码指令地址有什么用呢?
因为cpu 需要不停的切换各个线程,这时候切换回来之后,就要知道接着从哪开始继续执行
JVM 的字节码解释器就需要通过改变pc 寄存器的值来明确下一条应该执行什么样的字节码指令
pc 寄存器为什么会被设定为线程私有的?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个pc寄存器,这样一来各个线程便可以进行独立运算,从而不会出现相互干扰的情况
由于CPU 时间片限制,众多线程在并发执行的过程中,任何一个确定的时刻,一个处理或多个处理器中的一个内核,只会执行某个线程中的一条指令
这样必然导致经常中断或恢复,如何保证分毫无差呢? 每个线程在创建后,都会产生自己程序计数器和栈帧,程序计数器在各个线程之间互不影响
常见面试问题
堆
Java 7 与Java 8堆的区别
Java7之前的内存逻辑上区分为 新生代+老年代+永久区
Java 8之后的内存逻辑上区分为 新生代+老年代+元空间
如何设置堆内存大小
通常会将 -Xms 和 -Xmx 两个参数配置相同的值,其目的就是为了能够在Java 垃圾回收器机制清理完堆区以后不需要重新分割计算堆区大小,从而提高性能
默认情况下 初始内存大小 : 物理电脑内存大小的/64
默认情况下,最大内存大小:物理电脑内存大小/4
如何查看使用gc情况
jps 查看程序运行端口
jstat -gc 端口
-XX: +PrintGCDetails
Minor GC ,Major GC 与Full GC区别
JVM在进行GC时, 并非每次都对上面三个内存(新生代,老年代;方法区) 区域一起回收的,大部分时候的回收都是指新生代
针对 HotSpot 虚拟机的实现,它里面的GC按照回收区域又分为两大类: 一种是部分收集(Partial GC) 一种是整堆收集(Full GC)
部分收集(MajorGC/old GC): 指不是完整收集整个Java 堆的垃圾收集器,其中又分为新生代收集(Young GC/minor GC)、老年代收集(Major GC / old GC)、混合回收( Mixed GC) : 收集整个新生代以及部分老年代的垃圾收集
整堆收集(Full GC): 收集整个java堆和方法区的垃圾收集器
类的加载过程
JVM虚拟机自带的加载器
(从上至下依次加载)
启动类加载器(Bootstrap ClassLoader)
这个类加载使用C/C++语言实现,嵌套在JVM内部
它是用来加载java的核心内库(rt.jar,resource.jar或者sun.boot.class.path)路径下面的内容,提供JVM自身需要的类
并不继承与java.lang.ClassLoader没有父类加载器
加载扩展类加载器和应用程序加载器并指定他们的父类加载器
出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类
拓展类加载器(Extension ClassLoader)
java 语言编写,有Launcher的内部类
派生于ClassLoader类
父类加载器启动为启动类加载器
从java.ext.dirs系统属性所指定的目录中加载类库或从JDK的安装目录的jre/lib/ext子目录(拓展目录)下,如果用户创建的jar放在此目录,也会由拓展类加载器加载
应用程序加载器(Application ClassLoader)
java语言编写,有Launcher的内部类
派生于ClassLoader类
父类加载器启动为拓展类加载器
它负责加载环境classpath或系统属性,java.class.path指定路径下的类库
该类加载时程序中默认的类加载器,一般来说,java应用类都是由它来完成
通过ClassLoader#getSystemClassLoader()方法可以直接获取该类加载器
用户自定义类加载器(User Defined ClassLoader)
日常开发中,类加载器几乎由上述3种类加载器互相配合执行,必要时也可以自定义类加载器来定制类的加载方式
什么情况需要自定义类加载器
隔离加载类
修改类加载的方式
扩展加载源
防止源码泄露
双亲委派机制
定义
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器(Bootstrap ClassLoader)中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类
双亲委派模式优势
(向下委派)避免类的重复加载,保障所有类都被加载
(向下委派)保护程序安全,防止核心API被恶意篡改
沙箱安全机制
比如我定义了一个类名为String所在包为java.lang,因为这个类本来是属于jdk的,如果没有沙箱安全机制的话,这个类将会污染到我所有的String,但是由于沙箱安全机制,所以就委托顶层的bootstrap加载器查找这个类,如果没有的话就委托extsion,extsion没有就到aapclassloader,但是由于String就是jdk的源代码,所以在bootstrap那里就加载到了,先找到先使用,所以就使用bootstrap里面的String,后面的一概不能使用,这就保证了不被恶意代码污染
在JVM中表示两个class对象是否为同一个类存在的两个必要条件
类的完整类名必须一致,包括包名
加载这两个类的ClassLoader必须相同
类的主动使用和被动使用
类的主动使用
创建类的实例
创建某个类或接口的静态变量或者对该静态变量赋值
调用类的静态方法
反射
初始化一个类的子类
Java虚拟机启动时被标记为启动类的类
JDK1.7开始提供的动态语言支持
类的被动使用
除了主动使用外,其他都是被动使用,都不会导致类的初始化
JVM常用命令
jps
jstat
jmap
jhat
jstack
性能检测工具
图形化工具
jconsloe
VisualVM
远程机器配置JVM可视化图
通过jstatd启动RMI
配置JMX管理Tomcat
远程机器视图
JVM调优实例
常见调优策略
选择合适的垃圾回收器
调整内存大小
设置符合预期的停顿时间
调整大对象的标准
JVM调优实例
收藏
0 条评论
下一页