【JAVA基础——JAVA虚拟机JVM】_java jvm-程序员宅基地

技术标签: JAVA  jvm  java  

JVM



JVM (Java Virtual Machine) JAVA虚拟机. 由堆、栈、方法区所组成,其中栈内存是给线程用的.
每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存.
每个线程程只能有一个活动栈帧,对应着当前正在执行的那个方法.

一.JVM结构

1.1.JVM包含两个子系统和两个组件

  • 两个子系统为 类装载子系统 ClassLoader,执行引擎子系统 Execution engine;
    • 类加载子系统:包含类加载器;根据给定的全限定类名装在class文件到运行时数据区的方法区;
    • 执行引擎:包含即时编译器(JITCompiler)和垃圾回收器(Garbage Collector);执行class文件中的命令;
  • 两个组件为 运行时数据区 Runtime data Area,本地接口 Native Interface;
    • 本地接口:与本地方法库交互,与其他变成语言交互的接口;
    • 运行时数据区域: 是jvm的内存;包含方法区,虚拟机栈,本地方法栈,堆,程序计数器;

JAVA7:
在这里插入图片描述

1.2.运行时数据区

1.2.1.简介
  • 程序计数器(Program Counter Register)
    • 线程私有.
    • 当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变程序计数器的值,来选取下一条需要执行的字节码指令,
    • 分支、循环、跳转、异常处理、线程恢复等基础功能,都依赖程序计数器来完成;线程是不具备记忆功能,需要程序计数器.
  • 本地方法栈(Native Method Stack)
    • 线程私有.
    • C所编写的Native方法相关.本地方法栈是为 虚拟机调用 Native本地方法 服务.
  • Java虚拟机栈(Java Virtual Machine Stacks)
    • 线程共享.
    • 虚拟机栈描述的是 Java方法执行 的内存模型:
    • 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储 局部变量表(基本类型,对象引用,和 returnAddress)、操作数栈、动态链接、方法出口等信息。
    • 每一个方法调用直至执行完的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • Java堆(Java Heap)
    • 线程共享.
    • 虚拟机启动时创建.JVM 中内存最大的一块,被所有线程共享,java堆 唯一目的就是存放对象实例,'几乎’所有的对象实例都在这里分配内存;
    • 堆空间是垃圾收集器管理的主要区域.
  • 方法区(Method Area)java8-
    • 线程共享,非堆内存.
    • 用于存储已被虚拟机加载的类信息、常量、静态变量、JIT即时编译后的代码等数据。在Java8之后被更改为元数据空间.
    • (JAVA7-)在方法区中有一个叫’运行时常量池’的区域,主要用来存放编译器生成的各种字面量和符号引用,在类加载完成后载入到运行时常量池中,以便后续使用。
    • JAVA7时已从方法区转移到堆内存,为了java8移除永久代做准备.

程序计数器,java虚拟机栈 为线程私有;
本地方法栈,Java堆,方法区 为线程共享;

在这里插入图片描述

1.2.2.程序计数器
  1. 程序计数器是一块较小的内存空间,可以看作:保存当前线程所正在执行的字节码指令的地址(行号)
  2. 程序计数器线程私有, Java虚拟机的多线程是通过 线程轮流切换 并分配处理器执行时间的方式 来实现的,同一时刻一个处理器都只会执行一条线程中的指令。
    因此,为了线程切换后能恢复到正确地执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。
    程序计数器内存区域是虚拟机中唯一没有规定 OutOfMemoryError 情况的区域。
1.2.3.虚拟机栈
  1. Java虚拟机是线程私有的,生命周期和线程相同。
  2. 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时 都会创建一个栈帧 用于存储 局部变量表、操作数栈、动态链接、方法出口 等信息.

java虚拟机栈的单位为栈帧:

  1. 局部变量表:是用来存储临时8个基本数据类型、对象引用地址、returnAddress 类型。(returnAddress 中保存的是return后要执行的字节码的指令地址)
  2. 操作数栈:操作数栈就是用来操作的数据,例如代码中有个 i = 3*4,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
  3. 动态链接:方法中需要链接到别的方法中去(动态链接),存储链接的地方
  4. 方法出口:出口 正常就是return 不正常就是抛出异常

一个方法调用另一个方法,会创建很多栈帧吗?

如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧.

栈指向堆是什么意思?

栈中要使用成员变量时,栈中不会存储成员变量,只会存储一个应用地址

递归的调用自己会创建很多栈帧吗?

递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去.

1.2.4.堆

java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。java堆目的就是存放对象实例。
所有的对象实例以及数组都要在堆上分配。

java堆是垃圾收集器管理的主要区域,从内存回收角度来看java堆可分为:新生代和老年代。
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
无论哪个区域,存储的都是对象实例,进一步地划分都是为了 更好地回收内存,或者更快的分配内存。

根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。
当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。
如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

栈与堆的区别:

对比
物理地址 堆的物理地址分配对对象是不连续的。因此性能慢些。 在GC的时候也要考虑到不连续的分配,所以有各种算法。 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
内存分配 堆因为是不连续的,分配的内存是在运行期确认的,因此大小不固定。一般堆远远大于栈。 栈是连续的,分配的内存大小要在编译期就确认,大小是固定。
存放内容 堆存放 对象的实例和数组。更关注的是数据的存储 栈存放 局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
可见度 堆对于整个应用程序都是共享、可见的。 栈只对于线程是可见的。线程私有。生命周期和线程相同。
1.2.5.本地方法栈

线程私有的.

  1. 用于执行本地方法,这些方法是使用其他语言编写的,并且与Java程序进行交互。本地方法栈中的帧用于保存本地方法的执行上下文和局部变量信息.
  2. 本地方法栈提供了与本地库(Native Library)的连接,使得Java程序能够调用本地库中的函数和方法。
1.2.6.方法区(永久代实现)java8-
  1. 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码 等数据。
  2. 别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出 OutOfMemoryError 异常。
  3. java8版本永久代已移除,使用元空间实现方法区

运行时常量池:

  • 1.7- 运行时常量池在永久代中(HotSpot虚拟机对方法区的实现);
  • 1.7+ 常量池已经从永久代移动到堆内存中。仍然存在永久代,但从这个版本开始,JVM逐渐将永久代的功能移动到堆内存中,包括运行时常量池的位置。
  • 1.8+ 常量池依旧在堆内存中.移除永久代,由元空间代替永久代(本地内存).
1.2.7.元空间(Metaspace)

在Java8开始取代了永久代的一种内存区域。与永久代不同,元空间不使用Java虚拟机堆内存,而是使用本地内存来存储类的元数据信息。

  • 存储类的元数据:元空间用于存储加载的类的元数据信息,包括类的结构信息、字段信息、方法信息、注解、字节码等。这些元数据信息在程序运行时被JVM使用。
  • 动态大小调整:与永久代不同,元空间的大小不再受到默认固定大小的限制,可以根据需要进行动态调整。元空间的大小受限于系统的可用本地内存大小,可以通过设置JVM参数来限制元空间的最大大小。
  • 自动回收和垃圾回收:由于元空间存储的是类的元数据信息而不是对象实例,所以不再需要像永久代进行垃圾回收。元空间的自动回收主要发生在类加载和卸载过程中,当某个类不再被引用或者无法被访问时,相关的元数据将会被卸载。
  • 类型信息的存储方式:元空间使用了一种新的机制来存储类的类型信息,即虚拟机中的Class对象被替换为一种叫作Klass Metadata(Klass元数据)的结构。Klass元数据是在运行时根据类的加载和转换而动态生成的,它包含了与类相关的信息,并被存储在元空间中。
  • 元空间的内存管理:元空间的内存管理由操作系统进行控制,不再依赖于Java虚拟机的垃圾回收机制。元空间的分配和释放是基于本地内存的管理操作,可通过操作系统提供的API进行管理。
1.2.8.JVM字节码执行引擎

执行引擎,负责执行虚拟机的字节码,一般先进行编译成机器码后执行。
“虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要 JVM字节码执行引擎编译成机器码后才可在物理机上执行。

1.2.9.直接内存(Direct Memory)

直接内存是基于物理内存和Java虚拟机内存的中间内存,能在一些场景中显著提高性能。
直接内存不受Java堆大小限制,它的分配和释放不依赖于JVM的垃圾回收机制,而是通过操作系统提供的本地内存管理函数进行操作。
直接内存是通过操作系统的本地内存管理函数(如malloc()、free()等)来进行分配和释放的,不需要经过JVM的对象分配和垃圾回收机制。

在JDK1.4中引入了NIO(New Input/Output)类,一种基于通道(Chanel)与缓冲区(Buffer)的I/O方式,NIO提供了一套非阻塞式的I/O操作方式,使用直接内存可以提高I/O操作的效率和性能。
可以使用 Native函数库直接分配堆外内存,然后通过一个存储在 Java 中的 DirectByteBuffer 对象作为对这块内存的引用进行操作。

1.2.10.垃圾收集系统

负责自动管理内存的组成部分。帮助Java程序管理内存,对于垃圾对象的清除、存活对象的管理以及内存碎片的回收等工作,都交由GC系统负责。

二.垃圾回收

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

程序在运行过程中,会产生大量的内存垃圾.为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行自动的垃圾回收(GC)。

在java中,不需要显式的去释放一个对象的内存的,而是由虚拟机自行执行。
JVM中的垃圾回收线程,是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,

执行时扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

2.1.GC

  • Minor GC(YoungGC) 清理整个新生代的过程,eden,S0\S1都会在空间不足时,触发minorGC的清理.
    因为Java对象大多都是朝生夕死,Minor GC非常频繁,一般回收速度也非常快.
  • Major GC(Full GC)老年代 区内存不足,触发Major GC(Major GC通常是跟full GC是等价的).
    出现了Major GC通常会伴随至少一次Minor GC。Major GC的速度通常会比Minor GC慢10倍以上。
  • Mixed GC 混合GC,覆盖整个新生代空间及部分年老代空间的GC.
    目前只有G1存在该行为,其他收集器均不支持.

full gc触发时机:

  1. 每次晋升到老年代的对象平均大小 > 老年代剩余空间
  2. MinorGC后存活的对象超过了老年代剩余空间(除CMS收集器)
  3. 元空间空间不足
  4. 执行System.gc()
  5. CMS标记清除收集器 GC异常
  6. 堆内存分配很大的对象
  7. 晋升失败promotion failed (年轻代晋升失败,比如eden区的存活对象晋升到幸存者区放不下,又尝试直接晋升到老年区又放不下,那么晋升失败,会触发 FullGC)

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。
如果大于则进行 Minor GC,如果小于则看 HandlePromotionFailure 设置是否允许担保失败(不允许则直接Full GC)。
如果允许担保失败,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,
如果大于则尝试 Minor GC(如果尝试失败也会触发Full GC),如果小于则进行 Full GC。

2.2.内存分配规则

  • 对象优先在Eden区分配
    多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空间进行 分配时,虚拟机将会发起一次 Minor GC。
    如果本次GC后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
  • 大对象直接进入老年代
    -XX:PretenureSizeThreshold 大于此值得对象直接分配在老年代(只对Serial和ParNew两款收集器有效),以B为单位,1kb为1024
    大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发GC以获取足够的连续空间来安置新对象。
    前面我们介绍过新生代使用的是标记-清除算法来处理垃圾回收的,如果大对象直接在新生代分配就会导致Eden区和两个Survivor区之间发生大量的内存复制。
    因此对于大对象都会直接在老年代进行分配。
    (考虑ParNew加CMS的收集器组合)
  • 长期存活对象将进入老年代
    数 -XX:MaxTenuringThreshold 晋升老年代阈值
    虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在Eden区出生,并且能够被Survivor容纳,将被移动到Survivor空间中,
    这时设置对象年龄为1。对象在Survivor区中每熬过一次Minor GC年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代。
  • 动态对象年龄判定
    虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代.如果在Survivor空间中某年龄所有对象大小的总和大于Survivor空间的一半,
    年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。
  • 空间分配担保
    在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果老年代最大可用的连续空间大于新生代所有对象总空间,那么Minor GC可以确保是安全的。
    如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。
    • 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,
      • 如果大于,将尝试着进行一次有风险的Minor GC.
      • 如果小于改为进行一次Full GC。
    • 允许担保失败,进行一次Full GC。

2.3.新生代,老年代,永久代,元空间

在这里插入图片描述

2.3.1.分区

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。

新生代 ( Young )被划分为三个区域:Eden、From Survivor、To Survivor。目的是为了使 JVM 能够 更好的管理堆内存中的对象,包括内存的分配以及回收。
新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批的对象死去,只有少量的对象存活,便采用了 复制算法 ,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采 用 “标记-清理”或者“标记-整理” 算法。

Java8- 永久代就是JVM的方法区。放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收。
Java8中已经移除了永久代,新加了一个叫做空间的本地内存区.

2.3.2.比例

新生代:堆1/3 老年代:堆2/3 通过参数 –XX:NewRatio=2来指定
eden:新生代8/10 survivor:新生代1/10 通过参数 –XX:SurvivorRatio=8来设定

2.3.3.原因

为什么要这样分代?
其实主要原因就是可以根据各个年代的特点进行对象分区存储,更便于回收,采用最适当的收集算法:
新生代中,每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法。

新生代又分为Eden和Survivor (From与To)两个区。加上老年代就这三个区。
数据会首先分配到Eden区当中(特殊情况,如果是大对象(大于PretenureSizeThreshold阈值)那么会直接放入到老年代(大对象是指需要大量连续内存空间的java对象)。

当Eden没有足够空间的时候就会触发jvm发起一次Minor GC。
如果对象经过一次Minor-GC还存活,并且又能被Survivor空间接受,那么将被移动到Survivor空间当中。并将其年龄设为1,对象在Survivor每熬过一次Minor GC,年龄就加1,
当年龄达到一定的程度(默认15)时,就会被晋升到老年代中了,-XX:MaxTenuringThreshold=15,设置晋升年龄.

为什么新生代要分Eden和两个 Survivor 区域?

  1. 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC.
    老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor。
  2. Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代。
  3. 设置两个Survivor区最大的好处就是解决了碎片化
    刚刚新建的对象在Eden中,经历一次MinorGC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;
    等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor spaceS1.
    (这种复制算法保证了S1中 来自S0和Eden两部分的存活对象 占用连续的内存空间,避免了碎片化的发生)

元空间metaSpace替换永久代perm(方法区移至Metaspace,字符串常量池移至Java Heap)

  1. 字符串常量池存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

2.4.垃圾回收算法

  1. 标记-清除算法:标记无用对象,然后进行清除回收。
    a,标记(使用可达性分析算法;不使用引用计数法,存在循环引用);
    b,回收;
    • 缺点:效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
  2. 复制算法:按照容量划分两个大小相等的内存区域,每次只使用其中一个区域.当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
    • 缺点:内存使用率不高,只有原来的一半。对象存活率高时会频繁进行复制。
    • 实现:年轻代分为一个Eden和两个Survivor区,Eden与Survivor比例为 8:1:1,当其中eden和正在使用的survivor满时,发生gc,将存活对象复制到另一个幸存者区中.
  3. 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
    • 优点:解决了标记-清理算法存在的内存碎片问题。
    • 缺点:仍需要进行局部对象移动,一定程度上降低了效率.
  4. 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。(永久代为方法区)
    分代回收器有两个分区:老年代和新生代,新生代默认的空间占比总空间的 1/3,老年代的默认占比是 2/3。
    新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1.
    • 执行流程如下:
      • 把Eden + From Survivor 存活的对象放入 To Survivor 区;
      • 清空 Eden 和 From Survivor 分区;
      • From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
      • 每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年 龄到达15(默认配置)时,升级为老生代。大对象也会直接进入老生代。
      • 老年代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。
      • 以上循环往复就构成了整个分代垃圾回收的整体执行流程。

2.5.垃圾收集器

垃圾收集器是垃圾回收算法(标记清除法、标记整理法、复制算法、分代算法)的具体实现,不同垃圾收集器、不同版本的JVM所提供的垃圾收集器可能会有很在差别。

  • 年轻代 Serial,Parallel Scavenge,PraNew
  • 老年代 Serial Old、Parallel Old、CMS
  • 堆(包括老年代和年轻代) G1

收集器分为分代收集器和分区收集器:
分代收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old
分区收集器:G1、ZGC(java11)、Shenandoah(java12)

收集器间搭配:
Serial可搭配: Serial Old、CMS
Parallel Scavenge可搭配: Serial Old、Parallel Old
PraNew可搭配: Serial Old、CMS

  • Serial 收集器(复制算法): 新生代单线程收集器.优点:简单高效. 适合单线程环境和对暂停时间要求不高的应用场景。

  • ParNew 收集器 (复制算法): 新生代收并行收集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现.用于搭配CMS的新生代收集器.

  • Parallel Scavenge 收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用CPU。
    吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),
    高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互响应 要求不高的场景.

  • Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

  • Parallel Old 收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

  • CMS(Concurrent Mark Sweep 并发标记清除回收器)收集器:
    老年代并发收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
    以牺牲吞吐量为代价来获得 最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,非常适合。
    在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用CMS垃圾回收器。
    CMS 使用的是标记-清除的算法实现的,所以在gc的时候会产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,
    临时CMS会采用 Serial Old 回收器进行垃圾清除(标记整理算法),此时的性能将会被降低。
    CMS收集器有单独收集年老代空间的行为.(其他收集器发生老年代GC时,年轻代GC会一起发生)

    • 回收过程:
      1. 初始标记:仅标记GcRoot节点直接关联的对象,该阶段速度会很快,需在STW中进行。
      2. 并发标记:该阶段主要是做GC溯源工作(GcTracing),从根节点出发,对整个堆空间进行可达性分析,找出所有存活对象,该阶段的GC线程会与用户线程同时执行。
      3. 重新标记:这个阶段主要是为了修正“并发标记”阶段由于用户线程执行造成的GC标记变动的那部分对象,该阶段需要在STW中执行,并且该阶段的停顿时间会比初始阶段要长不少。
      4. 并发清除:在该阶段主要是对存活对象之外的垃圾对象进行清除,该阶段不需要停止用户线程,是并发执行的。
  • G1(Garbage First)收集器 (标记-整理算法):
    Java堆 并发 分区回收 收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,不会 产生内存碎片。
    在不牺牲吞吐量前提下,实现低停顿垃圾回收。
    此外,G1收集器回收的范围是整个Java堆(包括新生代,老年代)

    • 特性:
      • 并发收集,与用户线程同时执行
      • 标记整理,不会产生内存碎片
      • GC时,停顿时间可控,尽可能会保证高吞吐量。
      • 对于堆的未使用内存可以返还给操作系统。JDK12

    JAVA9时变为默认使用的收集器.
    堆中的内存区域被划为了一个个Region区。Region区的默认数量限制为2048个.每个区大小为堆空间大小/2048.(不推荐用XX:G1HeapRegionSize指定)
    每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。运行时,每个分区都会被打上唯一的分区标识。

    JVM不需要再为堆空间分配连续的内存,堆空间可以是不连续物理内存来组成Region的集合.
    有的区域垃圾对象少,有的垃圾对象多,G1优先回收垃圾对象多的区域.
    -XX:G1NewSizePercent 设置新生代初始占比(默认5)
    -XX:G1MaxNewSizePercent 设置新生代最大占比(默认60)
    新生代中的Eden区和Survivor区对应的Region区比例默认8:1:1.

    G1中的年老代晋升条件和之前的相同,达到年龄阈值的对象会被转入年老代的Region区中.
    对于大对象的分配,在G1中不会让大对象进入年老代,在G1中由专门存放大对象的Region区叫做 Humongous 区,
    如果在分配对象时,判定出一个对象属于大对象,那么则会直接将其放入Humongous区存储。(超过单个普通Region区的50%为大对象,单个Humongous区存不下时,可能会横跨多个Region区存储)
    可以避免一些生命周期短的大对象直接进入年老代,节约年老代的内存空间,可以有效避免年老代因空间不足时的GC开销。
    FullGC时,也会对Humongous区进行回收。

    • YoungGC:
      在G1中,当Eden域被用完时,G1首先会计算回收当前的新生代空间需要花费的时间,如果回收时间远远小于参数-XX:MaxGCPauseMills 值(默认200ms),那么不会触发YoungGC,
      而是会继续为新生代增加新的Region区用于存放新分配的对象实例。
      直至某次Eden区空间再次被放满并经过计算后,此次回收的耗时接近-XX:MaxGCPauseMills参数设定的值,才触发YoungGC。
      YoungGC被触发时,首先会将目标Region区中的存活对象移动(多线程并行复制)至幸存区空间(Survivor-from标签的区域).达到晋升年龄标准的对象也会被移入至年老代区中存储.
      G1内部做了优化,一旦发现没有引用指向巨型对象,则可直接在年轻代收集周期中被回收。

    • MixedGC:
      当整个堆中年老代的区域占有率达到参数 -XX:InitiatingHeapOccupancyPercent(默认45) 设定的值后触发MixedGC.
      触发时会回收所有新生代区和部分年老代区(根据期望的GC停顿时间选择合适的年老代Region区优先回收)以及大对象Humongous区.

    • FullGC:
      当 G1 无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式单线程的 Full GC,Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。

      • 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
      • 从老年代分区转移存活对象时,无法找到可用的空闲分区
      • 分配巨型对象时在老年代无法找到足够的连续分区
    • MixedGC 回收过程:

      1. 初始标记(InitialMark):先触发STW,然后使用单条GC线程快速标记GCRoots直连的对象。
      2. 并发标记(ConcurrentMarking):与CMS的并发标记过程一致(三色标记算法),采用多条GC线程与用户线程共同执行,根据Root根节点标记所有对象。
      3. 最终标记(Remark):同CMS的重新标记阶段,主要是为了纠正并发标记阶段因用户操作导致的错标、误标、漏标对象。
      4. 筛选回收(Cleanup):先对各个Region区的回收价值和成本进行排序,找出「回收价值最大」的Region优先回收。
        根据用户指定的期望停顿时间(即-XX:MaxGCPauseMillis参数设定的值)选择「价值最大且最符合用户预期」的Region区进行回收.
    • 缺点:

      • 停顿时间过短时,导致每次回收的空间只占堆内存的小部分.回收速度跟不上分配速度时导致垃圾堆积.
      • 相比CMS更高的内存开销和处理开销:需要维护额外的数据结构来管理分区和跟踪对象的存活情况.以及GC过程中的标记阶段、内存整理等操作.
      • 在小内存的应用中可能不如CMS
    • 优点:

      • 相比CMS采用的标记清除算法,G1的标记整理不会产生内存碎片.
      • 在不牺牲吞吐量前提下,实现低停顿垃圾回收。(同时注重吞吐量和低延迟场景)
  • Epsilon(JDK11): 用于测试的无操作收集器,装配该款GC收集器的JVM,在运行期间不会发生任何GC相关的操作,程序所分配的堆空间一旦用完,Java程序就会因OOM原因退出。

  • ZGC(JDK11):
    ZGC主打的是超低延迟与吞吐量,ZGC也会在尽可能堆吞吐量影响不大的前提下,
    实现在任意堆内存大小下都可以把垃圾回收的停顿时间限制在10ms以内的低延迟。没有实现分代架构.
    ZGC的目的主要有如下四点:

    1. 奠定未来GC特性的基础。
    2. 为了支持超大级别堆空间(TB级别),最高支持16TB。
    3. 在最糟糕的情况下,对吞吐量的影响也不会降低超过15%。
    4. GC触发产生的停顿时间不会偏差10ms。
  • ShenandoahGC(JDK12):追求极致低延迟.没有实现分代架构.
    ZGC是基于colored pointers染色指针实现的,而ShenandoahGC是基于brooks pointers转发指针实现。

2.6.判断对象是否可以被回收(标记算法)?

一般有两种方法来判断:

  • 引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器+1,引用被释放时计数-1,当计数器为0时就可以被回收。不能解决循环引用的问题!
  • 可达性分析算法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。涉及到的对象不能从GC Roots强引用可到达,垃圾回收器都会进行清理来释放内存。
    当一个对象到 GC Roots没有任何引用链相连时,则证明此对象是可以被回收的。
    GC Roots有:
    • 类,由系统类加载器加载的类。这些类从不会被卸载,可以通过静态属性的方式持有对象的引用。一般情况下由自定义的类加载器加载的类不能成为GC Roots.
    • 线程,存活的线程
    • Java方法栈中的局部变量或者参数
    • JNI方法栈中的局部变量或者参数
    • JNI全局引用
    • 用做同步监控的对象
    • 被JVM持有的对象,由于特殊的目的不被GC回收。可能是系统类加载器,重要的异常处理类,为处理异常预留的对象,正在执行类加载的自定义的类加载器等.

2.7.其他

java内存溢出

Java存在着内存泄漏的情况,导致内存泄露的原因:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,
尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。

System.gc()
public static void gc() {
    
    Runtime.getRuntime().gc();
}

-XX:+ DisableExplicitGC 禁用gc()方法.
ExplicitGCInvokesConcurrent 是G1垃圾回收器的一个JVM参数,用于在执行显式垃圾回收时并发执行部分清理操作。
当设置为true时,当应用程序显式调用System.gc()方法或通过JMX接口执行显式的垃圾回收请求时,
G1垃圾回收器将在执行垃圾回收的同时尽可能地启动并发标记和清理阶段。可以在显式垃圾回收请求期间减少停顿时间。

堆外内存常配合使用System GC
堆外内存主要针对java.nio.DirectByteBuffer,这些对象的创建过程会通过Unsafe接口直接通过os::malloc来分配内存,
然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。
这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old区,但是一直没有触发GC,物理内存可能被他们耗尽.
因此为了避免这种悲剧的发生,通过 -XX:MaxDirectMemorySize 来指定最大的堆外内存大小,
当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存.
显式调用System.gc垃圾回收并不能直接回收堆外内存,而是通过垃圾回收器清理无法访问到的DirectByteBuffer对象,并触发finalize()方法。
在finalize()方法中,可以手动释放堆外内存的资源,通常使用Unsafe接口的freeMemory()方法来释放内存。

三.内存分配

3.1.对象创建方式

  • new关键字 调用了构造函数
  • Class的 newInstance方法 调用了构造函数
  • Constructor类的 newInstance方法 调用了构造函数
  • clone方法 没有调用构造函数
  • 反序列化 没有调用构造函数

3.2.对象的分配

  • 对象优先在 Eden 区分配
    当 Eden 区分配没有足够的空间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存。
  • 大对象直接进入老年代
    需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续空间来安置新对象。
    如果大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制。因此对于大对象都会直接在老年代进行分配。
  • 长期存活对象将进入老年代
    虚拟机采用分代收集的思想来管理内存,内存回收时必须判断对象应该放在新生代或老年代。
    虚拟机给每个对象定义了一个对象年龄的计数器,如果对象在 Eden区出生,并且能够被 Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。
    对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代.(-XX:MaxTenuringThreshold=15,设置晋升年龄)

3.3.对象的内存布局

在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据和对齐填充.
在这里插入图片描述

3.3.1.对象头
3.3.1.1.对象标记(Mark Word)

用于存储对象自身的运行时数据,如哈希码(hashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
占用空间大小根据JVM决定,为JVM的一个字大小,也就是32位JVM中Mark Word占用4个字节,64位JVM中占用8个字节。

默认存储对象的HashCode、分代年龄和锁标志位等信息。
这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。

根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着’锁标志位’的变化而变化。
在这里插入图片描述

32位JVM
在这里插入图片描述

64位JVM
在这里插入图片描述

Epoch(时间戳):用于记录偏向锁的撤销条件,当其他线程尝试获取该对象的锁时,需要检查该时间戳是否与对象头中的时间戳匹配。
如果不匹配,则偏向锁会被撤销,对象将升级为轻量级锁或重量级锁。

3.3.1.2.类元信息(Class pointer类型指针)

对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(4字节)
在这里插入图片描述

3.3.2.实例数据

存放类的属性(Field)数据信息,包括父类的属性信息.
数组的实例部分还包括数组的长度.
这部分内存按4字节对齐。

3.3.3.对齐填充

虚拟机要求对象起始地址必须是8字节的整数倍。
填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。

3.4. 对象内存查看

依赖:

<!--堆内存 存储结构-->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

查看对象内存占用信息:

System.out.println(ClassLayout.parseInstance(object).toPrintable());

//省略getter,setter,constructor
public class ObjectHeadTest {
    
    static class LongObject {
    
        private Long num;
    }
    static class SimpleLongObject {
    
        private long num;
    }
    static class IntegerObject {
    
        private Integer num;
    }
    static class IntObject {
    
        private int num;
    }
    static class LongObjInObj {
    
        private LongObject num;
    }
    static class SimpleLongObjObjInObj {
    
        private SimpleLongObject num;
    }

    public static void main(String[] args) {
    
        LongObject longObject = new LongObject(1L);
        System.out.println("longObject = " + ClassLayout.parseInstance(longObject).toPrintable());
        LongObject nullLongObject = new LongObject();
        System.out.println("nullLongObject = " + ClassLayout.parseInstance(nullLongObject).toPrintable());

        SimpleLongObject simpleLongObject = new SimpleLongObject(1L);
        System.out.println("simpleLongObject = " + ClassLayout.parseInstance(simpleLongObject).toPrintable());
        SimpleLongObject nullSimpleLongObject = new SimpleLongObject();
        System.out.println("nullSimpleLongObject = " + ClassLayout.parseInstance(nullSimpleLongObject).toPrintable());

        IntegerObject integerObject = new IntegerObject(1);
        System.out.println("integerObject = " + ClassLayout.parseInstance(integerObject).toPrintable());
        IntObject intObject = new IntObject(1);
        System.out.println("intObject = " + ClassLayout.parseInstance(intObject).toPrintable());
        LongObjInObj longObjInObj = new LongObjInObj(longObject);
        System.out.println("longObjInObj = " + ClassLayout.parseInstance(longObjInObj).toPrintable());
        SimpleLongObjObjInObj simpleLongInObj = new SimpleLongObjObjInObj(simpleLongObject);
        System.out.println("simpleLongInObj = " + ClassLayout.parseInstance(simpleLongInObj).toPrintable());
    }
}

3.5.类加载的机制及过程

程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。

  1. 加载
    加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,
    并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
    类加载的最终产物就是位于堆中的 Class对象(不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口.
    Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。
    除此之外,可以通过继承 ClassLoader基类 来创建自己的类加载器。
    类加载器,可以从不同来源加载类的二进制数据,比如:本地 Class文件、Jar包 Class文件、网络Class文件 等。
  2. 连接过程
    连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。
    类连接可分为3个阶段:
    1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理;
    2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配;
    3. 解析:虚拟机常量池的符号引用替换为字节引用过程;
  3. 初始化(初始化是为类的静态变量赋予正确的初始值)
    初始化阶段是执行类构造器 () 方法的过程。
    类构造器 ()方法是Java编译器生成的字节码中出现的一个特殊方法。负责执行类的静态变量初始化和静态代码块中的代码.代码从上往下执行。
    当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化.
    虚拟机会保证一个类的 () 方法在多线程环境中被正确加锁和同步.

3.6.JVM加载Class文件的原理机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。
在写程序的时候,几乎不需要关心类的加载,因为这些都是隐式装载的,除非有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种:

  1. 隐式装载,程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类到jvm中.
  2. 显式装载,通过class.forName() 等方法,显式加载需要的类.

为了节省内存开销,Java类的加载是动态的,并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中.其他类,在需要的时候才加载。

3.7.类加载器定义与分类

实现通过类的全限定名获取类的二进制字节流的代码块叫做类加载器。
存在多种类加载器:

  • 分工,各自负责各自的区块
  • 为了实现委托模型

类加载器顺序:

  1. 启动类加载器(Bootstrap ClassLoader):用来加载java核心类库,无法被java程序直接引用。
  2. 扩展类加载器(extensions class loader):用来加载Java的扩展库。Java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader ):根据Java应用的类路径(CLASSPATH )来加载Java类。一般来说,Java应用的类都是由它来完成加载的。
    可以通过ClassLoader.getSystemClassLoader()获取。
  4. 自定义类加载器: 通过继承java.lang.ClassLoader 类的方式实现。

类装载步骤

3.8.自定义类加载器

自定义类加载器的应用场景:

  • 加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,
    类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
  • 从非标准的来源加载代码:如果字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。

综合运用:比如应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。
这个时候就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。

3.9.双亲委派模型:

双亲委派模型的工作过程:
一个类加载器收到了类加载的请求,不会先自己尝试去加载 这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,
这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

//双亲委派模型的工作过程源码
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    
    // 检查类是否已存在
    Class c = findLoadedClass(name);
    if (c == null) {
    
        try {
    
            if (parent != null) {
    
                c = parent.loadClass(name, false);
            } else {
    
                c = findBootstrapClassOrNull(name);
            }
        }
        catch (ClassNotFoundException e) {
    
        // 加载失败 抛出ClassNotFoundException thrown if class not found
        // from the non-null parent class loader
        //父类加载器无法完成类加载请求
        }
        if (c == null) {
    
            // If still not found, then invoke findClass in order to find the class
            //子加载器进行类加载
            c = findClass(name);
        }
    }
    if (resolve) {
    
        //判断是否需要链接过程,参数传入
        resolveClass(c);
    }
    return c;
}

好处:

  • 安全性,避免用户编写的类动态替换Java的核心类,比如 String。
  • 避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。

3.10.JVM新建对象

User user = new User();JVM做了哪些操作?

  1. 加载类信息:JVM将首先加载 User 类的字节码文件,并解析其结构。这包括验证字节码的正确性,并构建类的运行时数据结构.
  2. 分配对象内存:JVM将根据 User 类的定义,在堆上分配内存空间以创建一个新的对象。这个对象包含了类的实例变量和一些额外的管理信息。
  3. 初始化对象:JVM会调用 User 类的构造函数来初始化这个对象。构造函数会为实例变量设置初始值,执行其他必要的初始化代码。
  4. 引用赋值:将对象的引用存储在 user 变量中,使得可以通过该变量访问对象。

对象内存布局

3.11.Java引用类型

  • 强引用 发生gc的时候不会被回收。
  • 软引用 SoftReference:有用但不是必须的对象,在发生内存溢出之前会被回收。
  • 弱引用 WeakReference:有用但不是必须的对象,在下一次GC时会被回收。
  • 虚引用 PhantomReference:无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在gc时返回一个通知。

四.JVM调优

4.1.工具

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具。
jconsole:用于对JVM中的内存、线程和类等进行监控;
jvisualvm:JDK自带的全能分析工具,可以分析:内存快照、线程快照、程序 死锁、监控内存的变化、gc变化等。

4.2.调优参数

堆配置:

  • -Xms2g:初始化推大小为2g
  • -Xmx2g:堆最大内存为2g (为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值)
  • -XX:NewSize=m;设置年轻代大小
  • -XX:NewRatio=4:设置年轻代的和老年代的内存比例为 1:4; 年轻代和年老代将根据默认的比例(1:2)分配堆内存.
  • -XX:SurvivorRatio=8:设置新生代Eden和Survivor比例为 8:1

收集器配置:

  • 串行收集器

    • -XX:+UseSerialGC:设置串行收集器,只适用小数据量,一般不使用
  • 并行收集器(吞吐量优先)

    • -XX:+UseParallelGC:设置并行收集器,年轻代
    • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数,此值最好配置与处理器数目相同。
    • -XX:+UseParalledlOldGC:设置并行年老代收集器;
    • -XX:MaxGCPauseMillis=n:设置年轻代并行收集最大的暂停时间(如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
    • -XX:+UseAdaptiveSizePolicy:设置此选项以后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低响应时间或者收集频率等,
      此值建议使用并行收集器时,一直打开并发收集器(响应时间优先) ,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等
    • -XX:+UseConcMarkSweepGC:设置并发收集器;指定使用 CMS + Serial Old 垃圾回收器组合;
    • -XX:CMSFullGCsBeforeCompaction=n:由于并发收集器不对内存空间进行压缩、整理、所以运行一段时间以后会产生“碎片”,
      使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理
    • -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
    • –XX:+UseParNewGC:指定使用 ParNew 垃圾回收器
    • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
    • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况.
  • 辅助的GC典型配置参数:

    • -XX:+PrintGC:开启打印 gc 信息;
    • -XX:+PrintGCDetails:打印 gc 详细信息。
    • -XX:+PrintGCTimeStamps:用于输出GC时间戳(JVM启动到当前日期的总时长的时间戳形式)
      0.855: [GC (Allocation Failure) [PSYoungGen: 33280K->5118K(38400K)] 33280K->5663K(125952K), 0.0067629 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
    • -XX:+PrintGCDateStamps 用于输出GC时间戳(日期形式)
      2022-01-27T16:22:20.885+0800: 0.299: [GC pause (G1 Evacuation Pause) (young), 0.0036685 secs]
    • -XX:+PrintHeapAtGC 在进行GC前后打印出堆的信息。
    • -Xloggc:…/logs/gc.log:将日志输出到指定的文件中(已存在追加)
  • 推荐配置

    • 通过-XX:MaxRAMPercentage限制堆大小:
      //参数需JDK 8u191+、JDK 10及以上版本。
      /使用容器内存。允许JVM从主机读取cgroup限制,例如可用的CPURAM,并进行相应的配置。当容器超过内存限制时,会抛出OOM异常,而不是强制关闭容器。
      -XX:+UseContainerSupport
      //设置JVM使用容器内存的初始百分比。建议与-XX:MaxRAMPercentage保持一致,推荐设置为70.0。
      -XX:InitialRAMPercentage=70.0
      //设置JVM使用容器内存的最大百分比。由于存在系统组件开销,建议最大不超过75.0,推荐设置为70.0。
      -XX:MaxRAMPercentage=70.0
      //输出GC详细信息。
      -XX:+PrintGCDetails
      //输出GC时间戳。日期形式
      -XX:+PrintGCDateStamps
      //GC日志文件路径。需保证Log文件所在容器路径已存在. 
      -Xloggc:/home/admin/nas/gc-${
              POD_IP}-$(date '+%s').log
      //JVM发生OOM时,自动生成DUMP文件。
      -XX:+HeapDumpOnOutOfMemoryError
      //DUMP文件路径。需保证DUMP文件所在容器路径已存在. 
      -XX:HeapDumpPath=/home/admin/nas/dump-${
              POD_IP}-$(date '+%s').hprof  
      
    • 通过-Xms -Xmx限制堆大小:
      存在问题:
      • 当规格大小调整后,需要重新设置堆大小参数。
      • 当参数设置不合理时,会出现应用堆大小未达到阈值但容器OOM被强制关闭的情况。
      //设置JVM初始内存大小。建议与-Xmx相同,避免每次垃圾回收完成后JVM重新分配内存。 推荐百分之70左右内存大小.
      -Xms2048m 
      //设置JVM最大可用内存大小。为避免容器OOM,请为系统预留足够的内存大小。
      -Xmx2048m 
      -XX:+PrintGCDetails 
      -XX:+PrintGCDateStamps 
      -Xloggc:/home/admin/nas/gc-${
              POD_IP}-$(date '+%s').log 
      -XX:+HeapDumpOnOutOfMemoryError 
      -XX:HeapDumpPath=/home/admin/nas/dump-${
              POD_IP}-$(date '+%s').hprof
      

4.3.性能调优

  1. 线程池:解决用户响应时间长的问题
  2. 连接池
  3. JVM启动参数:调整各代的内存比例和垃圾回收算法,提高吞吐量
    目标: GC的时间足够的小;GC的次数足够的少;发生Full GC的周期足够的长;
    • 为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值
    • 年轻代和年老代将根据默认的比例(1:2)分配堆内存
    • 更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC;
      更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率;
    • 在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集
    • 线程堆栈的设置:每个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,
      -Xss 一般256K就足用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但实际上还受限于操作系统。
    • 可以通过下面的参数打印Heap Dump信息
      -XX:HeapDumpPath: 指定堆转储(JVM中对象的所有详细信息)文件的输出路径
      -XX:+PrintGCDetails 打印 gc 详细信息。
      -XX:+PrintGCTimeStamps 打印 gc 详细信息。
      -Xloggc:/usr/aaa/dump/heap_trace.txt
    • 通过下面参数可以控制 OutOfMemoryError 时打印堆的信息
      -XX:+HeapDumpOnOutOfMemoryError
    • 请看一下一个时间的Java参数配置:(服务器:Linux 64Bit,8Core×16G)
      JAVA_OPTS=“$JAVA_OPTS -server -Xms3G -Xmx3G -Xss256k -XX:PermSize=128m -XX:MaxPermSize=128m
      -XX:+UseParallelOldGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/aaa/dump
      -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/usr/aaa/dump/heap_trace.txt
      -XX:NewSize=1G -XX:MaxNewSize=1G”

4.4.程序算法:改进程序逻辑算法提高性能

五.启动参数与命令

5.1.设置参数方式

开发工具

  • IDEA 、Eclipse 在run configuration 里设置VM option
  • 运行jar包, java -XX:+UseG1GC xxx.jar

线上环境:

  • web容器:Tomcat, startup.sh -> catalina.sh(卡特琳娜) 里设置JVM 参数
  • jsp + jinfo 查看某个java进程的参数,然后再调整设置

真实调优:

  • java -XX:+UseG1GC xxx.jar

5.2.java -help 标准参数(不会随着JDK 变化而变化版本的参数)

  • -d32 使用 32 位数据模型 (如果可用)
  • -d64 使用 64 位数据模型 (如果可用)
  • -server 选择 “server” VM ;默认 VM 是 server.
  • -cp <目录和 zip/jar 文件的类搜索路径>
  • -classpath <目录和 zip/jar 文件的类搜索路径> 用 ; 分隔的目录, JAR 档案 和 ZIP 档案列表, 用于搜索类文件。
  • -D<名称>=<值> 设置系统属性 可用System.getProperty(“property”)获取
  • -verbose:[class|gc|jni] 启用详细输出
  • -version 输出产品版本并退出
  • -showversion 输出产品版本并继续
  • -? -help 输出此帮助消息
  • -X 输出非标准选项的帮助
  • -enableassertions[:…|:]
  • -ea[:…|:] 按指定的粒度启用断言
  • -disableassertions[:…|:]
  • -da[:…|:] 禁用具有指定粒度的断言
  • -esa | -enablesystemassertions 启用系统断言
  • -dsa | -disablesystemassertions 禁用系统断言
  • -agentlib:[=<选项>] 加载本机代理库 , 例如 -agentlib:hprof 另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help
  • -agentpath:[=<选项>] 按完整路径名加载本机代理库
  • -javaagent:[=<选项>] 加载 Java 编程语言代理, 请参阅 java.lang.instrument
  • -splash: 使用指定的图像显示启动屏幕

5.3.java -X 非标准参数 (java -X命令,能够获得当前JVM支持的所有非标准参数列表)

  • -Xmixed 混合模式执行 (默认)
  • -Xint 仅解释模式执行
  • -Xbootclasspath: <用 ; 分隔的目录和 zip/jar 文件> 设置搜索路径以引导类和资源
  • -Xbootclasspath/a: <用 ; 分隔的目录和 zip/jar 文件> 附加在引导类路径末尾
  • -Xbootclasspath/p: <用 ; 分隔的目录和 zip/jar 文件> 置于引导类路径之前
  • -Xdiag 显示附加诊断消息
  • -Xnoclassgc 禁用类垃圾收集
  • -Xincgc 启用增量垃圾收集
  • -Xloggc: 将 GC 状态记录在文件中 (带时间戳)
  • -Xbatch 禁用后台编译
  • -Xprof 输出 cpu 配置文件数据
  • -Xfuture 启用最严格的检查, 预期将来的默认值
  • -Xrs 减少 Java/VM 对操作系统信号的使用
  • -Xcheck:jni 对 JNI 函数执行其他检查
  • -Xshare:off 不尝试使用共享类数据
  • -Xshare:auto 在可能的情况下使用共享类数据 (默认)
  • -Xshare:on 要求使用共享类数据, 否则将失败。
  • -XshowSettings 显示所有设置并继续
  • -XshowSettings:all 显示所有设置并继续
  • -XshowSettings:vm 显示所有与 vm 相关的设置并继续
  • -XshowSettings:properties 显示所有属性设置并继续
  • -XshowSettings:locale 显示所有与区域设置相关的设置并继续

5.4.java -XX 非固定参数

使用方式:

  • -XX:+ 启用选项
  • -XX:- 不启用选项
  • -XX:= 给选项设置一个数字类型值,可跟单位,例如 32k, 1024m, 2g
  • -XX:= 给选项设置一个字符串值,例如-XX:HeapDumpPath=./dump.core

行为参数(功能开关):

  • -XX:-UseSerialGC 启用串行GC
  • -XX:-UseParallelGC 启用并行GC
  • -XX:GCTimeRatio=99 设置用户执行时间占总时间的比例(默认值99,即1%的时间用于GC)
  • -XX:MaxGCPauseMillis=time 设置GC的最大停顿时间(只对Parallel Scavenge有效)
  • -XX:+UseParNewGC 使用ParNew+Serial Old收集器组合
  • -XX:ParallelGCThreads 设置执行内存回收的线程数,在 +UseParNewGC 的情况下使用
  • -XX:-UseParallelOldGC 对Full GC启用并行,当-XX:-UseParallelGC 启用时该项自动启用,使用Parallel Scavenge +Parallel Old组合收集器
  • -XX:-UseConcMarkSweepGC 对老生代采用标记清除交换算法进行GC CMS(Concurrent Mark Sweep)收集器(标记-清除算法)
  • -XX:+ScavengeBeforeFullGC 新生代GC优先于Full GC执行
  • -XX:-DisableExplicitGC 禁止调用System.gc();但jvm的gc仍然有效
  • -XX:+MaxFDLimit 最大化文件描述符的数量限制
  • -XX:+UseGCOverheadLimit 在抛出OOM之前限制jvm耗费在GC上的时间比例
  • -XX:+UseThreadPriorities 启用本地线程优先级
  • -XX:AutoBoxCacheMax 缓存最大值,默认为127 (Integer默认缓存 -128~127)

性能调优:

  • -Xms 设置初始 Java 堆大小
  • -Xmx 设置最大 Java 堆大小
  • -Xss 设置 Java 线程堆栈大小,默认1m
  • -XX:PretenureSizeThreshold 大于此值得对象直接分配在老年代(只对Serial和ParNew两款收集器有效),以B为单位,1kb学制为1024
  • -XX:NewSize=2.125m 新生代对象生成时占用内存的默认值
  • -XX:MaxNewSize=size 新生成对象能占用内存的最大值
  • -XX:PermSize=64m 方法区分配的初始内存
  • -XX:MaxPermSize=64m 方法区能(永久代)占用内存的最大值
  • -XX:NewRatio=2 新生代内存容量与老生代内存容量的比例,默认2,即 1:2
  • -XX:SurvivorRatio=8 Eden区域Survivor区的容量比值,如默认值为8,代表Eden:Survivor1:Survivor2=8:1:1
  • -XX:MaxTenuringThreshold=15 对象在新生代存活区切换的次数(坚持过MinorGC的次数,每坚持过一次,该值就增加1),大于该值会进入老年代(年龄阈值)
  • -XX:MinHeapFreeRatio=40 GC后java堆中空闲量占的最小比例
  • -XX:MaxHeapFreeRatio=70 GC后java堆中空闲量占的最大比例
  • -XX:ThreadStackSize=512 设置线程栈大小,若为0则使用系统默认值
  • -XX:MetaspaceSize=128m 元空间(永久代) 初始大小;元空间的默认初始大小是20.75MB
  • -XX:MaxMetaspaceSize=128m 元空间(永久代) 最大空间 一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值
  • -XX:LargePageSizeInBytes=4m 设置用于Java堆的大页面尺寸
  • -XX:ReservedCodeCacheSize=32m 保留代码占用的内存容量
  • -XX:+UseLargePages 使用大页面内存

调试参数:

  • -XX:-CITime 打印消耗在JIT编译的时间
  • -XX:ErrorFile=./hs_err_pid.log 保存错误日志或者数据到文件中
  • -XX:-ExtendedDTraceProbes 开启solaris特有的dtrace探针
  • -XX:HeapDumpPath=./java_pid.hprof 指定导出堆信息时的路径或文件名
  • -XX:-HeapDumpOnOutOfMemoryError 当首次遭遇OOM时导出此时堆中相关信息
  • -XX:OnError=“;” 出现致命ERROR之后运行自定义命令
  • -XX:OnOutOfMemoryError=“;” 当首次遭遇OOM时执行自定义命令
  • -XX:-PrintClassHistogram 遇到Ctrl-Break后打印类实例的柱状信息,与jmap -histo功能相同
  • -XX:-PrintConcurrentLocks 遇到Ctrl-Break后打印并发锁的相关信息,与jstack -l功能相同
  • -XX:-PrintCommandLineFlags 打印在命令行标记,用于查看jvm参数
  • -XX:-PrintCompilation 当一个方法被编译时打印相关信息
  • -XX:-PrintGC 每次GC时打印相关信息
  • -XX:-PrintGC Details 每次GC时打印详细信息
  • -XX:-PrintGCTimeStamps 打印每次GC的时间戳
  • -XX:-TraceClassLoading 跟踪类的加载信息
  • -XX:-TraceClassLoadingPreorder 跟踪被引用到的所有类的加载信息
  • -XX:-TraceClassResolution 跟踪常量池
  • -XX:-TraceClassUnloading 跟踪类的卸载信息
  • -XX:-TraceLoaderConstraints 跟踪类加载器约束的相关信息

5.5.其他命令

  • JPS 查看java进程id
  • jinfo [options]
    • -flags:显示 JVM 启动时设置的标志(Flag)信息。
    • -sysprops:显示 Java 系统属性(System Property)信息。
    • -commandline:显示 Java 进程的启动命令行参数信息。
    • -flag :显示指定 Flag 的设置值。
    • -flag [+/-]:将指定 Flag 的设置值在运行时开启或关闭。
    • -help:帮助信息。
  • jstat 查看性能 类加载、内存、垃圾收集情况、 JIT 实时编译的运行时数据
    • jstat [-t] [-h]
    • jstat [option [interval [s|m] [count] ] ]
option参数 解释
-class 显示ClassLoad的相关信息
-compiler 显示JIT编译的相关信息
-gc 显示和gc相关的堆信息-
-gccapacity 显示各个代的容量以及使用情况
-gccause 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因
-gcnew 显示新生代的信息
-gcnewcapacity 显示新生代大小和使用情况
-gcold 显示老年代和永久代的信息
-gcoldcapacity 显示老年代的大小
-gcpermcapacity 显示永久代的大小
-gcutil 显示垃圾收集信息
-printcompilation 输出JIT编译的方法信息
参数 解释
-t 可以在打印的列上加上Timestamp列,用于显示系统运行的时间
-h 可以在周期性数据的时候,可以在指定输出多少行以后输出一次表头
interval 执行每次的间隔时间,单位为毫秒
count 用于指定输出多少次记录,缺省则会一直打印

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_44765647/article/details/132596809

智能推荐

基于php微信小程序购物商城 校园二手商品 图书鲜花商城 毕业设计(1)首页_小程序校园购物平台页面-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏17次。效果图wxml代码<!--轮播:远程数据(图片地址+连接地址)【对象数组,数组中的每个元素包含图片地址+链接地址】--><view ><!--swiper属性里面的设置相比上面,多了个if判断是否显示,jieguo=true; jieguo2=false--><!--远程数据地址:http://phpshop.yaoyiwangluo.com/wx_lunbo.php--><swiper indicator-dots="{{indicato_小程序校园购物平台页面

Java分布式锁_java 分布式锁-程序员宅基地

文章浏览阅读3.3k次,点赞7次,收藏30次。1.什么是锁?在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。不同地方实现锁的方式也不一样,只要能满足所有线程都_java 分布式锁

进制之间的相互转换(超详细)_进制转换-程序员宅基地

本篇文章介绍了十进制与其他进制之间的相互转换方法,包括十进制转换为二进制、八进制、十六进制,以及其他进制转换为十进制的方法。同时还提供了一些具体的转换示例。

课后习题1-数据结构_简述逻辑结构的四种基本关系并画出它们的关系图-程序员宅基地

文章浏览阅读4k次,点赞11次,收藏26次。1.概念:数据:Data,是客观事物的符号表示,是所有能输入到计算机中并被计算机程序处理的符号的总称。数据元素:Data Element,是数据的基本单位,在计算机中常作为一个整体进行考虑和处理,用于完整的描述一个对象。数据项:Data Item,是组成数据元素的、有独立含义的、不可分割的最小单位。数据对象:Data Object,是性质相同的数据元素的集合,是数据的一个子集。数据结构:Data Structure,是相互之间存在一种或多种特定关系的数据元素的集合。逻辑结构:从具体问题抽象出来的_简述逻辑结构的四种基本关系并画出它们的关系图

Compute Shader 语法及函数 Reference for HLSL_hlsl compute shader-程序员宅基地

文章浏览阅读887次。///////////////////////////////////////变量语法使用以下语法规则声明 HLSL 变量。[Storage_Class] [Type_Modifier]Type Name[Index] [: Semantic] [: Packoffset] [: Register]; [Annotations] [= Initial_Value]参数存储 _班级可选的存储类修饰符,它们为编译器提示指定变量范围和生存期;可以按任意顺序指定修饰符。值 说明._hlsl compute shader

一文彻底搞懂 Alertmanager 的告警抑制与静默_alertmanager告警-程序员宅基地

文章浏览阅读4.3k次,点赞4次,收藏16次。一文彻底搞懂 Alertmanager 的告警抑制与静默。_alertmanager告警

随便推点

SQL语句左连接LEFT OUTER JOIN的作用_left outer join 的效果-程序员宅基地

文章浏览阅读7.8k次。SQL在联表查询的时候,如果遇到字表的数据是没有的,如果是普通的查询就会出现一种查不到数据的情况,这时候就需要用到联表查询的使用左连接指向主表(左右连接看功能的具体需求),我的情况是主表连接多个子表(这些表是查询下拉框的数据),因为总有用户会不选上某一个的下拉框的情况,这样就会在修改时绑定数据的时候就会出现数据绑不上的情况我们就可以使用左连接,然后就可以查询到有的数据,就不会受到没有的数..._left outer join 的效果

linux分段内存管理中的GDT,LDT,GDTR,LDTR-程序员宅基地

文章浏览阅读2.9k次,点赞8次,收藏17次。本文是对上一篇文章《逻辑地址、线性地址、物理地址的关系以及段寄存器在不同位数CPU中的用途演变以及GDT LDT PGD PT的关系》的补充。一. 寻址方式:实地址模式和保护地址模式我们知道,内存寻址模式在早期是采用的实地址模式(intel 80286之前),后面发展到了保护模式(80286开始)。在8086的时候,也就是16位cpu的时候,CPU配备了4个16位段寄存器(CS代码段寄存器..._ldtr

JSP中的EL表达式报错org.apache.jasper.JasperException_org.apache.jasper.jasperexception: 在 [31] 行处理 [/re-程序员宅基地

文章浏览阅读1.9k次。1、报错场景:在jsp中使用el表达式时,出现JasperException异常。2、报错信息:org.apache.jasper.JasperException: 在 [45] 行处理 [/register.jsp] 时发生异常42: <td style="width:40%">43: <input type="text" clas..._org.apache.jasper.jasperexception: 在 [31] 行处理 [/register1.jsp] 时发生异

H5(html5)入门教程_html5教程-程序员宅基地

文章浏览阅读9.9k次,点赞14次,收藏94次。html零基础01_html5教程

layui--layer.open参数传递问题_layer.open data-程序员宅基地

文章浏览阅读2.1w次,点赞5次,收藏6次。1、第一种方式/**function layer_show(title,url,data,w,h){if (title == null || title == ‘’) {title=false;};if (url == null || url == ‘’) {url=“404.html”;};if (w == null || w == ‘’) {w=800;};if (h ..._layer.open data

华为实习面试(二)_华为实习业务主管面试-程序员宅基地

文章浏览阅读2.1w次,点赞9次,收藏7次。这是来自一位学长的 (业务主管综合面)4.29下午2:10分,全程20min学长的视角:主管也是真的很nice!我每次回答问题后都给我说谢谢,搞得我都不好意思了,整个过程非常随和,完全没有架子,很耐心的给我解释问题,最后退出还说非常感谢面试华为,体验超好,面完五分钟官网刷新通过,十分钟短信通过。总结一下吧,总的来说,我这次华子的面试准备了很多东西,但是基本没问…整个过程体验非常好,不会让你尴尬的,面试官都大赞!给大家分享面筋,希望对还没面试的小伙伴提供参考,不过目前进了池子,得等很久才能出结果,许愿offe_华为实习业务主管面试

推荐文章

热门文章

相关标签