当前位置: 首页 > news >正文

网站维护说明重庆网上找工作哪个网站好

网站维护说明,重庆网上找工作哪个网站好,网站建设方案书备案设计图,广州最新政策文章目录 为什么要学 JVM一、JVM 整体布局二、Class 文件规范三、类加载模块四、执行引擎五、GC 垃圾回收1 、JVM内存布局2 、 JVM 有哪些主要的垃圾回收器#xff1f;3 、分代垃圾回收工作机制 六、对 JVM 进行调优的基础思路七、 GC 情况分析实例 JVM调优指南 -- 楼兰 ​ JV… 文章目录 为什么要学 JVM一、JVM 整体布局二、Class 文件规范三、类加载模块四、执行引擎五、GC 垃圾回收1 、JVM内存布局2 、 JVM 有哪些主要的垃圾回收器3 、分代垃圾回收工作机制 六、对 JVM 进行调优的基础思路七、 GC 情况分析实例 JVM调优指南 -- 楼兰 ​ JVM 虚拟机这是一个Java 程序员一直以来熟悉但是又陌生的神秘东东。他是夹在 Java 代码与操作系统之间的一层神秘空间。这一次楼兰就来带大家以可视化的方式来彻底剖析一下这个神秘的 JVM 虚拟机。 为什么要学 JVM ​ 这可能是很多接触 Java 多年的程序员们需要转变的第一个思维。而只有你先把思维转变过来你才会有兴趣去接触 JVM 中那些晦涩难懂的内容。所以我觉得是有必要花一些功夫来带你建立起这样的信念的。 ​ 关于 JVM 是什么这个就不用楼兰来多说了。他是整个 Java 技术体系的奠基石。 ​ 对于操作系统Java 提供了不同的 JRE。这样我们写的 Java 代码可以通过这些不同的 JRE在不同的操作系统上运行而不用关心不同操作系统之间的区别。这就是所谓的一次编写多次执行。而在语言层面时至今日JVM 也远不是 Java 语言的专属。他所执行的是 Java 代码编译后生成的class文件。而其他语言只要满足 Java 提出的class文件规范就可以在 JVM 上执行。 ​ Java 发展到现在已经远远超出了 Java 语言的范围成了一个庞大的技术体系。正是基于强大的 JVM 虚拟机我们甚至可以在上层使用多种语言混合进行开发。例如在大数据领域常见的 SparkFlink 等组件都提供了 Java、 Scala 等不同语言的客户端你完全可以在一个项目中混合使用 Java 和 Scala 进行开发。而如果你愿意甚至可以再加入 Jython从而让你的Java 应用可以引入Python的强大类库。 ​ 所以关于哪种语言是最强大的语言或许还存在”PHP天下第一“的争论。但是关于最强大的语言虚拟机则没人可以和 JVM叫板。 ​ 但是很可惜在很多程序员的眼里往往会将 SpringBootSpringCloud 这些应用层面的框架看成 Java 的主体。而对于这些底层的基石却看得不是很重。这不能不说是一种可惜。在学习 Java 的初期注重应用就好像学武先学套路招数一样这没有什么问题。但是如果你学习 Java 多年就好像学武学到了一定的境界再要想突破瓶颈那就一定要开始尝试回头来修修内功了。人生至少要有这么一次跟着楼兰来 JVM 中逛一逛。 首先JVM 能够解释很多上层语言中的困惑 ​ 如果你做过一些 Java 面试题那么你一定接触过很多让人莫名其妙的折磨人的变态面试题。比如下面这个简单的例子 public class ByteCodeInterView {//包装类对象的缓存问题Testpublic void typeTest(){Integer i1 10;Integer i2 10;System.out.println(i1 i2);//trueInteger i3 128;Integer i4 128;System.out.println(i3 i4);//falseBoolean b1 true;Boolean b2 true;System.out.println(b1 b2);//trueDouble d11.00;Double d21.00;System.out.println(d1d2);//false}Testpublic int mathTest(){int k 1 ;k k;return k;} } ​ 运行的结果已经写在后面的注释里了。如果之前没有接触过这样的题目那么 99%的程序员都会是一脸懵逼的。这些莫名其妙的true和false到底是怎么崩出来的相信这种非人类的代码你也应该或多或少接触过。那么要怎么才能彻底把握这些代码呢死记硬背见招拆招那肯定是不够的。这就需要至少深入到 JVM 层面去了解这些莫名其妙的代码到底是怎么运行的。后面章节我会带你来解密。 当然JVM 只是操作系统的一层代理。 JVM 的本质只是将程序员写出来的class文件转换为操作系统对应的一些指令。所以有些与操作系统联系紧密的机制甚至还需要到操作系统底层去了解。楼兰之前带大家去分析过 NIO 的系统调用有兴趣的可以去关注一下。 其次学习 JVM 是进行调优的基础 ​ 谈到 JVM 调优这不光是面试的重灾区更是所有程序员都应该送给自己的一份礼物。毕竟这首先就关系到你写的各种各样的 Java 代码到底是怎么在服务器上运行的。这是对我们自己的一份交代。另外当然还涉及到一些线上项目出问题了你怎么快速去找到并解决这些问题。 ​ 这方面需要很多的实战经验但是毕竟不是每个程序员都有机会去天天接触那些线上的服务器的。那我们到底应该怎么去培养自己的实战经验呢楼兰给大家的最好建议就是去向开源的服务学习。比如RocketMQ 中执行 NameServer 的核心JVM 参数是这样的 ​ 这里是针对不同的 JDK 版本定制了不同的参数。主要是用 JDK8 作为一个分水岭。 JDK8之前是上面那一段。 JDK9 以后是下面这一段。这些眼花缭乱的参数是怎么定制出来的你不可能真的依靠把这些莫名其妙的配置参数背得滚瓜烂熟再来调优吧。这就需要你对 JVM 至少要建立一个足够精细的理论模型。 ​ 接下来RocketMQ 如果真的线上出现了OOM 内存溢出这样的问题怎么快速去分析并定位呢这当然也需要从这些优化配置入手。比如在这个 JDK8的执行指令当中RocketMQ 就打印了 GC 垃圾回收的日志。这些日志显然是所有 Java 应用都可以打印出来的。接下来要怎么对这些日志进行分析呢这也需要配合 JVM 底层模型去进行定位。 ​ 当然这里面的东西是很复杂的很多底层的东西全是理论看不到也摸不着。并且很难实操。无法验证的技术就会很容易让人觉得枯燥迷茫。那么接下来就跟楼兰一起用可视化的方式来分析一波 JVM 吧。 一、JVM 整体布局 ​ 咱们不说废话先总后分。先整体来预览一下对于 JVM楼兰会带你分析哪些东西。然后再来慢慢拆解。 ​ 我们写的 Java 文件都要先通过编译器编译成.class文件然后才能进入 JVM 执行。而在 Java 发展的过程当中其实也有有很多的具体 JVM 实现产品。比如我们现在用 Java 指令就能看到现在用的是HotSpot 的 JVM。 # java -version java version 1.8.0_391 Java(TM) SE Runtime Environment (build 1.8.0_391-b13) Java HotSpot(TM) 64-Bit Server VM (build 25.391-b13, mixed mode)​ 这也是 Oracle 官方现在支持的 JVM 。而在此之外其实还有很多其他的 JVM 虚拟机。比如Java 最早的Classic 虚拟机还有 JRockit虚拟机以及可以直接将 Java 程序编译成本地指令的 GraalVM 虚拟机。我们接下来还是以HotSpot 虚拟机来进行分析。 二、Class 文件规范 ​ 实际上我们需要了解的是Java 官方实际上只定义了JVM的一种执行规范也就是class文件的组织规范。理论上只要你能够写出一个符合标准的class文件就可以丢到 JVM 中执行。至于这个class文件是怎么来的JVM 虚拟机是不管的。这也是 JVM 支持多语言的基础。 ​ 这个规范到底是什么样子呢如果你足够硬核当然可以直接去看 Oracle 的官方文档。JDK8 的文档地址https://docs.oracle.com/javase/specs/jvms/se8/html/index.html 。不过研究这么复杂的文档显然不是我们的取死之道。接下来我们就从class文件入手来抽取一些对我们比较有用的规范信息。 ​ 首先我们要知道class文件本质是一个二进制文件虽然不能直接用文本的方式阅读但是我们是可以用一些文本工具打开看看的。比如我们可以用 UltraEdit 工具打开一个class文件看到的内容部分是这样的 ​ 中间这一部分就是他的二进制内容。当然这是十六进制的表达。空格隔开的部分代表了 8 个bit而每一位代表的是 4 个 bit字节也就是一个十六进制的数字。例如 第一个字母 C 就表示十六进制的 12二进制是 1100。而所有的class文件都必须以十六进制的 CAFEBABE 开头这就是 JVM 规范的一部分。这也解释了 Java 这个词的由来到底是一种咖啡还是爪哇岛。 ​ 后面的部分就比较复杂了没法直接看。这时我们就需要用一些工具来看了。这样的工具很多。 JDK 自己就提供了一个 javap 指令可以直接来看一些class文件。例如可以用 javap -v ByteCodeInterView.class 查看到这个class文件的详细信息。 ​ 当然这样还是不够直观。我们可以在 IDEA 里添加一个 ByteCodeView 插件来更直观的查看一个 ClassFile 的内容。看到的大概内容是这样的 插件安装以及使用这里就不多说了相信对各位都是小菜一碟。 ​ 可以看到一个class文件的大致组成部分。然后再结合官方的文档或许能够让你开始对class文件有一个大致的感觉。 、 ​ 例如前面u4表示四个字节是magic魔数而这个魔数就是不讲道理的 CAFEBABE 。 ​ 而后面的两个u2表示两个字节的版本号。例如我们用 JDK8 看我们之前的class文件minor_version就是 00 00major_version就是 00 34。换成二进制就是 52这就是 JVM 给 JDK8 分配的版本号。这两个版本号就表示当前这个class文件只能用 JDK8 以前的 JDK 版本执行。你想用 JDK9 就无法执行。 ​ 接下来如果你感兴趣可以结合官方文档更详细的去解析这个class文件。其中常量池是最复杂的部分包含了表示这个class文件所需要的几乎所有常量。比如接口名字方法名字等等。而后面的几个部分比如方法接口等都是引用常量池中的各种变量。 ​ 而这其中我们重点关注的是方法也就是class文件是如何记录我们写的这些关键代码的。例如我们之前写的typeTest这个方法在class文件中就是这样记录的 、 ​ 这些东西你看不懂没关系至少现在你可以知道你写的代码在 Class 文件当中是怎么记录的了。另外如果你还想更仔细一点的分辨你的每一样代码都对应哪些指令那么在这个工具中还提供了一个LineNumberTable会告诉你这些指令与代码的对应关系。 起始 PC 就是这些指令的字节码指令的行数行好则对应 Java 代码中的行数。 实际上Java 程序在遇到异常时给出的堆栈信息就是通过这些数据来反馈报错行数的。 ​ 字节码中像 bipush 这些就对应一个 JVM 的字节码指令。 JVM 中的字节码指令是用一个字节来表示操作再用后面若干个字节表示操作的参数。参数个数根据不同的指令确定。 ​ 也就是说JVM 中的指令最多也不超过 255 个。这些指令相比于庞大的操作系统来说已经是非常小的了。另外其中还有很多差不多的。 比如aload_1aload_2 这些明显就是同一类的指令。这些指令在官方文档中都有详细的记录。 ​ 要了解这些指令的作用就不得不先了解一下 JVM 中两个重要的数据结构局部变量表和操作数栈。 ​ 在 JVM 虚拟机中会为每个线程构建一个线程私有的内存区域。其中包含的最重要的数据就是程序计数器和虚拟机栈。其中程序计数器主要是记录各个指令的执行进度用于在 CPU 进行切换时可以还原计算结果。虚拟机栈中则包含了这个线程运行所需要的重要数据。 ​ 虚拟机栈是一个先进后出的栈结构其中会为线程中每一个方法构建一个栈帧。而栈帧先进后出的特性也就对应了我们程序中每个方法的执行顺序。每个栈帧中包含四个部分局部变量表操作数栈动态链接库、返回地址。 操作数栈是一个先进后出的栈结构主要负责计算。局部变量表可以认为是一个数组结构主要负责存储计算过程中的变量。动态链接库主要存储一些指向运行时常量池的方法引用通过动态链接库将方法的符号引用转换成为调用方法的直接引用。返回地址存放调用当前方法的指令地址。如果方法正常退出这个返回地址就记录下一条指令的地址。如果是抛出异常退出返回地址就会通过异常表来确定。 ​ 其中最为重要的就是操作数栈和局部变量表了。例如对于初学者最头疼的操作下面的 mathTest 方法 public int mathTest(){int k 1 ;k k;return k;}我们都知道k的返回结果是 1但是自增操作到底有没有执行呢就可以按照指令这样进行解释 0 iconst_1 //往操作数栈中压入一个常量1 1 istore_1 // 将 int 类型值从操作数栈中移出到局部变量表1 位置 2 iload_1 // 从局部变量表1 位置装载int 类型的值到操作数栈中 3 iinc 1 by 1 // 将局部变量表 1 位置的数字增加 1 6 istore_1 // 将int类型值从操作数栈中移出到局部变量表1 位置 7 iload_1 // 从局部变量表1 位置装载int 类型的值到操作数栈中 8 ireturn // 返回 int 类型的值这个过程中k是在局部变量表中对数字进行了自增此时栈中还是 1。接下来执行操作就对应一个istore指令从栈中将数字装载到局部变量表中。局部变量表中的k的值(对应索引 1 位置)就还是还原成了 1。 ​ 那么接下来你是不是可以自行理解一下 kk是怎么执行的呢 另外这里补充一个在互联网大厂的高级职位面试过程中被问到过的细节问题 如何确定一个方法需要多大的操作数栈和局部变量 ​ 有些面试时是会给你一个具体的方法让你自己一下计算过程中需要几个操作数栈和几个局部变量。但是在工作中其实class文件当中就记录了所需要的操作数栈深度和局部变量表的槽位数。例如对于 mathTest方法所需的资源在工具中的纪录是这样的 ​ 这里会有一个小问题如果你自己推演过刚才的计算过程可以看到局部变量表中明明只用到了索引为 1 的一个位置而已为什么局部变量表的最大槽数是 2 呢 ​ 这是因为对于非静态方法JVM 默认都会在局部变量表的 0 号索引位置放入this变量指向对象自身。所以我们可以在代码中用this访问自己的属性。 ​ 有了这个基础之后再来看看之前的typeTest方法。其中我们只来解析最容易让人困惑的这几行代码。 Integer i1 10;Integer i2 10;System.out.println(i1 i2);//trueInteger i3 128;Integer i4 128;System.out.println(i3 i4);//false ​ 首先我们可以从LineNumberTable 中获取到这几行代码对应的字节码指令 ​ 以前面三行为例三行代码对应的 PC 指令就是从 0 到 12 号这几条指令。把指令摘抄下来是这样的 0 bipush 102 invokestatic #2 java/lang/Integer.valueOf : (I)Ljava/lang/Integer;5 astore_16 bipush 108 invokestatic #2 java/lang/Integer.valueOf : (I)Ljava/lang/Integer;11 astore_212 getstatic #3 java/lang/System.out : Ljava/io/PrintStream;​ 可以看到在执行astore指令往局部变量表中设置值之前都调用了一次Integer.valueOf方法。 ​ 而在这个方法中对于[-128,127]范围内常用的数字实际上是构建了缓存的。每次都从缓存中获取一个相同的值他们的内存地址当然就是相等的了。这些童年梦魇是不是在这个过程中找到了终极答案 实际上你甚至可以使用反射来修改这个内部的 IntegerCache 缓存从而让 Integer 的值发生紊乱。你有试过这样的骚操作吗 ​ 聊到了这些 Java 字节指令那最后再问一个让人没什么头脑的高端面试题作为部分总结吧。 面试题Java 当中的静态方法可以重载吗 普通答案不能吧因为没见过这么用的。吧啦吧啦吧啦。。。。。我还是做个例子测测吧。 高手答案不能。因为在 JVM 中调用方法提供了几个不同的字节码指令。invokcvirtual 调用对象的虚方法(也就是可重载的这些方法)。invokespecial 根据编译时类型来调⽤实例⽅法比如静态代码块(通常对应字节码层面的cinit 方法)构造方法(通常对应字节码层面的init方法)。invokestatic 调⽤类静态⽅法。invokcinterface 调⽤接⼝⽅法。 静态方法和重载的方法他们的调用指令都是不一样的那么肯定是无法重载静态方法的。 三、类加载模块 ​ 有了 Class 文件之后接下来就需要通过类加载模块将这些 Class 文件加载到 JVM 内存当中这样才能执行。而关于类加载模块以 JDK8 为例最为重要的内容我总结为三点 每个类加载器对加载过的类保持一个缓存。双亲委派机制即向上委托查找向下委托加载。沙箱保护机制。 ​ 其核心是 ClassLoader 类中的两个方法 //类加载器的核心方法 protected Class? loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// 每个类加载起对他加载过的类都有一个缓存先去缓存中查看有没有加载过Class? c findLoadedClass(name);if (c null) {】//没有加载过就走双亲委派找父类加载器进行加载。long t0 System.nanoTime();try {if (parent ! null) {c parent.loadClass(name, false);} else {c findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {}if (c null) {long t1 System.nanoTime();// 父类加载起没有加载过就自行解析class文件加载。c findClass(name);sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}//这一段就是加载过程中的链接Linking部分分为验证、准备解析三个部分。// 运行时加载类默认是无法进行链接步骤的。if (resolve) {resolveClass(c);}return c;}}​ 另一个核心方法就是所谓的沙箱机制保护 JDK 内部的类不会被覆盖。 private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){if (!checkName(name))throw new NoClassDefFoundError(IllegalName: name);// 不允许加载核心类if ((name ! null) name.startsWith(java.)) {throw new SecurityException(Prohibited package name: name.substring(0, name.lastIndexOf(.)));}if (pd null) {pd defaultDomain;}if (name ! null) checkCerts(name, pd.getCodeSource());return pd;}​ 然后还需要了解一下类在 JVM 内存中的存在方式这也是一些面试题喜欢问的问题。 ​ 类 Class 在 JVM 中的作用其实就是一个创建对象的模板。也就是说他的作用更多的体现在创建对象的过程当中。而在程序具体执行的过程中主要是围绕对象在进行这时候类的作用就不大了。所以在 JVM 中类并不直接保存在宝贵的堆内存当中而是挪到了堆内存以外的一部分内存中。这部分内存在 JDK8 以前被成为永久带PermSpace而在 JDK8 之后被改为了元空间 MetaSpace。 ​ 这个元空间逻辑上可以认为是堆空间的一部分但是他跟堆空间有不同的配置参数不同的管理方式。因此也可以看成是单独的一块内存。这一块内存就相当于家里的工具间或者地下室都是放一些用得比较少的东西。最主要就是类的一些相关信息比如类的元数据、版本信息、注解信息、依赖关系等等。 ​ 元空间可以通过-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize参数设置大小。但是大部分情况下你是不需要管理元空间大小的JVM 会动态进行分配。 ​ 另外这个元空间也是会进行 GC 垃圾回收的。如果一个类不再使用了JVM 就会将这个类信息从元空间中删除。但是显然对类的回收效率是很低的。只有一些自定义类加载器自行加载的一些类有被回收的可能大部分情况下类是不会被回收的。所以堆元空间的垃圾回收基本上是很少有效果的。大部分情况下我们是不需要管元空间的。除非你的JVM 内存确实非常紧张这时可以设定 -XX:MaxMetaspaceSize参数严格控制元空间大小。 ​ 然后在堆中每一个对象的头部还会保存这个对象的类指针(classpoint)指向元空间中的类。这样我们就可以通过一个对象的getClass方法获取到对象所属的类了。这个类指针我们也是可以通过一个小工具观察到的。 ​ 例如下面这个 Maven依赖就可以帮我们分析一个对象在堆中保存的信息。 dependencygroupIdorg.openjdk.jol/groupIdartifactIdjol-core/artifactIdversion0.17/version/dependency​ 然后可以用以下方法简单查看一下对象的内存信息。 public class JOLDemo {private String id;private String name;public static void main(String[] args) {JOLDemo o new JOLDemo();System.out.println(ClassLayout.parseInstance(o).toPrintable());synchronized (o){System.out.println(ClassLayout.parseInstance(o).toPrintable());}} }​ 看到的结果大概是这样 ​ 这里ClassPoint 实际上就是一个指向元空间对应类的一个指针。当然具体结果是被压缩过的。 ​ 另外Markdown标志位就是对象的一些状态信息。包括对象的 HashCode锁状态GC分代年龄等等。 这里面锁机制是面试最喜欢问的地方。无锁、偏向锁(新版本 JDK 中已经废除)、轻量级锁、重量级锁这些东西都是在Markdown中记录的。 ​ 至于类加载器的其他骚套路比如远程加载、热加载、同类多版本加载等之前已经分析过有兴趣的可以翻下我的相关文章。 ​ 在 JDK9 之后Java 引入了模块化机制类加载的体系也发生了一些变化。有兴趣后续再给大家分析。 四、执行引擎 ​ 之前已经看到过在 Class 文件当中已经明确的定义清楚了程序的完整执行逻辑。而执行引擎就是将这些字节指令转为机器指令去执行了。这一块更多的是跟操作系统打交道对开发工作其实帮助就不是很大了。所以如果不是专门研究语言执行引擎这一块就没有必要研究太深了。 解释执行与编译执行 ​ JVM 中有两种执行的方式 解释执行就相当于是同声传译。JVM 接收一条指令就将这条指令翻译成机器指令执行。编译执行就相当于是提前翻译。好比领导发言前就将讲话稿提前翻译成对应的文本上台讲话时就可以照着念了。编译执行也就是传说中的 JIT 。 ​ 大部分情况下使用编译执行的方式显然比解释执行更快减少了翻译机器指令的性能消耗。而我们常用的 HotSpot 虚拟机最为核心的实现机制就是这个 HotSpot 热点。他会搜集用户代码中执行最频繁的热点代码形成CodeCache放到元空间中后续再执行就不用编译直接执行就可以了。 ​ 但是编译执行起始也有一个问题那就是程序预热会比较慢。毕竟作为虚拟机你不可能提前预知到程序员要写一些什么稀奇古怪的代码也就不可能把所有代码都提前编译成模板。而将执行频率并不高的代码也编译保存下来也是得不偿失的。所以现在JDK 默认采用的就是一种混合执行的方式。他会自己检测采用那种方式执行更快。虽然你可以干预 JDK 的执行方式但是在绝大部分情况下都是不需要进行干预的。 ​ 另外现在也有一种提前编译模式AOT 。可以直接将Java 程序编译成机器码。比如GraalVM可以直接将 Java 程序编译成可执行文件这样就不需要 JVM 虚拟机也能直接在操作系统上执行。 ​ 关于 AOT 是不是会一统天下也是现在面试中比较喜欢问的问题。虽然在 SpringBoot3 等框架中已经有了落地但是从目前来看AOT还远没有成为主流离一统天下还有点距离。 ​ 少了 JVM 这个中间商之后虽然大部分情况下是可以提升程序执行性能的但是也并不是就完美无缺了。毕竟很显然这种方式其实是以丧失一定的跨平台特性作为代价的。 ​ 另外目前 AOT 这种方式还是不太安全的。毕竟 JVM 打了这么多年的怪什么牛鬼蛇神都见多了。现在 AOT 要绕开 JVM那么这些怪就都要自己去打了。中间有个什么疏忽那是难免的。 ​ 而且其实 JVM 这个中间商为了提升执行性能其实也是挺劳心费力的。 ​ 一方面目前还并没有成为主流。 ​ 另外其实JVM 这个中间商为了提升程序执行性能也是挺劳心费力的。在实际执行这些字节指令时也并不是埋头搬砖而是针对不同的应用场景提供了很多优化的机制。 ​ JDK 中提供了两种 JIT 编译器一种是-client客户端模式也称为 C1 编译器。另一种是 -server服务端模式也成为 C2 编译器。 ​ C1 会对字节码进行简单和可靠的优化耗时短以达到更快的编译速度。启动快占用内存小执行效率没有server快。默认情况下不进行动态编译适用于桌面应用程序。 ​ C1 的优化策略主要包括 方法内联将引用的函数代码编译到引用点处这样可以减少栈帧的生成减少参数传递以及跳转过程去虚拟化对唯一的实现类进行内联冗余消除在运行期间把一些不会执行的代码折叠掉。 ​ C2 进行耗时较长的优化以及激进优化但优化的代码执行效率更高。启动慢占用内存多执行效率高适用于服务器端应用。 默认情况下就是使用的 C2 编译器。并且绝大部分情况下也不建议特意去使用 C1。 ​ C2 的优化策略主要包括 标量替换用标量值代替聚合对象的属性值栈上分配对于未逃逸的对象直接分配到栈上而不是堆上。同步消除清除同步操作通常指synchronized JVM 也提供了参数关闭这些优化行为。但是显然除了一些偏执狂没人会愿意干这个事情。 其中这个栈上分配是面试时比较喜欢问的问题。一个方法内部使用的一些小对象在允许的情况下JVM 会允许将对象直接在栈上分配而不用分配在堆上。这样栈上的对象用完就抛出了不用进行 GC 执行的速度更快。 ​ 然后这两种模式也可以通过参数-XX:RewriteFrequentPairs参数控制client模式默认关闭server模式默认开启。 ​ 另外在JDK9之后HotSpot中也集成了一种新的编译器 Graal编译器。未来他可能会作为 C2 的替代品响应原本由 C2负责的编译请求。 五、GC 垃圾回收 ​ GC 垃圾自动回收这个可以说是 JVM 最为标志性的功能。不管是做性能调优还是工作面试GC 都是 JVM 部分的重中之重。而对于 JVM 本身GC 也是不断进行设计以及优化的核心。几乎 Java 提出的每个版本都对 GC 有或大或小的改动。这里我就用目前还是用得做多的 JDK8带大家快速梳理一下 GC 部分的主线。 1 、JVM内存布局 ​ 在了解 JVM之前给大家推荐一个工具阿里开源的 Arthas 。官网地址https://arthas.aliyun.com/ 。 这个工具功能非常强大是对 Java进程进行性能调优的一个非常重要的工具对于了解 JVM 底层帮助也非常大。 具体使用方式参照官方文档。 ​ 我们先运行一个简单的 Java 程序 public class GCTest {public static void main(String[] args) throws InterruptedException {List l new ArrayList();for(int i 0 ; i 100_0000 ; i ){l.add(new String(dddddddddddd));Thread.sleep(100);}} }​ 运行后使用Arthas 的dashboard指令可以查看到这个 Java 程序的运行情况。 ​ 重点关注中间的 Memory 部分这一部分就是记录的 JVM 的内存使用情况。而后面的 GC 部分就是垃圾回收的执行情况。我们就从这些能看到的部分作为入口来理解一下一个 Java 进程是怎么管理他的内存的。 ​ 从 Memory 部分可以看到一个 Java 进程会将他管理的内存分为heap堆区和nonheap非堆区两个部分。其中非堆区的几个核心部分像code_cache(热点指令缓存)metaspace(元空间),compressed_class_space(压缩类空间)我们之前都接触到了。这一部分就相当于 Java 进程中的地下室属于不太活跃的部分。而中间heap堆区就相当于客厅了属于Java 中最为核心的部分。而这其中又大体分为了eden_spacesurvivor_space和old_gen三个大的部分这就是 JVM 内存的主体。我们之前分析的栈区这里没有列出。 ​ 我们画个图把这几部分内存整理一下 ​ 其中堆区是 JVM 核心的存放对象的内存区域。他的大小可以由参数 -Xms(初始堆内存大小)-Xmx(最大堆内存)参数指令。从这两个参数可以看到堆内存是可以扩展的。如果初始内存不够JVM 会扩大堆内存。但是如果内存扩展到了最大堆内存时还不够。这时就无法继续扩展了而是会抛出 OOM 异常。这两个参数在生产环境中最好设置成一样减少内存扩展时的性能消耗。 对比之前提到的 RocketMQ 的运行脚本理解。 ​ 这一块内存就是由 Java 自己进行管理的核心内存。那这一块内存到底是如何使用的呢这就和具体的垃圾回收器有关了。 2 、 JVM 有哪些主要的垃圾回收器 ​ java 从诞生到现在最新的 JDK21 版本总共就产生了以下十个垃圾回收器 ​ 其中左边的都是分代算法。也就是将内存划分为年轻代和老年代进行管理。之前在dashboard中已经看到了老年代而 eden_space和survivor_space合起来就是年轻代。图中有虚线连接的部分就是可以配合使用的垃圾回收器组合。比如其中 Parallel ScavengeParallel Old就是 JDK8 中默认的垃圾回收器。也就是dashboard中看到的ps。 ​ 右侧的是不分代算法。也就是不再将内存严格划分位年轻代和老年代。其中 G1 有点特殊。严格来说G1 也包含老年代和年轻代但是他的内存划分是动态的不再那么严格。JDK9 开始默认使用 G1。而 ZGC是目前最主流的垃圾回收器。shennandoah则是OpenJDK 中引入的新一代垃圾回收器与 ZGC 是竞品关系。Epsilon是一个测试用的垃圾回收器根本不干活。 ​ 在这些垃圾回收器中目前主流的 JDK8 还是以分代算法为主。而从 JDK9 开始G1 为代表的不分代算法正在慢慢成为主流。 ​ 这些垃圾回收器应该要如何进行选择呢主要的选择思路有两个 一是考虑你要管理的JVM 内存有多大。二是对执行速度的要求也就是对 STW 时间的控制。 ​ 垃圾回收算法的不断演进最早就是随着内存扩大而演进的。GC 要管理的内存越来越大了所有就需要有不同的算法来进行管理。这就像给学生排课表一样。幼儿园的课表简单写几个字条就能进行比较好的排列。但是随着学习越来越深入课程越来越多要让学生的课程不冲突排课的逻辑就会越来越复杂排课的方式也就必须要随着进行升级。像现在在主流高校中课程排课就已经不可能用人工的方式进行安排了。 ​ Serial串行标记算法是最早的内存回收算法。通常只适合单 CPU 的运行环境多 CPU 环境下效率下降会非常明显。并且他只适合管理几十兆的内存空间如果空间过大STW 时间也会明显增加。 ​ Parallel并行标记算法在 Serial 算法的基础上增加了多线程 GC 。在多 CPU 环境下会比 Serial 更好。他所管理的内存大小在多 CPU 的加持下已经可以达到 GB 级别。在 JDK8 中是默认的垃圾回收器。 ​ CMS 则是比较特殊的一个存在。他的核心思想是尽量让 GC 线程和用户线程一起执行从而减少 Parallel 的 STW 时长。他已经可以管理GB 级别的内存了但是他的 STW 时间控制并不太稳定。从上图连线过程可以看到CMS 是需要 SerialOld垃圾回收器支持的。 CMS 容易产生大量内存碎片。当碎片累计比较多时就会用 SerialOld 进行一次垃圾回收而这次回收的效率是比较低的。并且 CMS 的算法也太过复杂。跟 CMS 相关的 JVM 调优参数也是最多的。所以 CMS 在任何一个 JDK 中都不是默认的垃圾回收器。他也只能适合一些业务不太频繁对执行效率不是太敏感的业务场景。比如 RocketMQ 的 NameServer 。 ​ G1 垃圾回收器是 JDK8 中性能最高的垃圾回收器。他开始推动垃圾回收器从分代管理向不分代管理的过度。在 JDK9 中就被指定为默认的垃圾回收器。而他支持的内存空间已经到了上百 G 的级别。G1 最为经验的地方是他可以设置期望的 STW 时长。因此可以比较适合一些对执行效率比较敏感的业务场景。比如 RocketMQ 的 Broker 。 ​ 而 ZGC 是目前最为先进的垃圾回收器最大可以支持 6TB(2 的 44 次方)的内存空间。 STW 时间非常短基本可以和 C 的执行效率相当了。并且 ZGC 最为亮眼的特点是内存的大小基本不会影响 STW 的时间。并且对于 ZGC几乎不需要进行 JVM 参数调优。官方资料显示ZGC 的调优参数非常少他的算法已经可以实现自行调优。在目前 JDK21 版本中ZGC 已经成功转正。并且很有可能成为未来唯一的垃圾回收器。到那个时候JVM 调优就成了一项考古技能了。 3 、分代垃圾回收工作机制 ​ 这其中各个垃圾回收器的工作机制是有比较大的区别的。但是整体来说这几个分代垃圾回收机制的内存管理方式是差不多的。区别跟多是在管理内存的实现方式上。 如果对各个算法感兴趣可以看下我之前出的 GC 基础 ​ 分代算法将内存划分成了两个大的区域。年轻代和老年代。其中发生在年轻代的垃圾回收操作就称为 YoungGC 或者MinorGC。发生在老年代的垃圾回收操作就称为OldGC或者 MajorGC 。多个内存区域一起进行的垃圾回收操作称为 FullGC 。 ​ 在内存分代模型中一个对象“小 O”的典型生命周期是这样的 小 O初始诞生在 Eden_Space中创建。这是一片寸土寸金的内存空间大部分的对象都是“朝生夕死”的愣头青。Eden_space空间不够就会触发 MinorGC 。清理不再使用的垃圾对象。如果小 O 没有被清理那么他会被移动到 survivor0 区域。并且给他记录一个 GC 年龄 1.GC 年龄就记录在小 O 的markdown标志位中。在下一次 MinorGC 中如果小 O 还没有被清理。那么他将会从survivor0区域移动到survivor1区域同时 GC 年龄增加 1。在后续的 MinorGC 中如果小 O 一直没有被清理。小 O 会在两个survivor区域之间不断转移。每次转移增加一次 GC 年龄。当小 O 的 GC 年龄达到了 16又一直没有被清理掉。那么表明小 O 的地位已经足够高了就不需要再记录GC 年龄了。在下一次 MinorGC 过程中小 O 将会从竞争激烈的年轻代转移到竞争相对平缓的老年代开始比较安稳的老年生活。老年代依然会有 MajorGC 不过相比年轻代不会那么频繁大家都安安稳稳的用到退休为止。 只是一个典型对象的生命周期。当然在这其中还有很多优化的机制。 比如如果小 O 占用内存非常小那么在创建小 O 时JVM 会在Eden_space中单独划分出一小片线程专属的内存空间称为 TLAB 。小 O 就在 TLAB 中创建。由于 TLAB 空间是线程私有的所以就可以避免多个线程之间的资源争抢。 另外如果小 O 占用的内存非常大Eden_space都装不下。这时小 O 就会跳过年轻代直接进入老年代。 六、对 JVM 进行调优的基础思路 ​ 这些垃圾回收算法中有非常多的技术细节。但是我们的目的毕竟是调优。JVM 调优形式上很容易就是定制各种各样的参数。但是这又是一个非常复杂的问题。项目运行期间会面临各种各样稀奇古怪的问题。比如 CPU 超高FullGC 过于频繁时不时的 OOM 异常等等。这些问题大部分情况下都只能凭经验进行深入分析才能做出针对性的解决。 ​ 那么有没有一些比较通用的调优思路呢毕竟我们不可能等每个项目都遇到问题了才去开始调优。其实基础的调优思路就是首先根据内存大小选择合适的垃圾回收器。比如在 JDK8 中使用参数 XX:UseG1GC来强制使用 G1 垃圾回收器。-XX:UseConcMarkSweepGC使用 CMS 垃圾回收器。 ​ 选择好垃圾回收器之后接下来就需要尽量多的干预到这些算法的实现过程中来。而其实这些算法在 JVM 的开发过程当中大部分都已经打磨得非常细致。我们其实很难根据自己的业务场景来干预这些算法的实现细节。我们能够调优的最主要手段就是定制这些分代算法的各个区域的大小。这往往也是调优效果最明显的部分。 从 RocketMQ 的调优脚本就能看到虽然选择了 CMS 垃圾回收算法但是对算法的参数设置非常少。最主要的还是设置各种各样space的大小。 ​ 比如如果应用当中对象创建非常频繁但是这些对象的大部分都是在方法内部创建使用。这就可以扩大年轻代的内存。让大部分的对象在 MinorGC 阶段就可以被快速回收。相反如果应用当中对象创建没有那么频繁有很多需要跨多个方法长期使用的缓存对象这就可以扩大 Old 区的内存。这样可以减少 MajorGC 发生的频率。 ​ 那么应该要如何定制JVM 内存大小呢这就需要我们能够尽量多的了解官网提供的各种参数。其中以下一些核心参数是需要重点了解的。 1 、先定制堆空间、栈空间以及非堆空间的大小 ​ -Xss 栈空间大小。这里是设置每个线程的栈空间大小。 ​ -XX:MetaspaceSize 元空间大小。 -XX:MaxMetaspaceSize 元空间最大大小。元空间默认是没有限制的。如果比较注重服务器的内存资源那么建议设置一个合理的大小。 ​ -Xms:初始堆内存大小-Xmx:堆内存最大值。这两个值尽量设置成一样。因为 JVM 发现堆内存不够时会进行内存扩充。在内存扩充过程中会涉及到大量的对象转移。直接将最大内存和初始内存设置成一样这样可以减少 JVM 在扩充内存时的性能消耗。 ​ Heap 默认最大值为物理内存的 1/4 最小值默认为物理内存的 1/64。按这个比例Heap 存在一个理论上的上限32 位虚拟机物理内存最大 4G堆最大 1G 。 64 位虚拟机物理内存最大 128G堆最大 32G 。如果你的内存确实特别大那么转为使用 G1ZGC 这样的不分代算法可能更合适。 2 、再定制堆空间年轻代和老年代的大小 ​ -XX:/-UseTLAB 设置是否开启 TLAB 空间。默认是开启的。 ​ -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小默认 1% ​ -XX:NewSize: 设置年轻代大小-Xmn:新生代最大内存大小。 老年代大小不需要单独设置。堆内存减去新生代内存剩下的就是老年代内存。 ​ -XX:NewRatio:设置年轻代与老年代的内存比例默认是2表示年轻代与老年代的内存比例是12 ​ -XX:SurvivorRatio:设置年轻代中Eden区与Survivor区的内存比例。这里注意下Survivor是有两个的默认值是8这就表示Eden:S0:S1的内存大小比例是8:1:1 3 、打印 GC 日志定期进行分析 ​ 这是最重要的一步。因为大部分的JVM 问题都需要通过日志打印出来。 ​ -XX:PrintGC: 打印GC信息 类似于-verbose:gc ​ -XX:PrintGCDetails: 打印GC详细信息这里主要是用来观察FGC的频率以及内存清理效率。 ​ -XX:PrintGCTimeStamps 配合 -XX:PrintGC使用。在 GC 中打印时间戳。 ​ -XX:PrintHeapAtGC: 打印GC前后的堆栈信息 ​ -Xloggc:filename : GC日志打印文件。 七、 GC 情况分析实例 1 、打印 GC 日志 ​ 例如我们可以用以下这个小实验来上手练练。 public class GcLogTest {public static void main(String[] args) {ArrayListbyte[] list new ArrayList();for (int i 0; i 500; i) {byte[] arr new byte[1024 * 100];//100KBlist.add(arr);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}} }然后在执行这个方法时添加以下 JVM 参数 -Xms60m -Xmx60m -XX:SurvivorRatio8 -XX:PrintGCDetails执行后可以看到类似这样的输出信息。 ​ 这里面就记录了两次 MinorGC 和两次 FullGC 的执行效果。另外在程序执行完成后也会打印出 Heap 堆区的内存使用情况。 ​ 当然目前这些日志信息只是打印在控制台你只能凭经验自己强行去看。接下来就可以添加-Xloggc参数将日志打印到文件里。然后拿日志文件进行整体分析。 2 、分析 GC 日志 ​ 之前那些日志信息只是打印在控制台你只能凭经验自己强行去看。我们当前这个程序非常简单还好一点。往往一个真实应用的日志非常复杂。一行行自己看就完蛋了。接下来就可以添加-Xloggc参数将日志打印到文件里。然后拿日志文件进行整体分析。 ​ 这里推荐一个开源网站 https://www.gceasy.io/ 这是国外一个开源的GC 日志分析网站。你可以把 GC 日志文件直接上传到这个网站上他就会分析出日志文件中的详细情况。 这是个收费网站但是有免费使用的额度。 ​ 比如可以将我本地部署的一个 RocketMQ 的 NameServer 的日志文件上传到这个网站就可以拿到这样的分析图。 RocketMQ 的 GC 日志文件默认打印在/dev/shm目录下。 ​ 如果你跟着教程走到这那么恭喜你JVM 调优你已经真正入门了。接下来多练多看多分析无他喂手熟而。 Java 高手之路正在向你敞开。 ​ 最后记得给楼兰点个赞吧。
http://wiki.neutronadmin.com/news/170057/

相关文章:

  • 营销网站用户体验有哪些网页游戏排行选择
  • 国内做网站建设知名的公司软件开发者模式
  • 怎么做网站排名优化免费视觉设计评价标准的要素
  • 登录浏览器是建设银行移动门户网站做短租类型的网站
  • 美丽南方官网网站建设凡科商城是什么
  • 湖北网站推广公司技巧做网站跳转怎么收费
  • 深圳网站建设clh重庆开县网站建设公司推荐
  • 网站设置不可粘贴免费网站优化排名
  • 温州电力建设有限公司网站深圳电商代运营公司排名
  • 个人网站建设价格表wordpress访客函数
  • jsp开发网站开发源码有没有什么设计排版类网站
  • 如何转移网站宝安营销型网站制作
  • 陕西 汽车 网站建设做网站不备案
  • 淘宝客模板网站有没有专门学做婴儿衣服的网站
  • 商城类网站功能列表如何防止网站挂马
  • 百度调整导致网站排名下降wordpress连接微信支付
  • 青海省教育厅门户网站江苏环泰建设有限公司网站
  • 学院网站建设流程图免费logo在线制作头像
  • 网站及微站建设合同wordpress wiki插件
  • 贵阳网站开发网站建设 月嫂 模板
  • wordpress整合百度站内搜索巩义网站
  • 旅游网站哪家好又便宜网站建设与推cctv-10
  • 百度站内搜索 wordpress写手机版网站的静态页面
  • 地产设计网站域名访问网址
  • 网站开发投标书自助网站建设公司
  • 哪个网站代做装修效果图ui和平面设计的区别
  • 淘宝优惠劵网站怎么做wordpress通过id获取文章
  • 宝安网站设计排名手机网站建设与制作
  • 偷dede网站模板佛山外贸网站建设行情
  • 塘厦 网站建设 百度推广新建网页的方法有哪些