【JVM】JVM内存结构+JVM参数配置+GC垃圾回收+OOM内存溢出

【JVM】GC与JVM调优

1.JVM内存结构

在这里插入图片描述
1.1 上面是概览图,JVM内存结构大致可以分为线程共有和线程私有两部分:

  • 线程私有:

1,程序计数器:用于存放下一条运行的指令,这里是唯一无内存溢出的区域。如果当前程序正在执行一个Java方法,则程序计数器记录正在执行的Java字节码地址,如果当前线程正在执行一个Native方法,则程序计数器为空。

2,虚拟机栈:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务;虚拟机执行java程序的时候,每个方法都会创建一个栈帧,栈帧存放在java虚拟机栈中,通过压栈出栈的方式进行方法调用。

3,本地方法栈:本地方法栈则是为虚拟机使用到的Native方法服务。

  • 线程共有:

1,方法区:存放的是类的类型信息、常量池、域信息、方法信息。总之,方法区保存的信息,大部分来自于 class 文件,是 Java 应用程序运行必不可少的重要数据。

2,堆:用于存放Java程序运行时所需的对象等数据,Java堆又分为新生代,老年代和永久代。我们平常所说的垃圾回收,主要回收的就是堆区。更细一点划分新生代又可划分为Eden区和2个Survivor区(From Survivor和To Survivor);老年代主要存放程序中年龄较大和需要占用大量连续内存空间的对象。永久代主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域。

  • 永久代又叫Perm区,并不是所有的jvm中都有永久带,ibm的j9,oracle的JRocket都没有永久带,它只存在于hotspot jvm中,里面存的东西基本上就是方法区规定的那些东西,并且只存在于jdk7和之前的版本中,在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取,元空间使用的是直接内存。

在这里插入图片描述

1.2 直接内存

  • 直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,就是堆外单独的一块内存区域。

  • 上面有提到, 在Java8后,JVM中永久代被移除,被“元数据区”(元空间)取代,而元空间不在虚拟机中,它是直接使用本地内存,而除了元空间,还有java8的新特性NIO,也可以直接操作直接内存。

  • NIO(new input/output),基于通道与缓冲区结合的I/O方式,可以用native函数库直接堆外分配内存,通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中提高性能。

  • 创建直接内存是通过本地方法创建,回收由GC回收实例引用,再由Cleaner的调用clean方法释放内存。

  • 直接内存的分配不会受到Java堆大小的限制。但是会受到本机总内存大小以及处理器寻址空间的限制。

2. GC作用域

在这里插入图片描述

  • 我们说的GC垃圾回收主要回收的就是堆区。
3.常见的垃圾回收算法

3.1 引用可达法

  • 程序运行从开始,每次引用对象,都将对引用的对象进行连接起来,到最后形成一张网,没有在这张网上的对象则被认为是垃圾对象。
    在这里插入图片描述
  • JVM中GC垃圾回收就是使用此算法来判断对象存活状态。

3.1.1 引用可达法可以做GCroot的对象

  • 1.被启动类加载的类和创建的对象
  • 2.栈内存中引用的对象
  • 3.方法区中静态和常量引用的对象
  • 4.本地方法中JNI引用的对象

3.2 引用计数法

  • 对于对象的引用,每引用一次计数器加一,引用失败,计数器减一,当计数器一段时间为0,则可以被认为是垃圾。

在这里插入图片描述

  • 因为循环引用等原因,JVM一般不采用此算法。

3.3 标记-清除算法

  • 标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
  • 此算法的缺点是最后把内存搞得七零八落,造成大量内存碎片

在这里插入图片描述

3.4复制算法

  • 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存A中的存活对象复制到未使用的内存块B中,之后,清除正在使用的内存块A中的所有对象,交换两个内存中的角色,完成垃圾回收。
  • 缺点很明显,同一时刻会浪费一些内存空间
    在这里插入图片描述

3.5 标记-整理算法

  • 首先从根节点开始,对所有可达的对象做一次标记,但之后,它并不是简单的清理未标记的对象,而是将所有的存活对象压缩到内存空间的一端。之后,清理边界外所有的空间。

在这里插入图片描述

3.6 分代

  • 将内存区域根据对象的特点分成不同的内存区域,根据每块区域对象的特征不同使用不同的回收算法,以提高垃圾回收的效率。
  • JVM中堆内存就分为了年轻代和老年代,java8之前还有永久代,java8后改为元空间,具体前面有讲。
4.JVM参数配置

4.1 JVM参数类型

  • 标配参数:-version;-help;java -showversion
    在这里插入图片描述

  • X参数:-Xint;-Xcomp;-Xmixed
    在这里插入图片描述

  • XX参数:上面两种类型参数了解就行,重点是-XX参数,其又分为两种类型:

Boolean类型:-XX:+或者- 某个属性值;+表示开启,-表示关闭;例如:-XX:+printGCDetails 打印GC收集细节,-XX:-printGCDetails不打印GC收集细节

KEY-VALUE设置类型:-XX:属性key = 属性值value;例如:-XX:MetaspaceSize = 128m

4.2 查看当前运行程序配置

  • 在JDK的bin目录下,有许多JDK自带的工具,其中jps加jinfo就可以查看当前运行程序配置
    在这里插入图片描述

  • Demo1

    /**

    • 使用JDK自带工具查看当前线程配置

    • 1,jps -l 加 jinfo -flag InitialHeapSize 进程号

    • 2,jps -l 见 jinfo -flags 进程号

    • @author wangjie

    • @version V1.0

    • @date 2019/12/30
      */
      @Slf4j
      public class helloGC {

      public static void main(String[] args) {
      log.info(“【hello GC】”);
      try {
      Thread.sleep(Integer.MAX_VALUE);
      } catch (InterruptedException e) {
      e.printStackTrace();e.printStackTrace();
      }
      }
      }

  • 演示:
    在这里插入图片描述

  • 流程描述:

1,idea打开终端窗口,输入:jps -l (:相当于linux环境 ps -ef|grep gava)

2,输入jinfo -flag InitialHeapSize 进程号 查看当前进程堆初始大小

3,输入 jinfo -flags 进程号 查看当前线程所有配置;Non-default代表默认,Command line代表新设置

4.3 查看JVM默认配置

  • 查看JVM所有默认配置,打开终端直接输入 java -XX:+PrintFlagsInitial

    E:code est-case> java -XX:+PrintFlagsInitial
    [Global flags]
    intx ActiveProcessorCount = -1 {product}
    uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
    uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
    uintx AdaptiveSizePausePolicy = 0 {product}
    uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
    uintx AdaptiveSizePolicyInitializingSteps = 20 {product}
    uintx AdaptiveSizePolicyOutputInterval = 0 {product}
    uintx AdaptiveSizePolicyWeight = 10 {product}

    有删减。。。。。
    
     bool PrintPLAB                                 = false                  
     bool VerifyMergedCPBytecodes                   = true                                {product}
     bool VerifySharedSpaces                        = false                               {product}
     intx WorkAroundNPTLTimedWaitHang               = 1                                   {product}
    uintx YoungGenerationSizeIncrement              = 20                                  {product}
    uintx YoungGenerationSizeSupplement             = 80                                  {product}
    uintx YoungGenerationSizeSupplementDecay        = 8                                   {product}
    uintx YoungPLABSize                             = 4096                                {product}
     bool ZeroTLAB                                  = false                               {product}
     intx hashCode                                  = 5                                   {product}
    

    E:code est-case>

  • 查看修改更新的配置,终端输入java -XX:+PrintFlagsFinal
    在这里插入图片描述

  • 效果基本一样,不同的是修改更新过的配置不是 = 号,而是 :=

  • PrintFlagsFinal还可以在java代码运行的时候打印参数,用法基本不变:java -XX:+PrintFlagsFinal 方法名
    在这里插入图片描述

  • 还有个命令:java -XX:PrintCommandLineFlags -version可以打印出常用配置和使用的那个垃圾回收器
    在这里插入图片描述

4.4 常用基本参数

  • -Xms 等价于 -XX:InitialHeapSize

初始大小内存,默认为物理内存的1/64

  • -Xmx 等价于 -XX:MaxheapSize

最大分配内存,默认为物理内存1/4

  • -Xss 等价于 -XX:ThreadStackSize

设置单个线程的大小,一般默认为512-1024k,下图为Oracle公司文档说明:

在这里插入图片描述

  • -Xmn

设置年轻代大小

  • -XX:MetaspaceSize

设置元空间大小,元空间的本质和永久代类似,都是对JVM规范中方法区的实现,区别在于元空间不在虚拟机中,而是使用本地内存,其默认初始大小大小21兆左右。

在这里插入图片描述
在这里插入图片描述

  • -XX:+PrintGCDetails

输出GC收集日志信息

在这里插入图片描述

故意设置初始内存和最大内存为10M,写个mian方法,创建一个大于10M的对象,日志如下:

在这里插入图片描述

YiungGC日志图解

在这里插入图片描述

FullGC图解

在这里插入图片描述

  • -XX:SurvivoRatio

设置新生代中eden和S0/S1空间的比例,默认-XX:SurvivoRatio=8,Esen:S0:S1 = 8:1:1

在这里插入图片描述

  • -XX:NewRatio

配置年轻代与年老待比例,默认:-XX:NewRatio=2,新生代占1,老年代占2.

  • -XX:MaxTenuringThreshold

设置垃圾最大年龄,或是说躲过几轮GC就可以到年老代,默认15,最大也是15

:关于默认的晋升年龄是15,这个说法的来源大部分都是《深入理解Java虚拟机》这本书。 如果你去Oracle的官网阅读相关的虚拟机参数,你会发现-XX:MaxTenuringThreshold=threshold这里有个说明
在这里插入图片描述
Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.最大值是15,并行(吞吐量)收集器的默认值为15,但并不都是默认15,这个是要区分垃圾收集器的,CMS就是6

  • Hotspot动态年龄计算

Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值。
例如:Survivor区 = 64M,desired survivor = 32M,此时Survivor区中age<=2的对象累计大小为41M,41M大于32M,所以晋升年龄阈值被设置为2,下次Minor GC时将年龄超过2的对象被晋升到老年代。就会导致old generation 快速填满,触发old gc(old gc 有三处STW),所以这是建议调整-XX:SurvivorRatio参数。

原因:如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件: a)MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了。 b)MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能。

5.JVM垃圾回收器
  • 如果说GC算法(引用可达,引用计数、复制清除、标记清除,标记整理)是内存回收的方法论,那垃圾回收器就是算法落地。
  • 目前没有完美的收集器出现,更没有万能的收集器,只是正对具体应用最合适的。

在这里插入图片描述

  • 上图是广为流传的七大垃圾回收器,其中Serial Old已经过时,在java8后已不在使用,具体可看下图源码

在这里插入图片描述

  • 查看默认垃圾回收器,打开终端输入:java -XX:+PrintCommandLineFlags -version
    在这里插入图片描述

  • 再细聊垃圾回收器之前,我们先聊一聊,JVM client模式和Server模式的区别

1,Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升

2,当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,服务起来之后,性能更高.

3,所以通常用于做服务器的时候我们用服务端模式,服务器模式编译更彻底,垃圾回收优化更好,当然吃的内存要多点相对于客户端模式。

4,除了上面的区别,不同的操作系统,JVM默认的模式也不同,32位window操作系统,不论硬件如何默认使用Client模式;32位其他操作系统,2G运行内存的同时有2个CPU以上的用Server模式,低于该配置还是Client模式;最后64位noly server模式。

  • 下面我们按新生代和老年代一个一个垃圾回收器聊:
    在这里插入图片描述

5.1 新生代串行GC–Serial垃圾回收器

  • 它是个单线程的收集器,在进行垃圾收集的时候,必须暂停其他所有工作线程,直到它收集完成。

在这里插入图片描述

  • 串行收集器是最古老,最稳定以及效率高的收集器,虽然只用一个线程去回收会产生较长的停顿(Stop-The-World),但它简单高效,对于单CPU环境,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial依然是Cluent模式先的新生代默认垃圾收集器(思考:话虽如此,但现在这个时代,那还有单CPU)

  • 对应设置参数:-XX:UseSerialGC ,开启后会使用:Serial(Young区)+Serial Old(Old区)的垃圾回收组合,意思是说新生代和老年代都会使用串行回收器,区别在于新生代用复制清除算法,老年代用使用标记整理算法。

  • 举例:
    在这里插入图片描述

  • VM配置:-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseSerialGC

  • 运行日志:
    在这里插入图片描述

  • DeNew 意思是 Default New Generation :默认新生代垃圾回收器

  • Tenured 年老代的意思,指Serial Old收集器

5.2 新生代并行GC—ParNew垃圾回收器

  • ParNew垃圾回收器使用多线程进行垃圾回收,在收集时,会Stop-The-world暂停其他所有工作线程直到收集结束。
    在这里插入图片描述

  • ParNew垃圾回收器其实就是Serial的并行多线程版本,最常见的应用场景是配合老年代的CMS GC工作,其余的行为和Serial完全一致,它是Server模式先新生代默认垃圾回收器。

  • 对应设置参数:-XX:+UseParNewGC ,开启此参数后,会使用ParNew+Serial Old收集器组合,新生代使用复制清除算法,老年代使用标记整理算法。

  • -XX:ParallelGCThreads 限制线程数量,默认开启和CPU数目相同的线程数。

  • 举例:

  • 还是上面那个Demo,配置参数改为:-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseParNewGC

  • 运行日志:
    在这里插入图片描述

  • parNew 意思是Parallel New Generation

  • 但是ParNew+Tenured(指Serial Old收集器)这样的搭配,java8已经不推荐使用:
    在这里插入图片描述

5.3 新生代并行GC—Parallel Scavenge垃圾回收器

  • Parallel Scavenge垃圾回收器类似ParNew,也是新生代并行垃圾回收器,使用复制清除算法,也叫吞吐量优先收集器。
    在这里插入图片描述
  • 可控制吞吐量(Thouhput = 运行用户代码时间/(运行用户代码时间+垃圾回收时间)),高吞吐量意味着高效利用CPU时间,它多用于后台运算而不需要太多交互的任务。
  • 自适应调节策略也是ParallelScavenge收集器与ParNew收集器的一个重要区别。
  • 自适应调节策略:虚拟机根据当前系统运行情况收集性能监控信息,动态调整参数以提高最合适的停顿时间(-XX:MaxGCPauseMillis)或最大吞吐量。
  • 对应设置参数:-XX:+UseParallelGC 或-XX:+UseParallelOldGC(可相互激活),开启该参数后,新生代使用复制算法,老年代使用标记整理算法。
  • 举例,还是同一个Demo,参数设置改为:-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseParallelGC
  • 运行日志:
    在这里插入图片描述
  • PSYoungFen 意思是Parallel Scavenge
  • PSOldGen 意思是Parallel Old Generation

5.4 老年代串行GC–Serial Old收集器

  • Serial Old收集器是Serial收集器老年代版本,同样是个单线程收集器,使用标记整理算法。

  • 在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用。

  • 目前java8后除了作为CMS收集器的后备元,在并发收集发生 Concurrent Mode Failure的时候使用,其他地方已经过时不用了

  • 举例,设置::-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseSerialOldGC

  • 运行日志:
    在这里插入图片描述
    5.5 老年代并行GC—Parallel Old收集器

  • 在JDK1.6之前,新生代使用Parallel Scavenge垃圾回收器只能搭配Serial Old收集器,只能保证新生代吞吐量优先,无法保证整体的吞吐量。

  • Parallel Old收集器正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,在JDK8后可以考虑Parallel Scavenge + Parallel Old,也是JDK8默认配置

  • 具体例子可以看上面Parallel Scavenge垃圾回收器。

5.6 老年代并发清除GC—CMS

  • CMS收集器(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的收集器:
    在这里插入图片描述
  • CMS适合用于互联网站或者B/S系统服务器上,这类用于尤其重视服务器响应速度。
  • CMS也非常适合堆内存大,CPU核数多的服务器应用,也是G1出现前大型应用的首选收集器。
  • 参数设置:-XX:+UseConcMarkSweepGC ,开启该参数后会自动打开::-XX:+UseParNewGC;使用ParNew+CMS+Serial Old组合,Serial Old作为CMS收集器的后备元,在并发收集发生 Concurrent Mode Failure的时候使用
  • CMS的四步过程:1,初始标记;2,并发标记;3,重新标记;4;并发清除
    在这里插入图片描述
  • 举例:还是那个Demo,参数改为:-Xms 10m -Xmx 10m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC
  • 运行日志:

在这里插入图片描述

  • CMS优点:并发收集停顿低。
  • CMS缺点:

1,并发执行,对CPU资源压力大

在这里插入图片描述

2,采用的标记清除算法会导致大量碎片

在这里插入图片描述

  • 关于如何选择垃圾回收器:
    在这里插入图片描述

5.7 G1垃圾回收器

  • 聊G1之前,我们先总结一下上面那些收集器的特点:

1,年轻代和老年代都是各自独立且连续的内存块

2,年轻代收集器使用eden+S0+S1进行复制算法

3,老年代收集必须扫描整个老年代区域

4,都是以尽可能少而块的执行GC为设计原则

  • G1是什么?

G1收集器是一个面向服务端应用的收集器,其特点如下:

在这里插入图片描述

  • 底层原理:
    在这里插入图片描述

  • 逻辑结构图:
    在这里插入图片描述
    在这里插入图片描述

  • 回收步骤:
    在这里插入图片描述

  • 样例图解:
    在这里插入图片描述
    在这里插入图片描述

  • G1常用参数配置:
    在这里插入图片描述

  • 和CMS相比优势:

1,G1不会产生内存碎片

2,可以控制停顿,该收集器是把整个堆划分为多个固定大小区域,每次根据允许停顿的时间去收集垃圾最多的区域。

5.8 总结
在这里插入图片描述

6.OopMap、SafePoint(安全点)以及安全区域

6.1 OopMap

  • JVM进行垃圾回收是一个非常复杂的过程,如何进行垃圾标记、什么时候进行垃圾、如果进行垃圾回收等等都非常复杂,当前主流测JVM在垃圾回收时都会进行STW(stop the world),即使宣称非常快的CMS垃圾回收期早期也会STW标记垃圾状态。那么这里有个问题,JVM的GC执行时机是任何时候都可以吗?
  • CPU在执行运算过程时需要把数据从内存中载入到寄存器,运算完后再从寄存器中载入到内存中,Java中对象地址也是这么个过程,设想如果一个Java线程分配一个对象,此时对象的地址还在寄存器中,这时候这个线程失去了CPU 时间片,而此时STW GC发现没有任何GC ROOTS与该对象关联起来,此时这个对象呗认为是垃圾并被回收了,之后CPU重新获得时间片后发现此时对象已经不存在了这时候程序就GG了,因此不是在任何时候都可以随便GC的。
  • 而且每次GC的时候都要遍历所有的引用肯定是不现实的。 为了应对这种尴尬的问题,采用空间换时间的策略。在某个时候(安全点)把栈上代表引用的位置记录下来,GC时直接读取,避免了全部扫描。 HotSpot虚拟机采用了一种叫做OopMap的数据结构来记录这些引用(OopMap也帮助HotSpot实现了准确式GC),OopMap记录了栈上本地变量到堆上对象的引用关系,这些引用指向的对象不能够回收,并且可以作为根节点来进行可达性分析,查找出不能够回收的对象。

6.2 SafePoint(安全点)

  • 上面讲到了为了快点进行可达性的分析,使用了一个引用类型的映射表,可以快速的知道对象内或者栈和寄存器中哪些位置是引用了。

  • 但是随着而来的又有一个问题,就是在方法执行的过程中, 可能会导致引用关系发生变化,那么保存的OopMap就要随着变化。如果每次引用关系发生了变化都要去修改OopMap的话,这又是一件成本很高的事情。所以这里就引入了安全点的概念。

  • 什么是安全点?OopMap的作用是为了在GC的时候,快速进行可达性分析,所以OopMap并不需要一发生改变就去更新这个映射表。只要这个更新在GC发生之前就可以了。所以OopMap只需要在预先选定的一些位置上记录变化的OopMap就行了。这些特定的点就是SafePoint(安全点)。由此也可以知道,程序并不是在所有的位置上都可以进行GC的,只有在达到这样的安全点才能暂停下来进行GC。

  • 既然安全点决定了GC的时机,那么安全点的选择就至为重要了。安全点太少,会让GC等待的时间太长,太多会浪费性能。所以安全点的选择是以程序“是否具有让程序长时间执行的特征”为标准的(这句话是从书上看来的,不知道作者自己能不能看明白这话啥意思,反正我是看不懂),所以我们这里了解一下结果就行了。一般会在如下几个位置选择安全点:

循环的末尾
方法临返回前
调用方法的call指令后
可能抛异常的位置

  • 还有一个需要考虑的问题就是,如何让程序在要进行GC的时候都跑到最近的安全点上停顿下来。这里有两种方案:

抢断式中断
抢断式中断就是在GC的时候,让所有的线程都中断,如果这些线程中发现中断地方不在安全点上的,就恢复线程,让他们重新跑起来,直到跑到安全点上。(现在几乎没有虚拟机采用这种方式,原因不详)

主动式中断
主动式中断在GC的时候,不会主动去中断线程,仅仅是设置一个标志,当程序运行到安全点时就去轮训该位置,发现该位置被设置为真时就自己中断挂起。所以轮训标志的地方是和安全点重合的,另外创建对象需要分配内存的地方也需要轮询该位置。

6.3.安全区域

  • 安全点的使用似乎解决了OopMap计算的效率的问题,但是这里还有一个问题。安全点需要程序自己跑过去,那么对于那些已经停在路边休息或者看风景的程序(比如那些处在Sleep或者Blocked状态的线程),他们可能并不会在很短的时间内跑到安全点去。所以这里为了解决这个问题,又引入了安全区域的概念。

  • 安全区域很好理解,就是在程序的一段代码片段中并不会导致引用关系发生变化,也就不用去更新OopMap表了,那么在这段代码区域内任何地方进行GC都是没有问题的。这段区域就称之为安全区域。线程执行的过程中,如果进入到安全区域内,就会标志自己已经进行到安全区域了。那么虚拟机要进行GC的时候,发现该线程已经运行到安全区域,就不会管该线程的死活了。所以,该线程在脱离安全区域的时候,要自己检查系统是否已经完成了GC或者根节点枚举(这个跟GC的算法有关系),如果完成了就继续执行,如果未完成,它就必须等待收到可以安全离开安全区域的Safe Region的信号为止。

7.OOM—内存溢出
  • OOM–内存溢出是error还是Exception
    在这里插入图片描述
  • 你可以直接翻文档,也可以idea打开类图:
    在这里插入图片描述
  • 接下来我们聊几个常见的内存溢出

7.1 先看栈溢出 --Java.lang.StackOverFlowError

  • Demo:
    在这里插入图片描述
  • 死循环压栈出栈,上面我们聊过一个线程内存大小一般在512-1024k
  • 运行结果:
    在这里插入图片描述

7.2 堆溢出Java.lang.OutOfMemoryError:Java heap

  • Demo:
    在这里插入图片描述
  • 运行前参数设置一下,把堆内存最大改为10M:
    在这里插入图片描述
  • 运行结果:

在这里插入图片描述

  • 堆内存不足以分配给新建对象,导致内存溢出。

7.3 Java.lang.OutOfMemoryError: GC overhead limit exceeded

  • 程序在垃圾回收上花费了98%的时间,却收集不回2%的空间

  • 原因:程序基本上耗尽了所有的可用内存, GC也清理不了。

  • java.lang.OutOfMemoryError: GC overhead limit exceeded 错误只在连续多次 GC 都只回收了不到2%的极端情况下才会抛出。

  • 2%的内存每次GC后很快会再次填满, 导致再次GC.造成恶性循环, CPU使用率一直是100%, 而GC却没有任何成果。

  • Demo:
    在这里插入图片描述

  • 同样运行前把最大堆内存大小改为10M。

  • 运行日志:
    在这里插入图片描述
    7.4 直接内存溢出 Java.lang.OutOfMemoryError: Direct buffer memory

  • 导致原因:
    在这里插入图片描述

  • Demo:
    在这里插入图片描述

  • 代码里我们可以通过sun.misc.VM.maxDirectMemory()获取最大直接内存,也通过-XX:MaxDirectMemorySize的设置直接内存的值

  • 设置最大直接内存为5M:-XX:MaxDirectMemorySize=5M

  • 运行结果:
    在这里插入图片描述

7.5 Java.lang.OutOfMemoryError: unable to create new native thread

  • 导致原因与对应平台有关:
    在这里插入图片描述
  • Demo:
    在这里插入图片描述
  • 在服务器上建个用户,用该用户执行该代码;
    在这里插入图片描述
  • 这时候你可能已经无法操作该界面了,换窗口,用root用户登录服务器,输入:ps -ef |grep java 找到该线程:kill-9 进程号
  • 解决办法:
    在这里插入图片描述
  • linux调优:
    在这里插入图片描述
  • 服务器键入ulimit -u 命令可以查看登陆用户线程限制数
  • 再按图中命令打开配置文件:
    在这里插入图片描述
  • 可以看到root用户是不做限制的,其他用户限制为1024,你可以新添加一条数据把自己用户限制数设一个合适的值。

7.6 Java.lang.OutOfMemoryError: Metaspace

  • Metaspace元空间我们上面已经介绍过多次,java8后元空间取代了永久代,使用的内存是直接内存,主要内容忘记可以看下图:
    在这里插入图片描述
  • Demo:
    在这里插入图片描述
  • 元空间默认大小为21M,我们用-XX:MetaspaceSize=8M -XX:MaxMetaspaceSize=8M 来把元空间大小设置在8M
  • 运行结果:
    在这里插入图片描述
  • 276次后元空间被塞满溢出。
8.总结-思维导图

在这里插入图片描述

【完】

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 1. Spark中JVM内存使用及配置详情: Spark中的JVM内存使用主要包括堆内存和非堆内存。堆内存用于存储对象实例,而非堆内存用于存储类信息、方法信息等。在Spark中,可以通过以下参数配置JVM内存使用: - spark.driver.memory:用于配置Driver进程的堆内存大小,默认为1g。 - spark.executor.memory:用于配置Executor进程的堆内存大小,默认为1g。 - spark.driver.extraJavaOptions:用于配置Driver进程的非堆内存大小和其他JVM参数。 - spark.executor.extraJavaOptions:用于配置Executor进程的非堆内存大小和其他JVM参数。 2. Spark报错与调优: 在Spark运行过程中,可能会出现各种报错,如内存溢出、任务失败等。针对这些报错,可以采取以下调优措施: - 内存溢出:增加Executor进程的堆内存大小、减少每个任务的数据量、使用缓存等方式来减少内存使用。 - 任务失败:增加Executor进程的数量、减少每个任务的数据量、调整任务的并行度等方式来提高任务的执行效率。 3. Spark内存溢出OOM异常: Spark内存溢出OOM异常是指Executor进程的堆内存不足以存储当前任务所需的数据,导致任务执行失败。可以通过增加Executor进程的堆内存大小、减少每个任务的数据量、使用缓存等方式来减少内存使用,从而避免内存溢出异常的发生。 ### 回答2: Spark中JVM内存使用及配置详情: Spark使用JVM来执行任务,其中一个非常重要的参数是堆内存(Heap Memory)的大小。堆内存用于存储对象实例和方法调用的信息。在使用Spark时,可以通过spark.driver.memory和spark.executor.memory参数配置JVM内存的大小,默认情况下,它们都是1g。需要根据具体的任务需求和集群资源情况来进行调整。如果遇到内存不足的情况,可以增加堆内存的大小,但是需要保证集群资源充足。 Spark报错与调优: 在使用Spark过程中,常见的报错有内存溢出、数据倾斜、任务运行时间过长等问题。对于这些问题,可以采取一些调优策略进行处理。例如,在遇到内存溢出(Out of Memory)异常时,可以通过增加堆内存大小或者减少数据量来解决;对于数据倾斜的情况,可以考虑数据重分区或者使用一些聚合策略来优化;对于任务运行时间过长的情况,可以考虑增加Spark任务的并行度或者使用缓存机制来加速计算等。 Spark内存溢出OOM)异常: Spark中的内存溢出异常通常是由于使用的内存超过了配置的阈值引起的。在配置Spark应用程序时,可以设置spark.driver.memory和spark.executor.memory参数来调整JVM内存的大小。如果内存不足,则需要增加内存配置或者优化代码逻辑。另外,可以通过设置spark.memory.offHeap.enabled参数来开启堆外内存,将一部分内存放到堆外,从而减少对JVM内存的占用。此外,还可以通过设置spark.memory.fraction参数来调整JVM内存的分配比例,更好地利用内存资源。如果调整参数后仍然出现内存溢出问题,还可以考虑调整Spark任务的并行度或者增加集群资源。 ### 回答3: Spark是一个基于内存的数据处理框架,能够高效地处理大规模数据集。在Spark中,JVM内存的使用及配置对于保证程序的稳定和性能的提升非常重要。 首先,Spark的JVM内存分为堆内存和非堆内存两部分。堆内存是用来存储对象实例的,而非堆内存则用来存储JVM本身的运行时数据。为了合理配置JVM内存,可以通过配置spark.driver.memory和spark.executor.memory参数来设置堆内存的大小。根据集群的硬件配置和任务的需求情况,可以根据具体情况来调整这两个参数的数值。 其次,在Spark运行过程中,经常会遇到各种报错。常见的报错有内存溢出(OutOfMemoryError)、任务失败(TaskFail)等。当遇到内存溢出错误时,可以尝试以下几种方法来调优: 1. 增加可用内存:可以通过增加executor内存或调整任务分区大小来扩大可用内存。 2. 减少数据规模:可以通过过滤数据、采样数据或者使用压缩算法来减少数据的大小。 3. 优化代码:可以优化代码逻辑和算法,减少内存使用。 4. 调整缓存策略:可以通过手动控制缓存的数据量,及时释放不再使用的缓存。 最后,Spark的内存溢出OOM)异常通常是由于数据量过大,超出了可用内存的限制而导致的。当出现内存溢出异常时,可以参考上述的调优方法来解决问题。 总之,合理配置JVM内存、及时处理报错、避免内存溢出异常是保证Spark程序稳定与性能的关键。希望以上回答对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值