JVM_体系

本文详细探讨了JVM的体系结构,包括类装载器、执行引擎、本地接口、本地方法栈、程序计数器、方法区、虚拟机栈、堆和垃圾回收机制。讲解了类加载过程、双亲委派模型、各种垃圾收集器的工作原理以及JVM参数配置和调优。此外,还介绍了不同类型的引用和常见的内存溢出错误及其原因。
摘要由CSDN通过智能技术生成

文章目录

JVM位置

JVM是运行在操作系统之上的,它与硬件没有直接的交互。

在这里插入图片描述

JVM体系结构

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

类装载器ClassLoader

负责加载class文件,class文件在文件开头有特定的文件标示,将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定 。
在这里插入图片描述

种类

在这里插入图片描述

虚拟机自带的加载器
  • 启动类加载器(Bootstrap)C++
  • 扩展类加载器(Extension)Java
  • 应用程序类加载器(AppClassLoader)Java也叫系统类加载器,加载当前应用的classpath的所有类
用户自定义的加载器

java.lang.ClassLoader的子类,用户可以定制类的加载方式

加载时机(类仅加载一次)

  1. 实例化该类对象时
  2. 调用该类的静态方法或静态属性时
  3. JVM启动时调用main方法所在的类
  4. 调用java某些反射的方法(例如JDBC加载驱动类)
  5. 初始化该类的子类时

双亲委派机制

当一个类收到了类加载请求,首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

执行引擎 Execution Engine

负责解释命令,提交操作系统执行。

本地接口 Native Interface

本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等。

本地方法栈 Native Method Stack

  • 与虚拟机栈类型,只不过是登记native方法,在Execution Engine 执行时加载本地方法库。
  • 这个区域也会抛出StackOverflowError和OutOfMemoryError。

程序计数器/PC寄存器 Program Counter Register

  • 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令。
  • 如果执行的是一个Native方法,那这个计数器是空的。
  • 用以完成分支、循环、跳转、异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误

方法区 Method Area

  • 方法区存储的是从Class文件加载进来的静态变量、类信息、常量池以及编译器编译后的代码。
  • 线程共享区域,因此这是线程不安全的区域。
  • 方法区也是一个可能会发生OutOfMemoryError的区域。
  • 在不同虚拟机里实现是不一样的,最典型的就是永久代(PermGen space)和元空间(Metaspace)
  • 元空间与永久代之间最大的区别在于:
    永久代使用的JVM的堆内存,但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
  • 它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

虚拟机栈 VM Stack

在这里插入图片描述

  • 线程私有区域,每一个线程都有独享一个虚拟机栈,因此这是线程安全的区域。
  • 存放基本数据类型以及对象的引用。
  • 每一个方法执行的时候会在虚拟机栈中创建一个相应栈帧,方法执行完毕后该栈帧就会被销毁。
  • 方法栈帧是以先进后出的方式虚拟机栈的。
  • 每一个栈帧又可以划分为局部变量表、操作数栈、动态链接、方法出口以及额外的附加信息。
  • 这个区域可能有两种异常:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常(通常是递归导致的);JVM动态扩展时无法申请到足够内存则抛出OutOfMemoryError异常。

堆 Heap

  • 存储的是new出来的对象,不存放基本类型和对象引用。
  • 由于创建了大量的对象,垃圾回收器主要工作在这块区域。
  • 线程共享区域,因此是线程不安全的。
  • 能够发生OutOfMemoryError。
    在这里插入图片描述

类的生命周期

在这里插入图片描述

装载

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

连接

验证

  • 目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机自身的安全。
  • 步骤:
    • 文件格式验证(字节流)
    • 元数据验证(方法区)
    • 字节码验证(方法区)
    • 符号引用验证(方法区)

准备

  • 为静态变量在方法区分配内存并设置静态变量为默认值
  • 为静态常量在方法区分配内存并赋值

解析

  • 将常量池内的符号引用替换为直接引用的过程
    • 符号引用:包含类的信息,方法名,方法参数等信息的字符串,供实际使用时在该类的方法表中找到对应的方法
    • 直接引用:偏移量,通过偏移量可以直接在该类的内存区域中找到方法字节码的起始位置。

初始化

  • 执行类构造器()方法的过程
  • 按照源文件中的顺序收集类的静态数据,并为静态变量赋初始值
  • 静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问

对象实例化

  • new关键字为对象在堆中分配合适的内存,并为对象的成员属性赋默认值
    • 对象优先在Eden中分配
    • 大对象直接进入老年代
    • 长期存活的对象进入老年代
    • 动态对象年龄判定
    • 空间分配担保
  • 执行构造块(优先)和构造器为属性赋初始值
    • 如果父类构造器中调用了非静态方法,同时子类重写了该方法,则创建子类对象时,子类构造器中调用super实例化父类对象时,父类构造器调用子类重写后的方法,因为非静态方法前面有一个默认的对象this,构造器中this表示正在创建的对象,而此时正在创建子类对象,所以调用子类重写后的方法。
  • 构造块与非静态成员属性谁在前谁先执行

垃圾回收(Garbage Collection,GC)

判断对象是否需要被回收

引用计数法(Reachability Counting)
  • 通过在对象头中分配一个空间来保存该对象被引用的次数(Reference count)。如果该对象被其他对象引用,则它的引用计数+1,如果删除对该对象的引用,那么它的引用计数-1,当该对象的引用计数为0,那么该对象就会被回收。
  • 弊端:循环引用
可达性分析法(Reachability Analysis)

在这里插入图片描述

  • 通过一些被称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为Reference Chain,当一个对象到GC Roots没有任何引用链相连时(即从GCRoots节点到该对象不可达),则证明该对象是不可用的。
GC Roots
  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(一般说的Native方法)引用的对象

回收对象之前判断是否有必要执行finalize方法

  • finalize方法在对象被垃圾回收之前执行且仅执行一次
  • 如果该对象执行过了finalize方法或者没有重写该方法,那么认为该对象没有必要执行finalize方法,将其回收
  • 如归该对象重写了finalize方法并且没有执行过,那么会将其抽离到F-Queue队列,由一个低优先级线程执行该队列中对象的finalize方法

回收之后的内存处理

标记清理算法(Mark-Sweep)
  • 把内存去榆中可回收的对象标记出来,然后把这些垃圾清理掉,清理掉之后就出现了未使用的内存区域,等待再次被使用。
  • 优点:逻辑清晰,操作方法
  • 缺点:产生过多内存碎片
标记复制算法(Copying)
  • 将可用的内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,将还存活的对象复制到另一块上面,然后再把已经使用过的内存空间一次性清理掉
  • 优点:保证率内存的连续可用,内存分配时不用考虑内存碎片等复杂情况,逻辑清晰,运行高效
  • 缺点:浪费很多堆空间
标记整理算法(Mark-Compact)
  • 标记过程与标记清理算法一样,让所有存活的对象都向一端移动,在清理掉端边界以外的内存区域
  • 优点:解决了内存碎片、内存利用率低等问题
  • 缺点:内存频繁变动,整理所有存活对象的引用地址,效率低
分代收集算法(Generational Collection)
  • 融合了上述三种基础的算法思想,而产生的针对不同情况所采用的不同算法的一套组合

堆内存模型与回收策略

  • 新生代(1/3)
    • Eden(8/10)
      • 大多数情况下,对象会在新生代Eden区进行分配,当Eden区没有足够空间进行分配时,JVM会发起一次minor GC,minor GC相比major GC更频繁,回收速度更快
      • minor GC之后,Eden清空,绝大部分对象被回收,存活的对象进入Survivor From区(若空间不够,则直接接入Old)
      • 一般采用复制算法
    • Survivor
      • Survivor From(1/10)
      • Survivor To(1/10)
      • Eden与Old之间的缓冲区,分为两个区,每次执行minor GC,将Eden和其中一个区的存活对象复制到另一区(如果空间不够,直接方法Old)
      • 存在意义:减少被送到老年代的对象,进而减少Major GC的发生,只有经历过15次minor GC 还能在新生代存活的对象,才会被送到Old
      • 分2区的意义:解决内存碎片化。每次Minor GC将之前Eden和From中存活的对象复制到To。下一次From与To职责对换,永远有一个Survivor是空的,另一个非空是无碎片的。
    • 老年代(Old)
      • 由标记清除或者是标记清除与标记整理的混合实现

垃圾回收器

种类

Java的GC回收类型:

  • UseSerialGC
  • UseParallelGC
  • UseParNewGC
  • UseSerialOldGC
  • UseParallelOldGC
  • UseConcMarkSweepGC
  • UseG1GC
    在这里插入图片描述

新生代

串行Serial/Serial Coping

一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有工作线程直到它收集结束。
在这里插入图片描述

串行收集器是最古老,最稳定以及效率高的收集器,只使用一个线程去回收但其在垃圾收集过程中会产生较长时间的停顿(stop-the-world状态),尽管如此,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销可以获得最高的效率。因此Serial垃圾收集器仍然是JVM在client模式下默认的新生代垃圾收集器。

对应的JVM参数:-XX:+UseSerialGC
开启后会使用:Serial+Serial Old的收集器组合
表示新生代和老年代都会使用串行收集器,新生代使用复制算法,老年代使用标记整理算法。

并行 ParNew

使用多线程进行垃圾回收,在垃圾收集时,会stop-the-world暂停其他所有工作线程直到它结束。

ParNew其实就是Serial并行多线程版本,最常见的应用场景是配合CMS工作,其余行为和Serial完全一样。ParNew在垃圾收集的过程中也需要暂停所有工作线程。它是JVM在server模式下默认的新生代垃圾回收器。

对应JVM参数:-XX:+UseParNewGC
在这里插入图片描述

并行回收Parallel/Parallel Scavenge

在这里插入图片描述
Parallel Scavenge类似ParNew也是一个新生代垃圾回收器,使用复制算法,也是一个并行的多线程垃圾收集器,俗称吞吐量优先收集器。也就是串行收集器在新生代和老年代的并行化

关注点不同,CMS 等垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,因为停顿时间越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。

Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集时间的 -XX:MaxGCPauseMillis 参数及直接设置吞吐量大小的 -XX:GCTimeRatio(默认99%)

除了以上两个参数,还可以用 Parallel Scavenge 收集器提供的第三个参数 -XX:UseAdaptiveSizePolicy,开启这个参数后,就不需要手工指定新生代大小,Eden 与 Survivor 比例(SurvivorRatio)等细节,只需要设置好基本的堆大小(-Xmx 设置最大堆),以及最大垃圾收集时间与吞吐量大小,虚拟机就会根据当前系统运行情况收集监控信息,动态调整这些参数以尽可能地达到我们设定的最大垃圾收集时间或吞吐量大小这两个指标。自适应策略也是 Parallel Scavenge 与 ParNew 的重要区别!

老年代

串行Serial Old/Serial MSC

Serial Old是Serial的老年代版本,同样是个单线程的收集器,使用标记整理算法,也是JVM在client模式在默认的老年代垃圾回收器。
在这里插入图片描述

并行Parallel Old/Parallel MSC

Parallel Old是Parallel的老年代版本,使用多线程的标记整理算法。
在这里插入图片描述

并发标记清除 CMS

CMS(Concurrent Mark Sweep:并发标记清除)是一种以获取最短回收停顿时间为目标的垃圾回收器。适合在互联网或者B\S系统的服务器上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短。CMS非常适合内存大,CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。
在这里插入图片描述
对应JVM参数:-XX:UseConcMarkSweepGC,开启该参数会自动开启
-XX:UseParNewGC,开启后使用**ParNew+CMS+Serial Old(后备)**组合

  • 初始标记CMS initial Mark

  • 并发标记CMS concurrent Mark
    进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。此过程为主要标记过程,标记全部对象。

  • 重新标记CMS remark
    为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

  • 并发清除CMS concurrent sweep
    清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程,基于标记结果,直接清理对象。

由于耗时最长的并发标记和并发清理过程中,垃圾收集线程可以和用户线程一起并发工作,所以总体上来看CMS是并发执行的。

  • 优点:并发收集低停顿
  • 缺点:
    • 并发执行,对CPU压力大
    • 采用标记清除算法导致碎片

G1/Garbage-First

G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。

特性:

  • 同CMS一样,与应用程序线程并发执行。
  • 整理空闲空间更快
  • 需要更多的时间来预测GC停顿时间
  • 不希望牺牲大量的吞吐性能
  • 不需要更大的java heap
  • 设计目标是取代CMS
  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片
  • G1的stop-the-world更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。
Region区域化垃圾收集器

最大好处是化整为零,避免全内存扫描,只需要按照区域来进行扫描即可。

核心思想是将整个堆内存区域分成大小相同的region,在JVM启动时会自动设置这些region的大小,在堆的使用上,G1并不要求对象的存储一定是物理上的连续,只要逻辑上连续即可,每个分区也不会固定为某个代服务,可以按需在年轻代和老年代之间切换,启动时可以通过参数 -XX:G1HeapRegionSize=n指定region大小(1-32m,2的幂次方),默认2048个region。也即能支持的最大内存为32m×2048=64g
在这里插入图片描述
在这里插入图片描述

回收步骤

针对Eden区进行收集,Eden区耗尽后被触发,只要是小区域收集+形成连续的内存块,避免内存碎片。
在这里插入图片描述

  • 初始标记:只标记GC Roots能直接关联到的对象
  • 并发标记:进行GC Roots Tracing的过程
  • 最终标记:修正并发标记期间,因程序运行导致的变化
  • 筛选标记:根据时间来进行价值最大化的回收
    在这里插入图片描述
常用配置参数
# 使用G1垃圾回收器
-XX:+UseG1GC
# 设置G1区域的大小。值是2的幂,范围是1M到32M。目标是根据最小的Java堆大小划分出约2048个区域
-XX:G1HeapRegionSize=32m
# 最大停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿时间小于这个时间
-XX:MaxGCPauseMillis=n
# 堆占用了多少的时候就触发GC,默认是45
-XX:InitiatingHeapOccupancyPercent=n
# 并发GC使用的线程数
-XX:ConcGCThreads=n  
# 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
-XX:G1ReservePercent=n
与CMS相比的优势
  • 不会产生内存碎片
  • 可以精确控制停顿。G1把堆划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

如何选择垃圾收集器

  • 单CPU或小内存,单机程序
    • -XX:+UseSerialGC
  • 多CPU,需要最大吞吐量,如后台计算型应用
    • -XX:+UseParallelGC
  • 多CPU,追求低停顿时间,需要快速响应的互联网应用
    • -XX:+UseParNewGC
    • -XX:+UseConcMarkSweepGC
    • -XX:+UseG1GC

在这里插入图片描述

对象终结

卸载类型

JVM参数配置及调优

参数类型

标配参数

-version
-help
java -showversion 

X参数

-Xint 解释执行
-Xcomp 第一次使用编译成本地代码
-Xmixed 混合模式

XX参数

Boolean类型
-XX:±	#+表示开启,-表示关闭

# 是否打印GC细节
-XX:±PrintGCDetails
KV设值类型
-XX:key=value

# 设置元空间大小
-XX:MetaspaceSize=128m
jps和jinfo命令查看当前运行程序配置
# 查看运行程序id
jps -l

# 查看具体配置
jinfo -flag 配置项 进程编号
jinfo -flags 进程编号

查看JVM默认值

# 查看初始默认值
java -XX:+PrintFlagsInitial 

# 主要查看修改更新
java -XX:+PirntFlagsFinal

# 查看常用项
java -XX:+PrintCommandLineFlags

常用参数

# 初始大小内存,默认为物理内存1/64
-Xms
-XX:InitialHeapSize

# 最大分配内存,默认为物理内存1/4
-Xmx
-XX:MaxHeapSize

# 设置单个线程的大小,一般默认为512K~1024K
-Xss
-XX:ThreadStackSize

#设置年轻代大小
-Xmn

# 设置元空间大小
-XX:MetaspaceSize

# 输出详细GC收集日志信息
-XX:+PrintGCDetails

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

# 设置新生代中Eden和S0/S1的空间比例
-XX:SurvivoRatio=8

#设置老年代与新生代的空间比例
-XX:NewRatio=2

# 设置垃圾最大年龄
-XX:MaxTenuringThreshold=15

强引用、软引用、弱引用、虚引用

在这里插入图片描述

强引用:

即使出现OOM,强引用的对象也不会被回收,Java默认支持格式

软引用

垃圾回收时,内存够用就保留,不够就回收。

Object obj=new Object();
SoftReference<Object> softReference=new SoftReference<>(obj);
softReference.get();

弱引用

发生垃圾回收,就会被回收

WeakHashMap

WeakHashMap中的key为弱引用,垃圾回收时,WeakHashMap中的对象会被回收

软/弱引用的应用场景

当一个应用需要读取大量本地图片时,可以用HashMap来映射图片路径和图片bitmap

Map<String,SoftReference<Bitmap>> images=new HashMap<>();

虚引用

无论何时get()都为null,在垃圾回收时被回收,加入到ReferenceQueue

OOM

Java.lang.StackOverflowError

  • 造成原因
    • 无限递归循环调用(最常见原因),要时刻注意代码中是否有了循环调用方法而无法退出的情况
    • 执行了大量方法,导致线程栈空间耗尽
    • 方法内声明了海量的局部变量
    • native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)
public class StackOverflowErrorDemo {

    public static void main(String[] args) {
        test();
    }

    private static void test() {
        test();
    }
}
  • 解决方法
    • 修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug
    • 排查是否存在类之间的循环依赖(当两个对象相互引用,在调用toString方法时也会产生这个异常)
    • 通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制

Java.lang.OutOfMemoryError:Java heap space

  • 原因
    • 请求创建一个超大对象,通常是一个大数组
    • 超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值
    • 过度使用终结器(Finalizer),该对象没有立即被 GC
    • 内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收
/**
 * JVM参数:-Xmx1m
 */
public class JavaHeapSpaceDemo {

    static final int SIZE = 2 * 1024 * 1024;

    public static void main(String[] a) {
        int[] i = new int[SIZE];
    }
}
  • 解决
    • 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制
    • 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。
    • 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接

Java.lang.OutOfMemeoryError:GC overhead limit exceeded

  • 原因
    • 程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高。
/**
 * JVM 参数: -Xmx1m -XX:+PrintGCDetails
 */
public class KeylessEntry {

    static class Key {
        Integer id;

        Key(Integer id) {
            this.id = id;
        }
    }

    public static void main(String[] args) {
        Map m = new HashMap();
        while (true){
            for (int i = 0; i < Integer.MaxValue; i++){
                    m.put(new Key(i), "Number:" + i);
            }
            System.out.println("m.size()=" + m.size());
        }
    }
}
  • 解决
    • 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码
    • dump内存分析,检查是否存在内存泄露,如果没有,加大内存

Java.lang.OutOfMemeoryError:Direct buffer memory

使用 NIO 的时候经常需要使用 ByteBuffer 来读取或写入数据,这是一种基于 Channel(通道) 和 Buffer(缓冲区)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景就避免了 Java 堆和 Native 中来回复制数据,所以性能会有所提高。

  • ByteBuffer.allocate(capability) 是分配 JVM 堆内存,属于 GC 管辖范围,需要内存拷贝所以速度相对较慢;

  • ByteBuffer.allocateDirect(capability) 是分配 OS 本地内存,不属于 GC 管辖范围,由于不需要内存拷贝所以速度相对较快;

如果不断分配本地内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象就不会被回收,这时虽然堆内存充足,但本地内存可能已经不够用了,就会出现 OOM,本地直接内存溢出。

/**
 *  VM Options:-Xms10m,-Xmx10m,-XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m(默认是主机内存1/4)
 */
public class DirectBufferMemoryDemo {

    public static void main(String[] args) {
        System.out.println("maxDirectMemory is:"+sun.misc.VM.maxDirectMemory() / 1024 / 1024 + "MB");

        //ByteBuffer buffer = ByteBuffer.allocate(6*1024*1024);
        ByteBuffer buffer = ByteBuffer.allocateDirect(6*1024*1024);

    }
}
  • 解决
    • Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查
    • 检查是否直接或间接使用了 NIO,如 netty,jetty 等
    • 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值
    • 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效
    • 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间
    • 内存容量确实不足,升级配置

Java.lang.OutOfMemeoryError:unable to create new native thread

  • 原因

    • 线程数超过操作系统最大线程数限制(和平台有关)
    • 线程数超过 kernel.pid_max(只能重启)
    • native 内存不足;该问题发生的常见过程主要包括以下几步:
      • JVM 内部的应用程序请求创建一个新的 Java 线程;
      • JVM native 方法代理了该次请求,并向操作系统请求创建一个 native 线程;
      • 操作系统尝试创建一个新的 native 线程,并为其分配内存;
      • 如果操作系统的虚拟内存已耗尽,或是受到 32 位进程的地址空间限制,操作系统就会拒绝本次 native 内存分配;
      • JVM 将抛出 java.lang.OutOfMemoryError:Unableto createnewnativethread 错误。
  • 解决

    • 想办法降低程序中创建线程的数量,分析应用是否真的需要创建这么多线程
    • 如果确实需要创建很多线程,调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制

Java.lang.OutOfMemeoryError:Metaspace

JDK 1.8 之前会出现 Permgen space,该错误表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。随着 1.8 中永久代的取消,就不会出现这种异常了。

Metaspace 是方法区在 HotSpot 中的实现,它与永久代最大的区别在于,元空间并不在虚拟机内存中而是使用本地内存,但是本地内存也有打满的时候,所以也会有异常。

方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景中,就应该特别关注这些类的回收情况。这类场景除了 GCLib 字节码增强和动态语言外,常见的还有,大量 JSP 或动态产生 JSP 文件的应用(远古时代的传统软件行业可能会有)、基于 OSGi 的应用(即使同一个类文件,被不同的加载器加载也会视为不同的类)等。

方法区在 JDK8 中一般不太容易产生,HotSpot 提供了一些参数来设置元空间,可以起到预防作用

-XX:MaxMetaspaceSize 设置元空间最大值,默认是 -1,表示不限制(还是要受本地内存大小限制的)
-XX:MetaspaceSize 指定元空间的初始空间大小,以字节为单位,达到该值就会触发 GC 进行类型卸载,同时收集器会对该值进行调整
-XX:MinMetaspaceFreeRatio 在 GC 之后控制最小的元空间剩余容量的百分比,可减少因元空间不足导致的垃圾收集频率,类似的还有 MaxMetaspaceFreeRatio

java.lang.OutOfMemoryError: Requested array size exceeds VM limit

  • JVM 在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为 Integer.MAX_VALUE-2。
public static void main(String[] args) {
  int[] arr = new int[Integer.MAX_VALUE];
}
  • 检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行。

内存泄漏与内存溢出的区别

  • 内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个 Integer,但给它存了 Long 才能存下的数,那就是内存溢出。

  • 内存泄露( memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

memory leak 最终会导致 out of memory!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值