十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JVM知识点总结

JVM知识点总结 一. 概述1.什么是Java虚拟机为什么Java被称作是“平台无关的编程语言”1Java虚拟机是一个可以执行Java字节码的虚拟机进程。2因为JVM识别的是字节码文件只需将Java源文件编译成class字节码文件后即可被各个平台下的JVM所执行做到“一次编译处处运行”。1.5. Java文件是如何被运行的执行main方法的步骤如下:编译好 App.java 后得到 App.class 后执行 App.class系统会启动一个 JVM 进程从 classpath 路径中找到一个名为 App.class 的二进制文件将 App 的类信息加载到运行时数据区的方法区内这个过程叫做App 类的加载JVM 找到 App 的主程序入口执行main方法这个main中的第一条语句为 Student student new Student(“tellUrDream”) 就是让 JVM 创建一个Student对象但是这个时候方法区中是没有 Student 类的信息的所以 JVM 马上加载 Student 类把 Student 类的信息放到方法区中加载完 Student 类后JVM在堆中为一个新的 Student 实例分配内存然后调用构造函数初始化 Student 实例这个Student 实例持有 指向方法区中的 Student 类的类型信息 的引用执行student.sayName();时JVM 根据 student 的引用找到 student 对象然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表获得 sayName() 的字节码地址。执行sayName()二. 内存管理¥2.Java内存模型Java堆Heap存放对象实例元空间也被叫做永久代用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。使用直接内存运行时常量池。运行时常量池是元空间的一部分。程序计数器Program Counter Register当前线程所执行的字节码的行号指示器。作用字节码解释器通过改变程序计数器来依次读取指令从而实现代码的流程控制如顺序执行、选择、循环、异常处理。在多线程的情况下程序计数器用于记录当前线程执行的位置从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。JVM栈JVM Stacks虚拟机栈描述的是Java方法执行的内存模型每个方法被执行的时候都会同时创建一个栈帧Stack Frame用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。局部变量表主要存放了编译期可知的各种数据类型boolean、byte、char、short、int、float、long、double、对象引用reference 类型它不同于对象本身可能是一个指向对象起始地址的引用指针也可能是指向一个代表对象的句柄或其他与此对象相关的位置。本地方法栈Native Method Stacks虚拟机使用到的Native方法的栈。直接内存方法区和对是所有线程共享的内存区域而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。2.5 JVM堆和栈为什么要分开放从软件设计的角度来看栈代表了处理逻辑而堆代表了数据这样分离使得处理逻辑更为清晰。这种隔离、模块化的思想在软件设计的方方面面都有体现。堆与栈的分离使得堆中的内容可以被多个栈共享。这种共享有很多好处一方面提供了一种有效的数据交互方式如内存共享另一方面节省了内存空间。栈因为运行时的需要如保存系统运行的上下文需要进行址段的划分。由于栈只能向上增长因此会限制住栈存储内容的能力。而堆不同堆的大小可以根据需要动态增长。因此堆与栈的分离使得动态增长成为可能相应栈中只需要记录堆中的一个地址即可。2.6 直接内存直接内存并不是虚拟机运行时数据区的一部分也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用而且也可能导致OutOfMemoryError 异常出现所以我们放到这里一起讲解。 在 JDK 1.4 中新加入了 NIONewInput/Output类引入了一种基于通道Channel与缓冲区Buffer的 I/O 方式它可以使用 Native函数库直接分配堆外内存然后通过一个存储在 Java 堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能因为避免了在 Java 堆和 Native 堆中来回复制数据。显然本机直接内存的分配不会受到 Java堆大小的限制但是既然是内存则肯定还是会受到本机总内存包括物理内存、SWAP分区或者分页文件大小以及处理器寻址空间的限制一般服务器管理员配置虚拟机参数时会根据实际内存去设置-Xmx 等参数信息但经常忽略掉直接内存使得各个内存区域总和大于物理内存限制包括物理的和操作系统级的限制从而导致动态扩展时出现 OutOfMemoryError 异常。直接内存常用于NIO操作用于数据缓冲区。直接内存分配回收成本较高但读写性能高。直接内存不受JVM内存回收管理直接内存详解https://blog.csdn.net/weixin_51146329/article/details/1287707013.Java对象创建过程JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用是否被加载过没有则加载这个类为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”线程安全的解决“本地线程缓冲分配(TLAB)”或CAS重试将除对象头外的对象内存空间初始化为零值这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用对对象头进行必要设置例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外根据虚拟机当前运行状态的不同如是否启用偏向锁等对象头会有不同的设置方式。执行init方法3.5 对象内存分配方法分配方式有 “指针碰撞” 和 “空闲列表” 两种选择哪种分配方式由 Java 堆是否规整决定而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。内存分配并发问题在创建对象的时候有一个很重要的问题就是线程安全因为在实际开发过程中创建对象是很频繁的事情作为虚拟机来说必须要保证线程是安全的通常来讲虚拟机采用两种方式来保证线程安全CAS失败重试 CAS 是乐观锁的一种实现方式。所谓乐观锁就是每次不加锁而是假设没有冲突而去完成某项操作如果因为冲突失败就重试直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。TLAB 为每一个线程预先在 Eden 区分配一块儿内存JVM 在给线程中的对象分配内存时首先在 TLAB 分配当对象大于 TLAB 中的剩余内存或TLAB 的内存已用尽时再采用上述的 CAS 进行内存分配3.6 init方法和 clinit方法init是对象构造器方法在程序执行new一个对象调用该类的构造方法时会调用 init方法。clinit方法是类构造器方法在JVM进行类加载的初始化阶段会调用clinit方法4.Java对象结构Java对象由三个部分组成对象头、实例数据、对齐填充。对象头:由两部分组成第一部分存储对象自身的运行时数据哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID一般占32/64 bit。第二部分是指针类型指向对象的类元数据类型即对象代表哪个类。如果是数组对象则对象头中还有一部分用来记录数组长度。实例数据:用来存储对象真正的有效信息包括父类继承下来的和自己定义的对齐填充JVM要求对象起始地址必须是8字节的整数倍8字节对齐4.5 对象的访问定位句柄 如果使用句柄的话那么 Java 堆中将会划分出一块内存来作为句柄池reference 中存储的就是对象的句柄地址而句柄中包含了对象实例数据与类型数据各自的具体地址信息直接指针 如果使用直接指针访问那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息而 reference 中存储的直接就是对象的地址。这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址在对象被移动时只会改变句柄中的实例数据指针而 reference 本身不需要修改修改方便。使用直接指针访问方式最大的好处就是速度快它节省了一次指针定位的时间开销。$5.如何判断对象可以被回收判断对象是否存活一般有两种方式引用计数每个对象有一个引用计数属性新增一个引用时计数加1引用释放时计数减1计数为0时可以回收。此方法简单无法解决对象相互循环引用的问题。可达性分析Reachability Analysis从GC Roots开始向下搜索搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时则证明此对象是不可用的不可达对象。判断是否进行回收:1.该对象没有与GC Roots相连2.该对象没有重写finalize()方法或finalize()已经被执行过则直接回收第一次标记、否则将对象加入到F-Queue队列中优先级很低的队列在这里finalize()方法被执行之后进行第二次标记如果对象仍然应该被GC则GC否则移除队列。 在finalize方法中对象很可能和其他 GC Roots中的某一个对象建立了关联finalize方法只会被调用一次且不推荐使用finalize方法5.5 有哪些是可以作为GCroots虚拟机栈引用对象类静态属性引用对象常量引用对象本地方法栈引用对象5.6 记忆集RemberSet)跨代引用是指新生代中存在对老年代对象的引用或者老年代中存在对新生代的引用如下图所示YGC时为了找到年轻代中的存活对象不得不遍历整个老年代反之亦然。这种方案存在极大的性能浪费。因为跨代引用是极少的为了找出那么一点点跨代引用却得遍历整个老年代解决方案记忆集RemberSet)记忆集就是用来记录跨代引用的表通过引入记忆集避免遍历老年代。以YGC为例说明要回收年轻代只需要引用年轻代对象的GC ROOT记忆集就可以判断出Young区对象是否存活不必再遍历老年代。缺点具有“滞后性”浪费一定的空间如下图所示YGC时实际上对象E可以被回收但是由于没发生FGC老年代中的对象D仍存在对对象E的引用导致E无法被回收。G1是对每个region维护了一个rset记忆集中维护了指向自己的region的指针并且标记指针分别在那些卡页的范围之内。虚拟机发现对引用数据类型进行写操作时会产生一个中断判断引用对象是否处于不同Region之中如果是则把相关引用信息记录到被引用对象所属Region的RememberSet之中。5.7 OopMap在进行初始标记时对GC Roots 枚举的过程中是需要暂停用户线程的对栈进行扫描找到哪些地方存储了对象的引用。然而栈存储的数据不止是对象的引用因此对整个栈进行全量扫描显然是很耗费时间影响性能的。因此在 HotSpot 中采取了空间换时间的方法使用 OopMap 来存储栈上的对象引用的信息。在 GC Roots 枚举时只需要遍历每个栈桢的 OopMap通过 OopMap 存储的信息快捷地找到 GC Roots。6.方法区的回收方法区回收价值很低主要回收废弃的常量和无用的类。如何判断无用的类该类所有实例都被回收Java堆中没有该类的对象加载该类的ClassLoader已经被回收该类对应的java.lang.Class对象没有在任何地方被引用无法在任何地方利用反射访问该类¥7.垃圾收集算法及其特点标记清除算法标记出所有需要回收的对象然后清除可回收的对象。效率较低并且因为在清除后没有重新整理可用的内存空间如果内存中可被回收的小对象居多会引起内存碎片化问题。复制算法将可用内存分为区域1和区域2将新生成的对象放在区域1在区域1满后对区域1进行一次标记将标记后仍然存活的对象复制到区域2然后清除区域1。效率较高并且易于实现解决了内存碎片化的问题缺点是浪费了大量内存同时在系统中存在长生命周期对象时会在两区域间来回复制影响系统效率。标记整理算法结合了标记清除算法和复制算法的优点标记过程和标记清除算法一样标记后将存活的对象移动到一端清理另一端。分代收集算法根据对象不同类型把内存划分为不同区域把堆划分为新生代和老年代。由于新生代的对象生命周期较短主要采用复制算法。将新生代划分为一块较大的Eden区和两块较小的Survivor区Servivor区又分为ServivorTo和ServivorFrom区。JVM在运行过程中主要使用Eden和SurvivorFrom区进行垃圾回收时将这个两个区域存活的对象复制到SurvivorTo区并清除这两个区域。老年代主要存储长生命周期的大对象因此采用标记清除或标记整理算法。7.5 HotSpot 为什么要分为新生代和老年代在新生代中每次收集都会有大量对象死去所以可以选择复制算法只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的而且没有额外的空间对它进行分配担保所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。¥8.内存回收与回收策略对象优先分配在Eden区如果Eden区没有足够的空间时虚拟机执行一次Minor GC。大对象直接进入老年代避免在Eden区和两个Survivor区之间发生大量的内存拷贝。长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器如果对象经过了1次Minor GC那么对象会进入Survivor区之后每经过一次Minor GC那么对象的年龄加1年龄达到阀值对象进入老年区。动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半年龄大于或等于该年龄的对象可以直接进入老年代。空间分配担保。每次进行Minor GC时JVM会计算Survivor区移至老年区的对象的平均大小如果这个值大于老年区的剩余值大小则进行一次Full GC。9.新生代的垃圾回收机制新生代的GC过程叫做MinorGC采用复制算法实现具体过程如下把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区如果某对象的年龄达到老年代的标准则将其复制到老年代同时把这些对象的年龄加1。如果ServivorTo区的内存空间不够则也直接将其复制到老年代。如果对象属于大对象则也直接复制到老年代。清空Eden区和ServivorFrom区中的对象。将ServivorFrom区和ServivorTo区互换原来的ServivorTo区成为下一次GC时的ServivorFrom区。10.老年代的垃圾回收机制老年代主要存放有长生命周期的对象和大对象老年代的GC叫MajorGC。在老年代对象比较稳定MajorGC不会频繁触发。在进行MajorGC前JVM会进行一次MinorGC过后仍然出现老年代空间不足或无法找到足够大的连续内存空间分配给新创建的大对象时会触发MajorGC进行垃圾回收释放JVM的内存空间。MajorGC采用标记清除算法该算法首先会扫描所有对象并标记存活的对象然后回收未被标记的对象并释放内存空间。因为要先扫描老年代的所有对象再回收所以MajorGC的时间较长。容易产生内存碎片在老年代没有内存空间可分配时会出现内存溢出异常。10.5 哪些情况会触发FullGCSystem.gc()方法的调用此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。疏散失败在进行Young GC或Mixed GC时G1需要将存活对象从一个Region复制到另一个空闲的Region时堆中没有足够的空闲Region元空间空间不足统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间堆中分配很大的对象所谓大对象是指需要大量连续内存空间的java对象例如很长的数组此种对象会直接进入老年代而老年代虽然有很大的剩余空间但是无法找到足够大的连续空间来分配给当前对象此种情况就会触发JVM进行Full GC。¥11. G1垃圾回收器目标尽可能在用户规定的停顿时间内尽可能回收垃圾多的region内存结构将整个堆划分为多个大小相等的独立区域称为 Region。每个Region可以动态地扮演Eden区、Survivor区、Old区或Humongous区专存大对象。11.1 G1垃圾回收器的Young GC触发时机当前分配的新生代RegionEden区耗尽且应用程序需要分配新对象时G1就会触发一次Young GC。执行过程G1通过记录在RSetRemembered Set记录其他Region指向本Region的引用避免全堆扫描中的信息找到存活的对象。将Eden区和Survivor区中的存活对象复制到新的Survivor区或者晋升到Old区Region。清空原来的Eden区Region供后续分配使用。特点这个过程是STW的。G1会根据用户设定的期望停顿时间-XX:MaxGCPauseMillis默认200ms智能选择回收哪些Region尽量不超时。11.2 G1垃圾回收器的全局并发标记周期当老年代占用率达到一定阈值默认45%由-XX:InitiatingHeapOccupancyPercent控制时G1会触发全局并发标记为后续的Mixed GC做准备。这个过程分为四个步骤初始标记STW负责标记GC Roots能直接关联到的对象。需要STW但耗时极短。通常这个阶段会借用一次正在发生的Young GC来完成没有额外单独的停顿。并发标记并发从GC Roots开始对堆中的对象进行可达性分析找出存活对象。与用户线程并发执行不需要STW。这个阶段如果发现当前Region中没有任何存活对象G1会在该阶段就将其回收这种机制被称为“回收即清理” Live-Range Coalescing。最终标记STW处理并发标记阶段遗留的少量变更记录由于并发执行用户线程在标记期间产生了引用变更。需要短暂的STW使用SATBSnapshot-At-The-Beginning算法保证标记结果的正确性。筛选回收STW对各个Region的回收价值垃圾占比、回收耗时进行排序。制定回收计划将那些垃圾最多、回收收益最高的Region包含老年代和新生代加入到待回收集合中。11.3 G1垃圾收集器的Mixed GC混合回收全局并发标记周期结束后G1并不会立刻全盘清理而是通过Mixed GC逐步清理老年代。阶段说明Mixed GC不仅回收所有的年轻代Region还会回收部分被标记为“高收益”的老年代Region。执行过程与Young GC类似将存活对象复制到空的Region中。区别在于复制的对象不仅有年轻代的还有老年代的存活对象。执行次数G1会连续进行多次Mixed GC直到老年代的内存占用率降到安全水位以下或者待回收的老年代Region被清理完毕。这就是G1被称为“Garbage-First”的原因优先收集垃圾最多的Region。11.4 G1垃圾收集器的 Full GC退化/兜底机制在正常情况下G1通过Mixed GC就能完成内存清理。但如果出现异常情况G1会退化为传统的Serial单线程收集器进行Full GC性能极差需要极力避免。触发条件在Mixed GC或Young GC时由于存活对象过多导致没有足够的空Region来存放复制的存活对象发生Evacuation Failure疏散失败。巨型对象分配失败Humongous Allocation Failure无法在堆中找到连续的空闲Region存放巨型对象。执行过程停止所有用户线程使用单线程对整个堆新生代、老年代、元空间等进行全面的标记、清理和压缩。此过程耗时极长会导致应用长时间卡顿。11.5 G1垃圾收集器如何确定每次回收哪些Region计算回收价值 在并发标记的最终标记阶段结束后G1会统计数据计算出每个Region的“回收价值”。回收价值通常可以理解为(Region的大小 - 存活对象大小) / 预计回收耗时。简单来说就是垃圾越多、存活对象越少、回收越快的Region价值越高。构建回收集合 G1会将所有Region按照回收价值从高到低进行排序形成一个优先级列表。G1的“Garbage-First”之名正是来源于此它总是优先去回收那些垃圾最多的Region。记录历史耗时数据 在每次GC执行的过程中G1会详细记录各种操作的耗时统计数据主要包括复制存活对象的耗时G1会记录平均复制每个MB的存活对象需要多少时间。预测与筛选机制 当G1准备触发Mixed GC混合回收或Young GC时它会根据上述历史数据来“估算”回收列表中每个Region需要花费的时间。G1从按价值排序的Region列表中从高到低依次选取Region加入本次的“回收集合”。每加入一个RegionG1就会把预测的回收该Region的时间累加起来。一旦累加的预测时间即将达到或超过用户设定的期望停顿时间如200msG1就会停止继续添加新的Region到回收集合中。11.7 Safe Point然而在程序执行的过程中对象之间的引用关系随时都会发生改变这意味着对应的 OopMap 需要同步进行更新。如果每一条指令的执行都生成或更新对应的OopMap那么将会占用大量的内存空间增加了 GC 的空间成本。因此针对这个问题JVM 引入了 Safe Point 的概念只有在 Safe Point 才会生成或更新对应的 OopMap。Safe Point 就是一个安全点可以理解为用户线程执行过程中的一些特殊位置。线程执行到 Safe Point 的时候OopMap 保存了当前线程的上下文当线程执行到这些位置的时候说明线程当前的状态是确定的线程有哪些对象、使用了哪些内存。适合放置 Safe Point的地方所有的非计数循环的末尾 防止循环体的执行时间太长一直进入不了 Safe Point所有方法返回之前每条 Java 编译后的字节码的边界当所有线程都到达Safe Point有两种方法中断线程抢占式中断(Preemptive Suspension) JVM会中断所有线程然后依次检查每个线程中断的位置是否为SafePoint如果不是则恢复用户线程让它执行至 Safe Point 再阻塞。主动式中断(Voluntary Suspension) 大部分 JVM实现都是采用主动式中断需要阻塞用户线程的时候首先做一个标志用户线程会主动轮询这个标志位如果标志位处于就绪状态就自行中断。11.8 Safe Region然而实际情况中 Safe Point 仍然存在缺陷例如线程处于 Sleep 状态或者 Blocked 状态那么线程就无法达到 Safe Point。因此针对这个问题JVM 引入了Safe Region的概念。Safe Region 是一片区域在这个区域的代码片段引用关系不会发生变化因此在 Safe Region 中任意地方开始垃圾收集都是安全的。可以理解为 Safe Region 就是 Safe Point 的扩展点动成线。线程执行到 Safe Region 时首先标记线程已经进入 Safe Region当线程将要离开 Safe Region 时线程需要检查 JVM 是否已经完成 GC Roots 枚举。如果尚未完成则需要一直等待直到 GC Roots 枚举完成。参考文献https://zhuanlan.zhihu.com/p/44186730212. 有什么办法可以主动通知虚拟机进行垃圾回收呢程序可以使用system.gc回收但是回不回收看jvm13.finalize()方法什么时候被调用析构函数(finalization)的目的是什么调用时机当垃圾回收器要宣告一个对象死亡时至少要经过两次标记过程如果对象在进行可达性分析后发现没有和GC Roots相连接的引用链就会被第一次标记并且判断是否执行finalizer( )方法如果对象覆盖finalizer( )方法且未被虚拟机调用过那么这个对象会被放置在F-Queue队列中并在稍后由一个虚拟机自动建立的低优先级的Finalizer线程区执行触发finalizer( )方法但不承诺等待其运行结束。finalization的目的对象逃脱死亡的最后一次机会。只要重新与引用链上的任何一个对象建立关联即可。但是不建议使用运行代价高昂不确定性大且无法保证各个对象的调用顺序。可用try-finally或其他替代。14.如果对象的引用被置为null垃圾收集器是否会立即释放对象占用的内存不会立即释放对象占用的内存。 如果对象的引用被置为null只是断开了当前线程栈帧中对该对象的引用关系而 垃圾收集器是运行在后台的线程只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系扫描到对象没有被引用则会标记对象这时候仍然不会立即释放该对象内存因为有些对象是可恢复的在 finalize方法中恢复引用 。只有确定了对象无法恢复引用的时候才会清除对象内存。¥15. java中会存在内存泄漏吗如何排查。java中内存泄露只被该被回收的对象没有被正确回收内存泄漏的场景静态集合类内部对象没被及时释放外部类的实例对象的方法返回了一个内部类的实例对象这个内部类对象被长期引用了即使那个外部类实例对象不再被使用但由于内部类持久外部类的实例对象这个外部类对象将不会被垃圾回收这也会造成内存泄露。当一个对象被存储进HashSet集合中以后就不能修改这个对象中的那些参与计算哈希值的字段了否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了在这种情况下即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象也将返回找不到对象的结果这也会导致无法从HashSet集合中单独删除当前对象造成内存泄露。排查jmap获得堆转储快照再利用快照分析工具进行分析如Visual VM¥16Java中4种引用类型强引用最常见的引用类型把一个对象指向一个引用变量时就是强引用。强引用的对象一定为可达性状态所以不会被垃圾回收是内存泄漏的主要原因。软引用通过SoftReference实现如果一个对象只有软引用当内存空间不足时将被回收。软引用可用来实现内存敏感的高速缓存。弱引用通过WeakReference实现如果一个对象只有弱引用在垃圾回收过程中一定会被回收。虚引用通过PhantomReference实现虚引用和引用队列联合使用主要用来跟踪对象的垃圾回收过程。特别注意在程序设计中一般很少使用弱引用与虚引用使用软引用的情况较多这是因为软引用可以加速 JVM 对垃圾内存的回收速度可以维护系统的运行安全防止内存溢出OutOfMemory等问题的产生。16.5 虚引用与软引用和弱引用的区别虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于虚引用必须和引用队列ReferenceQueue联合使用。当垃圾回收器准备回收一个对象时如果发现它还有虚引用就会在回收对象的内存之前把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列那么就可以在所引用的对象的内存被回收之前采取必要的行动。17.永久代与元空间永久代指内存的永久保存区域主要存放Class和Meta元数据的信息。Class在类加载时被放入永久代。永久代和老年代、新生代不同GC不会在程序运行期间对永久代的内存进行清理这也导致了永久代的内存会随着加载的Class文件的增加而增加在加载的Class文件过多时会出现内存溢出异常比如Tomcat引用jar文件过多导致JVM内存不足而无法启动。在JDK1.8中永久代已经被元数据区取代。元数据区的作用和永久代类似二者最大的区别在于元数据区并没有使用虚拟机的内存而是直接使用操作系统的本地内存。因此元空间的大小不受JVM内存的限制只和操作系统的内存有关。在JDK1.8中JVM将类的元数据放入本地内存中将常量池和类的静态常量放入Java堆中这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存空间决定而由操作系统的实际可用内存空间决定。17.5 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?整个永久代有一个 JVM 本身设置固定大小上限无法进行调整而元空间使用的是直接内存受本机可用内存的限制虽然元空间仍旧可能溢出但是比原来出现的几率会更小。当你元空间溢出时会得到如下错误 java.lang.OutOfMemoryError: MetaSpace你可以使用 -XXMaxMetaspaceSize 标志设置最大元空间大小默认值为 unlimited这意味着它只受系统内存的限制。-XXMetaspaceSize 调整标志定义元空间的初始大小。如果未指定此标志则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。元空间里面存放的是类的元数据这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制这样能加载的类就更多了。在 JDK8合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。17.6 JVM调优策略根据需求选择合适的垃圾收集器调整新生代和老年代的比值将新对象预留在新生代由于 Full GC 的成本远高于 Minor GC因此尽可能将对象分配在新生代是明智的做法实际项目中根据 GC 日志分析新生代空间大小分配是否合理适当通过“-Xmn”命令调节新生代大小最大限度降低新对象直接进入老年代的情况。合理调整Survivor区和Eden区的比值避免Survivor区不够用。-XX:SurvivorRatio幸存代— 设置两个Survivor区和eden的比值大对象进入老年代大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代破坏新生代的对象结构可能会出现频繁的 full gc。因此对于大对象可以设置直接进入老年代。-XX:PretenureSizeThreshold 可以设置直接进入老年代的对象大小。合理设置进入老年代对象的年龄-XX:MaxTenuringThreshold 设置对象进入老年代的年龄大小减少老年代的内存占用降低 full gc 发生的频率。设置稳定的堆大小堆大小设置有两个参数-Xms 初始化堆大小-Xmx 最大堆大小。开发过程中通常会将 -Xms 与 -Xmx两个参数配置成相同的值其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。合理设置栈大小避免栈空间不足溢出。可以通过-Xss调整每个线程栈空间的大小。JDK5.0以后每个线程堆栈大小为1M以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的不能无限生成经验值在3000~5000左右二. 执行子系统¥18.Java类的加载过程Java类的生命周期加载、连接验证准备解析、初始化、使用和卸载加载查找并加载类的二进制数据在Java堆中也创建一个java.lang.Class类的对象一个非数组类的加载阶段加载阶段获取类的二进制字节流的动作是可控性最强的阶段这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式重写一个类加载器的 loadClass() 方法。数组类型不通过类加载器创建它由 Java 虚拟机直接创建。连接 1验证文件格式、元数据、字节码、符号引用验证 2准备为类的静态变量分配内存并将其初始化为默认值 3解析把类中的符号引用转换为直接引用初始化为类的静态变量赋予正确的初始值使用new出对象程序中使用卸载执行垃圾回收19.哪些情况下类会初始化哪些情况不会初始化场景①创建类的实例。②访问某个类或接口的静态变量或对该静态变量赋值。③调用类的静态方法。④初始化一个类的子类时初始化子类父类必须先初始化。⑤JVM启动时被标为启动类的类。⑥MethodHandle不会初始化场景除了上述都不会发生初始化如①常量在编译时会存放在使用该常量的类的常量池该过程不要调用常量所在的类不会初始化。②子类引用父类的静态变量时子类不会初始化只有父类会初始化。③定义对象数组不会触发该类的初始化。④在使用类名获取Class对象时不会触发类的初始化。⑤在使用Class.forName()加载指定的类时可以通过initialize参数设置是否需要初始化。⑥在使用ClassLoader默认的loadClass方法加载类时不会触发该类的初始化。¥20.类加载器启动类加载器Bootstrap ClassLoader负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录下同)下或被-Xbootclasspath参数指定的路径中的并且能被虚拟机识别的类库扩展类加载器Extension ClassLoader它负责加载DK\jre\lib\ext目录中或者由java.ext.dirs系统变量指定的路径中的所有类库应用程序类加载器Application ClassLoader该类加载器由sun.misc.Launcher$AppClassLoader来实现它负责加载用户类路径ClassPath所指定的类开发者可以直接使用该类加载器¥21.双亲委派机制好处是什么1顶层的启动类加载器外其余的类加载器都应当有自己的父类加载器。一个类收到类加载请求后会层层找父类加载器去尝试加载因此所有的加载请求最终都会被传送到顶层的启动类加载器只有当父类加载器反馈自己无法完成加载时子加载器才会尝试自己去加载。2双亲委派模型保证了Java程序的稳定运行可以避免类的重复加载JVM 区分不同类的方式不仅仅根据类名相同的类文件被不同的类加载器加载产生的是两个不同的类也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型而是每个类加载器加载自己的话就会出现一些问题比如我们编写一个称为 java.lang.Object 类的话那么程序运行的时候系统就会出现多个不同的 Object 类。22. 如何自定义类加载器如何打破双亲委派模型自定义加载器的话需要继承 ClassLoader。如果我们不想打破双亲委派模型就重写 ClassLoader 类中的findClass()方法即可无法被父类加载器加载的类最终会通过这个方法被加载。但是如果想打破双亲委派模型则需要重写loadClass() 方法双亲委派的源码实际就是递归调用父类加载器去加载加载不了再调用findClass找到类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); // 前面提到bootstrap classloader的类加载器为null通过find方法来获得 } else { c findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c null) { // 如果还是没有获得该类调用findClass找到类 long t1 System.nanoTime(); c findClass(name); // jvm统计 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } // 连接类 if (resolve) { resolveClass(c); } return c; } }23. 打破双亲委派的场景双亲委派出现之前ClassLoader只有loadClass() 方法方法之后才加的findClass()方法JNDI服务。父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制父类加载器无法加载到需要的文件以Driver接口为例由于Driver接口定义在jdk当中的而其实现由各个数据库的服务商来提供比如mysql的就写了MySQL Connector那么问题就来了DriverManager也由jdk提供要加载各个实现了Driver接口的实现类然后进行管理但是DriverManager由启动类加载器加载只能记载JAVA_HOME的lib下文件而其实现是由服务商提供的由系统类加载器加载这个时候就需要启动类加载器来委托子类来加载Driver实现从而破坏了双亲委派这里仅仅是举了破坏双亲委派的其中一个情况。具体实现通过线程上下文类加载器去进行类的加载可以通过Thread类的setContextClassLoader()去设置如果没有设置则默认为应用程序类加载器。代码热替换。每一个程序模块Bundle)都有自己的类加载器每当需要更换就把Bundle连同类加载器一起换掉以实现代码的热替换。24. JVM方法内联就是把调用方函数代码复制到调用方函数中如private int add2(int x1 , int x2 , int x3 , int x4) { return add1(x1 , x2) add1(x3,x4); } private int add1(int x1 , int x2) { return x1 x2; }经内联后private int add2(int x1 , int x2 , int x3 , int x4) { //return add1(x1 , x2) add1(x3,x4); return x1 x2 x3 x4; }JVM会自动的识别热点方法并对它们使用方法内联优化。那么一段代码需要执行多少次才会触发JIT优化呢通常这个值由-XX:CompileThreshold参数进行设置使用client编译器时默认为1500使用server编译器时默认为10000但是一个方法就算被JVM标注成为热点方法JVM仍然不一定会对它做方法内联优化。其中有个比较常见的原因就是这个方法体太大了分为两种情况如果方法是经常执行的默认情况下方法大小小于325字节的都会进行内联可以通过 -XX:MaxFreqInlineSizeN来设置这个大小如果方法不是经常执行的默认情况下方法大小小于35字节才会进行内联可以通过 -XX:MaxInlineSizeN 来设置这个大小我们可以通过增加这个大小以便更多的方法可以进行内联但是除非能够显著提升性能否则不推荐修改这个参数。因为更大的方法体会导致代码内存占用更多更少的热点方法会被缓存最终的效果不一定好。三、问题排查25、如何排查服务器中Java应用CPU飙高当Java应用启动时CPU使用率很高一段时间后下跌并恢复稳定这通常是因为在启动阶段进行了大量的初始化工作如类加载、JIT编译、数据库连接等。具体的排查步骤top 命令查询CPU占用率高的Java应用获取应用的PIDtop -Hp {PID} 命令查看进程下的线程获取CPU消耗高的线程pidjstack 跟踪线程的调用栈 jstack PID|grep 线程id发现线程就一直处于 java.lang.Thread.State: RUNNABLE 执行状态根据堆栈信息查询具体的代码jstack 介绍jstack 是 JDK 提供的一个命令行工具用于生成 Java 进程的线程快照。线程快照包含了 Java 进程中所有线程的状态信息如线程的名称、线程的状态RUNNABLE、WAITING、BLOCKED 等以及线程的调用栈。通过分析 jstack 生成的线程快照可以帮助您诊断诸如死锁、线程阻塞、CPU 使用率过高等与线程相关的问题。
返回列表