Day126 JVM

JVM

  • JVM(Java Virtual Machine ):Java虚拟机,简称JVM,是运行所有Java程序的假想计算机,是Java程序的运行环境,是Java 最具吸引力的特性之一。Java语言的跨平台特性是由JVM实现的。

类加载机制

  • Java程序运行机制:
    • 先在IDE上编写源代码(.java)
    • 之后编译器会将源代码编译成字节码文件 (.class)
    • 然后类加载器再把字节码加载到内存中,将其放在运行时数据区的方法区内
    • 由执行引擎将字节码翻译成底层系统指令,再交由CPU去执行
  • 这里面重点关注的是类加载的过程,JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。
  • 双亲委派模型:其实就是一种类加载器的层次关系。
    • 概念:当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
    • 好处:不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载 器最终得到的都是同样一个 Object 对象。
      在这里插入图片描述

JVM内存结构

  • JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
  • 以下重点研究内存区域,JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区 域【JAVA 堆、方法区】、直接内存。
    在这里插入图片描述
  • 程序计数器(线程私有) :当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令。
  • 虚拟机栈(线程私有) :是描述 java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈。
    中入栈到出栈的过程。
  • 本地方法区(线程私有) :与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
  • (线程共享):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例和数据都在这里分配内存,也是垃圾收集器进行 垃圾收集的最重要的内存区域。由于现代 JVM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为: 新生代和老年代。
  • 方法区(线程共享):即我们常说的永久代, 用于存储被 JVM 加载的类信息、常量、静 态变量、即时编译器编译后的代码等数据.

Java8做了什么修改:在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间 的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存 。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory, 字符串池和类的静态变量放入java 堆中, 这 样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。

垃圾回收

java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题。

  • Java垃圾回收机制:在java中,程序员是不需要显式的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
  • GC:GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
  • GC要做的三件事:
    • 那些内存需要回收?
    • 什么时候回收?
    • 怎么回收?

如何确定垃圾

  • 引用计数法:在Java 中,如果要操作对象则必须用引用进行,每个对象有一个引用计数器,当对象被引用一次计数器加1,当对象引用失效一次计数器减1,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。

    • 缺点:两个对象相互引用,如果不存在其他对象对它们的引用,两个对象计数也仍然为1,所占用的内存永远无法被回收(即不可达),这遍产生了内存泄漏。
  • 可达性算法:为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索,利用图论知识,判断如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。

  • 应用类型

    • 强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
    • 软引用:如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
    • 弱引用:有用但不是必须的对象,在下一次 GC 时会被回收。
    • 虚引用(幽灵引用/幻影引用):个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
    // 强引用
    String strongReference = new String("abc");
    // 软引用
    String str = new String("abc");
    SoftReference<String> softReference = new SoftReference<String>(str);
    // 弱引用
    WeakReference<String> weakReference = new WeakReference<>(str);
     

在这里插入图片描述
https://blog.csdn.net/baidu_22254181/article/details/82555485
https://www.zhihu.com/question/21539353

垃圾回收算法

  • 分代收集算法:分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活周期的不同将内存划分为不同的区域,进而选择适合的方法。一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
    • 分代模型
      • 新生代:是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
      • 老年代:主要存放应用程序中生命周期长的内存对象。
      • 永久区(已经被移除)
    • 内存分配策略
      • 对象优先在 Eden 区分配:多数情况,对象都在新生代 (Eden 区)分配。 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
      • 大对象直接进入老年代 :所谓大对象是指需要大量连续内存空间的对象
      • 长期存活对象将进入老年代:虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。
      • Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕死,所有 Minor GC 非常频繁,一般回收速度也非常快;
    • Full GC:Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。下边看看有那种情况会导致JVM进行Full GC及解决办法。
      • System.gc()方法的调用 :建议JVM进行Full GC,很多情况下它会触发 Full GC,建议能不使用此方法就别使用,让虚拟机自己去管理它的内存。
      • 老年代空间不足:为避免以上次情况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
      • 堆中分配很大的对象,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。

https://blog.csdn.net/tianya3530/article/details/88309852
JMM内存模型

  • 标记-清除算法:标记无用对象,然后进行清除回收,基础算法,其他垃圾收集算法都是在此算法的基础上进行改进的。
    • 优点:实现简单,不需要对象进行移动。
    • 缺点:标记、清除过程效率低,产生大量不连续的内存碎片,后续可能发生大对象不能找到可 利用空间的问题。
      在这里插入图片描述
  • 复制算法:为了解决标记-清除算法的效率不高的问题,复制算法按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的可回收内存空间一次清理掉。
    • 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
    • 缺点:内存使用率不高,只有原来的一半。
      在这里插入图片描述
  • 在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。
  • 标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片,因此就出现了一种标记-整理算法。
  • 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
    • 优点:解决了标记-清理算法存在的内存碎片问题。
    • 缺点:仍需要进行局部对象移动,一定程度上降低了效率。
      在这里插入图片描述

垃圾收集器

Java 堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除垃圾回收算法; 年老代主要使用标记-整理垃圾回收算法,因此 java 虚拟中针对新生代和年老代分别提供了多种不 同的垃圾收集器,JDK1.6 中Sun HotSpot 虚拟机的垃圾收集器如下:
在这里插入图片描述

  • 串行收集器
    • Serial收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
    • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • 并行收集器
    • ParNew(复制算法):新生代收并行收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
    • Parallel Old 收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
    • Parallel Scavenge(复制算法):Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃 圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码 的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)), 高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而 不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。
  • 并发收集器 CMS(标记-清除算法):Concurrent Mark Sweep 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
    • 工作流程:初始标记(需停顿)—并发标记—重新标记(需停顿)—并发清除
    • 为什么会有停顿(Stop The World):所有收集器都不可避免停顿,停顿目的,终止所有应用线程, 只有这样,系统才不会有新的垃圾产生。
    • 为何低停顿:由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
  • G1收集器(Garbage First收集器,标记-整理算法 ):G1可以说是CMS的终极改进版, Java堆并行收集器,G1收集器是JDK1.7提供的一个整堆回收器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
    • 高效益优先。G1会预测垃圾回收的停顿时间,原理是计算老年代对象的效益率,优先回收最大效益的对象。 G1则是把内存分为多个大小相同的区域Region,每个Region拥有各自的分代属性,但这些分代不需要连续。
    • 执行流程:初始标记(STW)—并发执行—最终标记(STW)—筛选回收(STW)
    • 堆内存结构的不同。以前的收集器分代是划分新生代、老年代、持久代等。
    • G1和CMS收集器的区别:用的标记-整理算法
    • G1收集器对CMS的改进:解决了CMS内存碎片、更多的内存空间登问题
  • ZGC:在JDK 11当中,加入了实验性质的ZGC。它的回收耗时平均不到2毫秒。它是一款低停顿高并发的收集器。ZGC几乎在所有地方并发执行的,除了初始标记的是STW的。所以停顿时间几乎就耗费在初始标记上,这部分的实际是非常少的。那么其他阶段是怎么做到可以并发执行的呢?
    • 原理:着色指针,读屏障
    • 思想:与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World。

https://www.cnblogs.com/datiangou/p/10245874.html

JVM调优

GC机制虽好,但问题是目前JDK的垃圾回收算法,始终无法解决垃圾回收时的暂停STW问题,因为这个暂停严重影响了程序的相应时间,造成拥塞或堆积。这也是后续JDK增加G1算法以及ZGC的一个重要原因。

  • 因此调优的目的就是:减少GC以减少代码停顿

  • 调优方法:主要还是从代码角度进行调优,然后才是JVM参数调优,JVM参数在一开始就应该设置好。

    • 代码角度:缩短对象生命期,尤其是大对象;减少对象产生的数量。
    • JVM参数调节: 优化JVM参数以减少YGC/FGC次数。降低移动到老年代的对象数量,缩短Full GC执行时间(stop-the-world持续的时间)。具体调优措施如下(按照重要性由高到低排序):
      • 减少对象产生的数量,尤其是大对象:GC产生的原因是heap内对象过多超过一定的大小导致。控制住对象的数量、大小,就控制住了GC的源头,从而保证了GC的性能。比如尽量少使用String,换用StringBuilder或StringBuffer
      • 选择合适的GC收集器:不同的GC收集器适用不同的场景,例如CMS是并发与并行的收集器,但是当资源不够用的时候,线程间切换会有开销,倒没有serial 单线程收集的快。
      • 调整新生代空间大小:在Oracle JVM中除了JDK 7及最高版本中引入的G1 GC外,其他的GC都是基于分代回收的。也就是对象会在Eden区中创建,然后不断在Survivor中来回移动。之后如果该对象依然存活年龄一般超过默认值15时,就会被移到老年代中。还有些对象,因为占用空间太大以致于创建时直接在老年代。老年代的GC较新生代会耗时更长,因此减少移动到老年代的对象数量可以降低full GC的频率。
      • 调整老年代空间大小:通过缩小老年代空间的方式来降低Full GC执行时间,可能会面临OutOfMemoryError或者带来更频繁的Full GC。通过增加老年代空间来减少Full GC执行次数,单次Full GC耗时将会增加。
  • JVM调优工具

    • jmap命令查看堆内存分配和使用情况
    • Top命令监控结果:top命令经常用来监控linux的系统状况,是常用的性能分析工具,能够实时显示系统中各个进程的资源占用情况
      • 通过使用top 命令进行持续监控发现此时CPU 空闲比例为85.7%,剩余物理内存为3619M,虚拟内存8G 未使用。持续的监控结果显示进程29003 占用系统内存不断在增加,已经快得到最大值。
        在这里插入图片描述
    • jstat:对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控。
      • 用jstat命令对PID为29003的进程进行gc回收情况检查,发现由于Old段的内存使用量已经超过了设定的80%的警戒线,导致系统每隔一两秒就进行一次FGC,FGC的次数明显多余YGC的次数,但是每次FGC后old的内存占用比例却没有明显变化—系统尝试进行FGC也不能有效地回收这部分对象所占内存。同时也说明年轻代的参数配置可能有问题,导致大部分对象都不得不放到老年代来进行FGC操作,这个或许跟系统配置的会话失效时间过长有关。
        在这里插入图片描述
  • Jstack打印出的堆栈内容

  • 具体的性能调优步骤

https://blog.csdn.net/lnazj/article/details/98167453
https://www.cnblogs.com/beyondcj/p/6273487.html#_Toc264315613

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值