网站设计流行趋势,做网站只开发手机端可不可以,手机是使用wordpress,县区工会网站建设方案概叙
科普文#xff1a;一文搞懂jvm(一)jvm概叙-CSDN博客
科普文#xff1a;一文搞懂jvm原理(二)类加载器-CSDN博客 前面我们介绍了jvm#xff0c;jvm主要包括两个子系统和两个组件#xff1a; Class loader(类装载器) 子系统#xff0c;Execution engine(执行引擎) 子系…概叙
科普文一文搞懂jvm(一)jvm概叙-CSDN博客
科普文一文搞懂jvm原理(二)类加载器-CSDN博客 前面我们介绍了jvmjvm主要包括两个子系统和两个组件 Class loader(类装载器) 子系统Execution engine(执行引擎) 子系统Runtime data area (运行时数据区域)组件 Native interface(本地接口)组件。 这里我们主要讲解Execution engine(执行引擎)讲到这个就和jvm优化相关。 图一jvm的运行时区、执行引擎、本地方法接口和库 图二这个是前面文中多次出现的jvm详细图 执行引擎是Java虚拟机核心的组成部分之一属于JVM的下层。 其中包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念这两种机器都有代码执行能力其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的而虚拟机的执行引擎则是由软件自行实现的因此可以不受物理条件制约地定制指令集与执行引擎的结构体系能够执行那些不被硬件直接支持的指令集格式。 JVM的主要任务是负责装载字节码到其内部但字节码并不能够直接运行在操作系统之上因为字节码指令并非等价于本地机器指令它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表以及其他辅助信息。 那么如果想要让一个Java程序运行起来执行引擎Execution Engine的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。 简单来说JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。 为什么说Java是半编译半解释性语言 JDK 1.0 时代将 Java 语言定位为“解释执行”还是比较准确的再后来 Java 也发展出可以直接生成本地代码的编译器。 现在 JVM 在执行 Java 代码的时候通常都会将解释执行和编译执行二者结合起来进行。 HotSpot采用的是解释执行与即时编译器并存的架构。在Java虚拟机运行时解释器和即时编译器能互相协作尽量选择最适合的方式来权衡编译本地代码的时间和解释执行代码的时间。也可以使用参数设置只使用解释器或者编译器启动。 Java 代码编译和执行过程 大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前都需要经过下图中的各个步骤
橙色部分是生成字节码文件的过程和JVM无关蓝色和绿色才是JVM需要考虑的过程 Java 代码编译是由 Java 源码编译器来完成流程图如下所示 Java 字节码的执行是由 JVM 执行引擎来完成流程图如下所示 jvm执行引擎Execution engine JVM中的执行引擎是JVM的重要组成部分之一主要负责将字节码指令翻译成机器码指令。
执行引擎的组成 执行引擎由三个组件组成
解释器Interpreter就是运行时的“翻译者”将字节码解释为机器指令。及时编译器JITJust In Time Compiler但是某些频繁执行的热点代码依然采用解释执行的话会导致程序执行很慢JIT编译器则是负责将热点代码编译分层优化成本地机器码。垃圾回收器Garbager Collection负责无用对象的销毁释放内存空间。 1.解释器Interpreter 解释器就是一个运行时的“翻译者”将字节码指令翻译为对应平台的本地机器指令由CPU执行当一条指令执行完成后再根据PC寄存器中记录的下一条指令执行解释操作。 JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性因此避免采用静态编译的方式直接生成本地机器指令从而诞生了解释器在Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。 当一条字节码指令被解释执行完成后接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。 在 Java 的发展历史里一共有两套解释执行器即古老的字节码解释器、现在普遍使用的模板解释器。 字节码解释器在执行时通过纯软件代码模拟字节码的执行效率非常低下。而模版解释器将每一条字节码和一个模板函数相关联模板函数中直接产生这条字节码执行时的机器码从而很大程度上提高了解释器的性能。在 HotSpot VM 中解释器主要由 Interpreter 模块和 Code 模块组成。
Interpreter 模块实现了解释器的核心功能Code 模块 用于管理 HotSpot VM 在运行时生成的本地机器指令。 由于解释器在设计和实现上非常简单因此除了 Java 语言之外还有许多高级语言同样也是基于解释器执行的比如 python 、Perl 、Ruby 等。但是在今天基于解释器执行已经沦落为低效的代名词并且时常被一些 C /C 程序员所调侃。 为了解决这个问题 JVM 平台支持一种叫做即时编译的技术。即时编译的目的是避免函数被解释执行而是将整个函数体编译成为机器码每次函数执行时只执行编译后的机器码即可这种方式可以时执行效率大幅度提升。 不过无论如何基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。 2.及时编译器JITJust In Time Compiler 当执行某些频繁被调用的代码比如for循环中的代码时如果按照解释执行效率非常低这种被频繁调用的代码成为热点代码。为了提高热点代码的执行效率在运行时JIT编译器则会将这些代码编译成与本地平台有关的机器码并进行各种层次的优化。 及时编译器Just In Time Compiler就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。 由于解释器在设计和实现上非常简单因此除了Java语言之外还有许多高级语言同样也是基于解释器执行的比如Python、 Perl、Ruby等。但是在今天基于解释器执行已经沦落为低效的代名词并且时常被一些C/C 程序员所调侃。 JIT的构成组件包括
中间代码生成器——生成中间代码代码优化器——优化中间代码目标代码生成器——生成机器码或本地代码分析器——负责查找热点代码 HotSpot VM 是目前市面上高性能虚拟机的代表作之一。它采用解释器与及时编辑器并行的结构。在 Java 虚拟机运行时解释器和即时编译器能够相互协作各自取长补短尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。 在今天 Java 程序的运行性能早已脱胎换骨已经达到了可以和 C/C 程序一较高下的地步。 有些开发人员会感觉到诧异既然 HotSpot VM 中已经内置了 JIT 编译器了那么为什么还使用解释器来“拖累”程序的执行性能呢 比如 JRockit VM 内部就不包含解释器字节码全部都依靠即时编译器编译后执行。 首先明确当程序启动后解释器可以马上发挥作用省去编译的时间立即执行。编译器要想发挥作用把代码编译成本地代码需要一定的执行时间。但编译为本地代码后执行效率高。所以尽管 JRockit VM中程序的执行性能会非常的高效但程序在启动时必然会花费更长的时间来进行编译。对于服务器来说启动时间并非时关注重点但对于那些看中启动时间的应用场景而言或许就需要采用解释器与及时编译器并存的架构来换取一个平衡点。在此模式下当 Java 虚拟机启动时解释器可以首先发挥作用而不必等待即时编译器全部编译完成后在执行这样可以省去许多不必要的编译时间。随着时间的推移编译器发挥作用把越来越多的代码编译成本地代码获得更高的执行效率。 同时解释执行在编译器进行激进优化不成立的时候作为编译器的“逃生门”。
概念解释 Java 语言的“编译期”其实是一段 “不确定”的操作过程因为他可能是指一个前端编译器其实叫 “编译器的前端” 更准确一些把 .java 文件转变成 .class 文件的过程 也可能是指虚拟机的后端运行期编译器 JIT 编译器 Just In Time Compiler 把字节码转变成机器码的一个过程。 还可能是指使用静态提前编译器 AOT 编译器 Ahead Of Time Compiler ) 直接把 .java 文件编译成本地机器代码的过程。
JIT及时编译器分类 在Hotspot虚拟机中内置了两种JIT编译器 C1编译器主要关注点在于局部性能优化适用于执行时间短或对启动性能有要求的程序如GUI应用C1编译器也被称为Client Compiler。 C2编译器为长期运行的服务端应用程序做性能优化的编译器适用于执行时间较长或对峰值性能有要求的程序C2编译器也被称为Server Compiler。
Graal编译器用Java实现的JIT编译器JDK10中引入具体信息参考https://openjdk.org/jeps/317JDK17中移除移除原因参考https://openjdk.org/jeps/410 JIT的编译器还分为了两种分别是C1和C2在HotSpot VM中内嵌有两个JIT编译器分别为Client Compiler和Server Compiler。但大多数情况下我们简称为C1编译器 和 C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器。 client指定Java虚拟机运行在Client模式下并使用C1编译器C1编译器会对字节码进行简单和可靠的优化耗时短。以达到更快的编译速度。 server指定Java虚拟机运行在server模式下并使用C2编译器C2进行耗时较长的优化以及激进优化。但优化的代码执行效率更高。 在不同的编译器上有不同的优化策略C1编译器上主要有方法内联去虚拟化、冗余消除 方法内联将引用的函数代码编译到引用点处这样可以减少栈帧的生成减少参数传递以及跳转过程 去虚拟化对唯一的实现樊进行内联 冗余消除在运行期间把一些不会执行的代码折叠掉 C2的优化主要是在全局层面逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化
标量替换用标量值代替聚合对象的属性值栈上分配对于未逃逸的对象分配对象在栈而不是堆同步消除清除同步操作通常指synchronized 在Java7版本之后一旦开发人员在程序中显式指定命令“-server时默认将会开启分层编译策略由C1编译器和C2编译器相互协作共同来执行编译任务。 总的来说C2编译器启动时长比C1慢系统稳定执行以后C2编译器执行速度远快于C1编译器。
分层编译 在Java7引入了分层编译这种方式综合了C1的启动优势和C2的峰值性能优势。分层编译将JVM的执行状态分为5个层次 第0层程序解释执行默认开启性能监控Profiling如果不开启可触发第二层编译 第1层C1编译将字节码编译成本地代码进行简单可靠的优化不开启Profiling 第2层C1编译开启Profiling仅执行带方法调用次数和循环回边执行次数Profiling的C1编译 第3层C1编译执行所有带Profiling的C1编译 第4层C2编译将字节码编译为本地代码但会启用一下编译耗时较长的优化甚至会根据性能监控信息进行一些不可靠的激进优化。 Java8中默认开启分层编译-XX:-TieredCompilation 参数可关闭分层编译只使用C2编译如果只使用C1编译可设置参数 -XX:TieredStopAtLevel1。 -Xint参数可设置为强制解释器模式运行-Xcomp可设置为强制运行JIT编译模式。 热点探测技术 关于编译器可大致分为两种
前端编译器把 .java 文件转变成 .class 文件后端编译器把 .class 文件转变为 机器指令 是否需要启动即时编译器将字节码转换为机器指令则需要根据代码的调用频率而定。一个被多次调用的方法或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”即时编译器在运行时会针对那些被频繁调用的热点代码做出深度优化将其直接编译为本地的机器指令以此来提升程序的性能。由于这种编译方式发生在方法的执行过程中因此被称之为栈上替换或简称为OSROn Stack Replacement编译。 一个方法究竟要被调用多少次或者一个循环体究竟需要执行多少次循环才可以达到这个标准必然需要一个明确的阈值JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。 目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测HotSpot 将会为每一个方法都建立2个不同类型的计数器分别为方法调用计数器和回边计数器。
方法调用计数器用于统计方法的调用次数回边计数器则用于统计循环体执行的循环次数
方法调用计数器 这个计数器就用于统计方法被调用的次数它的默认阈值在Client模式下是1500次在Server模式下是10000次超过这个阈值就会触发即时编译。这个阈值可以通过虚拟机参数 -XX:CompileThreshold 来设定。 当某个方法调用次数达到阈值就会触发JIT编译优化jinfo -flag CompileThreshold pid 或者 java -XX:PrintFlagsFinal -version 命令可查看方法调用次数阈值 当一个方法被调用时会先检查该方法是否存在被即时编译器编译过的版本如果存在则优先使用编译后的本地代码来执行。 如果不存在已被编译过的版本则将此方法的调用计数器值加1然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值。如果已超过阈值那么将会向即时编译器提交一个该方法的代码编译请求否则就通过解释器执行。
回边计数器 它的作用是统计一个方法中循环体代码执行的次数在字节码中遇到控制流向后跳转的指令称为“回边”Back Edge。 跟方法调用计数器搭配使用如何两者相加总和超过计数器的阀值那么就会除法即时编译器。显然建立回边计数器统计的目的就是为了触发栈上替换编译。 用于统计方法体中循环体代码执行次数字节码中遇到控制流向后跳转的指令称为“回边”Back Edge用于计算是否为热点代码的阈值。 计算公式如下 回边计数器阈值 方法调用计数器阈值CompileThreshold*OSR比率OnStackReplacePercentage-解释器监控比例InterpreterProfilePercentage/ 100
java -XX:PrintFlagsFinal -version 命令可查看相关参数 根据图中显示的各参数的默认值可以计算出回边计数器阈值为1000 * (140 - 33) 10700
热点衰减 如果不做任何设置方法调用计数器统计的并不是方法被调用的绝对次数而是一个相对的执行频率可理解为一段时间之内方法被调用的次数。当超过一定的时间限度如果方法的调用次数仍然不足以让它提交给即时编译器编译那这个方法的调用计数器就会被减少一半这个过程称为方法调用计数器热度的衰减而这段时间就称为此方法统计的半衰周期。可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间单位是秒。 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减让方法计数器统计方法调用的绝对次数。这样只要系统运行时间足够长绝大部分方法都会被编译成本地代码。 如何选择编译器和解释器 当然是否需要启动 JIT 编译器将字节码直接译为对应平台的本地机器指令则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码也被称之为 “热点代码” JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化将其直接编译为对应平台的本地机器指令以此提升 Java 程序的性能。 缺省情况下 HotSpot VM 是采用解释器与即时编译器并存的架构当然开发人员可以根据具体的应用场景通过命令显示地为 Java 虚拟机指定在运行时到底是完全采用解释器执行还是完全采用即时编译器执行。如下所示
-Xint : 完全采用解释器模式执行程序-Xcomp : 完全采用编译器模式执行程序。如果即时编译出现问题解释器会介入执行。-Xmixed :采用解释器即时编译器的混合模式共同执行程序。 静态提前编译器 JDK9 引入了静态提前编译器Ahead of Time Compiler。Java 9引入了实验性AOT编译工具AOTC。它借助了Graal编译器将所输入的Java类文件转换为机器码并存放至生成的动态共享库之中。 静态提前编译器:是直接把.java文件编译成机器指令。大致过程.java - .class - (使用jaotc) - .so。 所谓AOT编译是与即时编译相对立的一个概念。即时编译指的是在程序的运行过程中将字节码转换为可在硬件上直接运行的机器码并部署至托管环境中的过程。而AOT编译指的则是在程序运行之前便将字节码转换为机器码的过程。
优点 Java虚拟机加载已经预编译成二进制库可以直接执行。不必等待及时编译器的预热减少Java应用给人带来“第一次运行慢” 的不良体验
缺点 破坏了 java “ 一次编译到处运行”必须为每个不同的硬件OS编译对应的发行包 降低了Java链接过程的动态性加载的代码在编译器就必须全部已知 还需要继续优化中最初只支持Linux X64 java base Graal编译器 自JDK10起HotSpot又加入了一个全新的及时编译器Graal编译器编译效果短短几年时间就追评了G2编译器未来可期。
特点 高效能运行 Java 使用 GraalVM 执行 Java 程序可以变得更快 多语言并行可以在 Java 里面同时使用多种语言像是 JavaScript等 快速启动直接把 Java 应用编译成机器码执行起来体积更小、启动速度更快 编译优化技术
1. 方法内联 方法内联的优化行为是将目标方法的代码复制到发起调用的方法中避免真实方法调用。 private int add(int a, int b, int c, int d) {return addInt1(a, b) addInt2(c, d);}
private int addInt1(int a, int b) {return a b;}
private int addInt2(int a, int b) {return a b;} 例如上面的代码如果被检测为热点代码则会被优化为以下代码 private int add(int a, int b, int c, int d) {return a b c d;} 热点方法不一定都会被内联优化只有当方法体大小小于参数 -XX:FreqInlineSize 值默认325字节才会进行内联非热点方法当方法体小于参数 -XX:MaxInlineSize 值默认35字节才会进行内联。
相关性能调优 减小热点方法检测阈值增加内联方法体阈值缺点则是会增加内存占用尽量避免在一个方法体内写入大量代码习惯使用小方法体尽量使用final private static 关键字修饰方法代码优化时因为继承需要额外的类型检查。
2. 锁消除 当方法中的局部方法中创建的对象只能被当前线程访问时不存在锁竞争JIT编译会对这个对象的方法进行锁消除。 参数 -XX:EliminateLocks 可以开启锁消除默认开启-XX:-EliminateLocks 则是关闭锁消除
3. 锁粗化 如果检测到同一个对象执行了连续的加锁和解锁操作则会将这一系列操作合并成一个更大的锁从而提升程序运行效率。
4. 逃逸分析 JIT编译器会对热点代码中的对象进行逃逸分析分析该对象动态作用域当被传递到其他方法中称为方法逃逸当能被外部方法所引用则为线程逃逸。 不逃逸到方法逃逸再到线程逃逸逃逸程度由低到高。 逃逸分析可以通过参数 -XX:DoEscapeAnalysis开启jdk1.8默认开启或 -XX:-DoEscapeAnalysis 关闭。 关闭逃逸分析会导致对象分配到堆中频繁触发垃圾回收导致代码运行慢。
5. 标量替换 当确定对象不会逃逸出线程之外该对象则会被分配到栈上对象分配到栈需要进行成员变量拆分这种优化技术叫做标量替换。标量替换需要开启逃逸分析。 标量替换可以通过参数 -XX:EliminateAllocations开启jdk1.8默认开启或-XX:EliminateAllocations关闭 3.垃圾回收器Garbager Collection 程序计数器、虚拟机栈、本地方法栈随线程而生也随线程而灭栈帧随着方法的开始而入栈随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性在这几个区域内不需要过多考虑回收的问题因为方法结束或者线程结束时内存自然就跟随着回收了。 垃圾收集器(Garbage Collection,简称GC), GC主要用于Java堆的管理。Java 中的堆是 JVM 所管理的最大的一块内存空间主要用于存放各种类的实例对象。 在系统运行期间会产生大量的对象实例对于一些对象实例用完后已经没有引用指向(对象已无法访问)这些对象属于内存垃圾为了能够回收没用的对象就诞生了垃圾回收器。 既然JVM种有垃圾收集器为什么我们还要去了解垃圾收集和内存分配呢 答案很简单当需要排查各种内存溢出内存泄露问题时当垃圾收集成为系统达到更高并发量的瓶颈时我们必须对这些进行监控和调节。 关于垃圾回收器在讲完“jvm运行时区”后再单独详细描述。
判断对象是否存活
引用计数法 实现原理为每个对象头维护一个独立的 counter 计数器当对象被引用时加1取消引用时减1。当计数器为 0 时对象就是不可能再被使用的。 引用计数法虽然会占用一些额外的内存空间来计数但它的实现原理简单效率也很高Java虚拟没有选用计数算法来管理内存主要原因是引用计数算法存在对象之间循环引用的问题。
下图就是循环引用的例子
可达性分析法 Java使用的是可达性分析算法来判断对象是否可以被回收。可达性分析将对象分为两类垃圾回收的根对象GC Roots和普通对象对象与对象之间存在引用关系。 实现原理下图中A到B再到C和D形成了一个引用链可达性分析算法指的是如果从某个对象到GC Roots对象是可达的对象就不可被回收。如果某个对象到GC Roots间没有任何引用链相连那么就可以判定对象是不再使用。
固定可作为GC Roots对象包括以下几种
虚拟机栈栈帧中的本地变量表中引用对象比如方法中使用到的参数、局部变量、临时变量等。方法区中静态属性、常量引用的对象。本地方法(Native 方法)栈引用的对象。Java虚拟机内部引用的对象如基本数据类型对应的Clas对象。所有被同步锁(Synhronized关键字)持有的对象。当前活动线程正在运行的线程对象。
等等… GC Roots 并不包括堆中对象所引用的对象这样就不会有循环引用的问题。
常见的几种对象引用 可达性算法中描述的对象引用一般指的是强引用即是GCRoot对象对普通对象有引用关系只要这层关系存在普通对象就不会被回收。除了强引用之外Java中还设计了几种其他引用方式
强引用类似 “Object obj new Object()” 这类的引用就是强引用只要强引用存在垃圾收集器永远不会回收被引用的对象。软引用软引用相对于强引用是一种比较弱的引用关系如果一个对象只有软引用关联到它当程序内存不足时就会将软引用中的数据进行回收。弱引用弱引用的强度比软引用更弱一些弱引用包含的对象在垃圾回收时不管内存够不够都会直接被回收。虚引用最弱的一种引用关系虚引用唯一的用途是当对象被垃圾回收器回收时可以接收到对应的通知。常规开发中是不会使用的。
垃圾收集算法 垃圾回收的核心算法释放不再存活对象的内存使得程序能再次利用这部分空间。下图为垃圾收集的四种算法