Java内存模型和常见的内存溢出类型及解决方案

2 篇文章 0 订阅

一. Java 运行过程和内存分配

1 Java的平台无关性

JVM实现了Java语言最重要的特征:平台无关性。原理:编译后的 Java 程序指令并不直接在硬件系统的 CPU 上执行,而是由 JVM 执行。JVM屏蔽了与具体平台相关的信息,使Java语言编译程序只需要生成在JVM上运行的目标字节码(.class),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行,因此实现java平台无关性。

在这里插入图片描述

2 Java内存模型

2.1Java内存模型图

JVM(Java Virtual Machine)是一个抽象的计算模型。它有自己的指令集和执行引擎,可以在运行时操控内存区域。目的是为构建在其上运行的应用程序提供一个运行环境。JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。
在这里插入图片描述

名称作用
元空间(Meta Area)被虚拟机加载的类信息、即时编译器编译后的代码等数据,元空间直接占用主内存(NativeMemory)
堆区(Heap)用来存储对象实例(几乎所有的对象都在这分配内存),被所有线程共享的一块内存区域,在App启动时创建,堆区的数据放在主内存
虚拟机栈区(VM Stack)存储方法的局部变量,每次开启一个线程都会创建一个虚拟机栈,线程私有,生命周期与线程相同;栈区的数据存放在高速缓存区。java方法执行的内存模型,每个方法执行的时候,都会创建一个栈帧用于保存局部变量表,操作数栈,动态链接,方法出口信息等。一个方法调用的过程就是一个栈帧从VM栈入栈到出栈的过程。
本地方法栈(Native Stack)与VM栈发挥的作用非常相似,VM栈执行java方法(字节码)服务,Native方法栈执行的是Native方法服务。
程序计数器每条线程都需要有一个程序计数器,计数器记录的是正在执行的指令地址,如果正在执行的是Native方法,这个计数器值为空(Undefined)。
执行引擎(Execution Engine)将方法区中对应方法的arm指令集加载到栈区,而栈区存在于高速缓冲区中,cpu是直接从高速缓冲区取arm指令,一条一条执行。执行引擎就像一个中介,方法对应的arm指令相当于交易的物品

3 内存结构详解

3.1 元空间(MetaSpace)

jdk1.7及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区也叫“非堆”(Non-heap)或者“永久代”(PermGen sapce)
方法区是线程共享的内存区域,主要存放了被JVM加载的类信息(class字节码的信息)、常量、静态变量、即时编译后的代码等数据。方法区的数据放在主内存。
在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。

去方法区(永久代:PermGen)

从jdk1.8已经开始“去永久代”,方法区已经不存在。
原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等,这里只是把字符串常量池移到堆内存中)

去永久代的原因

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

元空间与永久代之间最大的区别

元空间并不在虚拟机中,而是使用本地内存。因此默认情况下元空间的大小仅仅受本地内存的大小限制。类的元数据放入 native memory,
字符串池和类的静态变量放入java堆中。 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制

3.2 堆区(Heap)

堆是Java虚拟机管理的最大的一块内存,Java堆是被所有的线程共享的一块区域。在虚拟机启动时创建,其唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此很多时候又被称为GC堆,Java堆可以细分为新生代和老年代

新生代

主要是用来存放新创建的对象。一般占据堆空间的1/3,由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收
新生代分为Eden区、ServivorFrom、ServivorTo三个区
当JVM无法为新建对象分配内存空间的时候(Eden区满的时候),JVM触发MinorG(采用复制算法)。因此新生代空间占用越低,MinorGc越频繁

名称作用
Eden区Java新对象的出生地(如果新创建的对象占用内存很大则直接分配给老年代)。当Eden区内存不够的时候就会触发一次MinorGc,对新生代区进行一次垃圾回收
ServivorTo保留了一次MinorGc过程中的幸存者
ServivorFrom上一次GC的幸存者,作为这一次GC的被扫描者
老年代

老年代的对象比较稳定,所以MajorGC不会频繁执行。
在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。

MajorGC采用标记—清除算法

  1. 首先扫描一次所有老年代,标记出存活的对象
  2. 然后回收没有标记的对象。 MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

3.3 虚拟机栈(VM Stack)

  1. Java虚拟机栈是线程私有,用来存储栈帧
  2. 虚拟机栈是线程执行的区域,它保存着一个线程中方法的调用状态。
  3. 每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧。 而栈帧持有局部变量和部分结果以及参与方法结果的调用语法,当方法调用结束以后,栈帧才会被销毁.:虚拟机栈包含了单个线程每个方法执行的栈帧,而栈帧则存储了局部变量表,关键数栈,动态链接和方法出口等信息

在这里插入图片描述

局部变量表

包含了方法执行过程中的所有变量,存放了编译期可知的各种数据类型。例如: Boolean、byte、char、short、int、float、long、double、对象引用类型(对象内存地址变量,指针或句柄)。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。

操作数栈

操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。

动态链接

每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。

返回地址

如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。

3.4 本地方法栈(Native Method Stack)

与虚拟机栈发挥的作用非常相似,VM栈执行java方法(字节码)服务,Native方法栈执行的是Native方法服务。
官方文档:

  • An implementation of the Java Virtual Machine may use conventional stacks, colloquially called “C stacks,” to support native methods (methods written in a language other than the Java programming language). Native method stacks may also be used by the implementation of an interpreter for the Java Virtual Machine’s instruction set in a language such as C. Java Virtual Machine implementations that cannot load native methods and that do not themselves rely on conventional stacks need not supply native method stacks. If supplied, native method stacks are typically allocated per thread when each thread is created.

  • This specification permits native method stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the native method stacks are of a fixed size, the size of each native method stack may be chosen independently when that stack is created.

  • A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the native method stacks, as well as, in the case of varying-size native method stacks, control over the maximum and minimum method stack sizes.

  • The following exceptional conditions are associated with native method stacks:
    If the computation in a thread requires a larger native method stack than is permitted, the Java Virtual Machine throws a StackOverflowError.
    If native method stacks can be dynamically expanded and native method stack expansion is attempted but insufficient memory can be made available, or if insufficient memory can be made available to create the initial native method stack for a new thread, the Java Virtual Machine throws an OutOfMemoryError.

3.5 程序计数器(Program Counter Register)

  • 可以看作是当前线程所执行的字节码行号指示器(逻辑上),也就是说,程序计数器是逻辑计数器,而不是物理计数器.其工作时就是改变计数器的值来选取下一条需要执行的字节码指令,包括跳转等基础功能。由于只是记录行号,程序计数器不会发生内存泄露的问题

  • 由于JVM的多线程是通过线程的轮流切换并分配处理器执行时间的方式来实现的,在任何一个确认的时刻,一个处理器只会执行一条线程中的指令.因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程的计数器之间互不影响,独立存储,我们就称这种为线程私有的内存

  • 如果线程正在执行一个Java方法,这个计数器是记录正在执行的虚拟机字节码指令的地址,如果正在执行的native方法,那么这个计数器值就是undefined

二.内存溢出类型

内存溢出的类型

JVM提供的内存管理机制自动垃圾回收极大的解放了用户对于内存的管理,大部分情况下不会出现内存泄漏和内存溢出问题。但是出现不同的内存溢出错误可能会发生在内存模型的不同区域,所以需要根据出现错误的代码分析出现的内存溢出或内存泄漏

Java中内存溢出常见于如下的几种情形:

  • 栈内存溢出(StackOverflowError)

  • 堆内存溢出(OutOfMemoryError:java heap space)

  • 永久代溢出(OutOfMemoryError:PermGen sapce)

1. 栈内存溢出(StackOverflowError)

1.1 栈内存说明

在这里插入图片描述
栈内存可以分为虚拟机栈(VM Stack)和本地方法栈(Native Method Stack),除了它们分别用于执行Java方法(字节码)和本地方法,其余部分原理是类似的(以虚拟机栈为例说明)。Java虚拟机栈是线程私有的,当线程中方法被调度时,虚拟机会创建用于保存局部变量表、操作数栈、动态连接和方法出口等信息的栈帧(Stack Frame)

当线程执行某个方法时,JVM会创建栈帧并压栈,此时刚压栈的栈帧就成为了当前栈帧。如果该方法进行递归调用时,JVM每次都会将保存了当前方法数据的栈帧压栈,每次栈帧中的数据都是对当前方法数据的一份拷贝。如果递归的次数足够多,多到VM栈中栈帧所使用的内存超出了栈内存的最大容量,此时JVM就会抛出StackOverflowError

1.2栈溢出demo

StackOverflowErrorDemo.java

package com.cxstar.demo;

/**
 * @author zhouquan
 * @description 栈溢出测试
 * @date 2023年6月4日10:27:20
 **/
public class StackOverflowErrorDemo {

    private static int stackLength = 0;

    public void pusStack() {
        stackLength++;
        pusStack();
    }

    public static void main(String[] args) {
        StackOverflowErrorDemo demo = new StackOverflowErrorDemo();
        try {
            demo.pusStack();
        } catch (Throwable e) {
            System.out.println("stack length: " + demo.stackLength);
            throw e;
        }
    }

}

执行结果:

stack length: 19625
Exception in thread "main" java.lang.StackOverflowError
	at com.cxstar.demo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:23)
	at com.cxstar.demo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:23)
	at com.cxstar.demo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:23)
	at com.cxstar.demo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:23)
	at com.cxstar.demo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:23)
	at com.cxstar.demo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:23)

1.3栈区参数配置说明

官方文档可以看到: 对于Windows操作系统, 其默认值取决于虚拟内存的大小.
对于Linux系统或者OS X 系统, 默认值是1024KB

参数名含义
-Xss设置单个线程栈的大小, 一般默认为512K~1024K,其等价于 -XX:ThreadStackSize

2.堆内存溢出(OutOfMemoryError:java heap space)

堆内存的唯一作用就是存放数组和对象实例,即通过new指令创建的对象,包括数组和引用类型

2.1堆内存溢出

当堆中对象实例所占的内存空间超出了堆内存的最大容量,JVM就会抛出OutOfMemoryError:java heap space异常
如果是因为堆内存空间太小,可以通过改变-Xmx来进行调整,或者分析程序中对象的生命周期和存储结构等信息进行调整;

2.2 堆内存溢出场景

最近客户反馈在生产环境导入操作时遇到任务一直执行中,(导入时需要对pdf进行封面提取和pdf加密,且pdf最大可能会超过500MB)并且入库的数据量一直不改变。通过日志查询,终于定位到报错信息如下:

java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source) ~[na:1.8.0_221]
	at java.io.ByteArrayOutputStream.grow(Unknown Source) ~[na:1.8.0_221]
	at java.io.ByteArrayOutputStream.ensureCapacity(Unknown Source) ~[na:1.8.0_221]
	at java.io.ByteArrayOutputStream.write(Unknown Source) ~[na:1.8.0_221]
	at com.cxstar.common.utils.DESUtil.decryptFile(DESUtil.java:193) ~[cxstar-common-1.0.0.jar!/:1.0.0]
	at com.cxstar.business.service.impl.ArchiveServiceImpl.upload(ArchiveServiceImpl.java:103) ~[cxstar-business-1.0.0.jar!/:1.0.0]
	at com.cxstar.business.factory.AsyncFactory.importAttachment(AsyncFactory.java:1016) ~[cxstar-business-1.0.0.jar!/:1.0.0]
	at com.cxstar.business.factory.AsyncFactory.access$800(AsyncFactory.java:62) ~[cxstar-business-1.0.0.jar!/:1.0.0]
	at com.cxstar.business.factory.AsyncFactory$7.run(AsyncFactory.java:708) ~[cxstar-business-1.0.0.jar!/:1.0.0]
	at java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source) ~[na:1.8.0_221]
	at java.util.concurrent.FutureTask.run(Unknown Source) ~[na:1.8.0_221]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(Unknown Source) ~[na:1.8.0_221]
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Unknown Source) ~[na:1.8.0_221]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [na:1.8.0_221]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [na:1.8.0_221]
	at java.lang.Thread.run(Unknown Source) [na:1.8.0_221]
问题分析与解决

java.lang.OutOfMemoryError: Java heap space (JVM 堆空间溢出)简单来说就是在创建新的对象时, 堆内存中的空间不足以存放新创建的对象,导致此种问题的发生。

1.优化项目代码

根据报错信息定位到内存消耗较大的代码,然后对其进行重构或者优化算法。如果是在生产环境,务必要在内存消耗过大的代码出增加日志信息输出,否则容易像我定位一晚上才找到问题所在
在这里插入图片描述

2.提升Java heap size

增加堆内存空间设置

-server -Xms4096m -Xmx4096m -XX:PermSize=256M -XX:MaxNewSize=512m -XX:MaxPermSize=512m

客户生产环境的服务器是8g内存,并且操作系统是比较老的windows server 2008服务器。在部署项目时使用winsw工具将jar包安装为服务。因此在项目启动时,增加内存参数,修改后配置如下:

<service>
    <id>app-id</id>
    <name>app-name service</name>
    <description>application descriptions</description>

	<!-- 配置的启动:使用jar -jar方式运行项目 -->
    <executable>java</executable>
    <arguments>-jar -server -Xms4096m -Xmx4096m -XX:PermSize=256M -XX:MaxNewSize=512m -XX:MaxPermSize=512m  "D:\application\api-rest-1.0.0.jar"</arguments>

    <startmode>Automatic</startmode>
    <logpath>%BASE%\logs</logpath>
    <logmode>rotate</logmode>
</service>

2.3 堆内存泄露

当堆中一些对象不再被引用但垃圾回收器无法识别时,这些未使用的对象就会在堆内存空间中无限期存在,不断的堆积就会造成内存泄漏。
如果发生了内存泄漏,则可以先找出导致泄漏发生的对象是如何被GC ROOT引用起来的,然后通过分析引用链找到发生泄漏的地方。

3.永久代溢出(OutOfMemoryError:PermGen sapce)

我们知道Hotspot jvm通过永久带实现了Java虚拟机规范中的方法区,而运行时的常量池就是保存在方法区中的,因此永久带溢出有可能是运行时常量池溢出,也有可能是方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置

当持久带溢出的场景下出现

  1. 使用一些应用服务器的热部署的时候,我们就会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。
  2. 如果应用程序本身比较大,涉及的类库比较多,但是我们分配给持久带的内存(通过-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。
  3. 一些第三方框架,比如spring,hibernate都通过字节码生成技术(比如CGLib)来实现一些增强的功能,这种情况可能需要更大的方法区来存储动态生成的Class文件。

4.JVM参数配置

配置参考

  • 服务器内存8G ,所以可以采取以下配置: set JAVA_OPTS=-server -Xms4096m -Xmx4096m
    -XX:PermSize=256M -XX:MaxNewSize=512m -XX:MaxPermSize=512m

  • 服务器内存4G ,所以可以采取以下配置: set JAVA_OPTS=-server -Xms2048m -Xmx2048m
    -XX:PermSize=256M -XX:MaxNewSize=512m -XX:MaxPermSize=512m

  • 服务器内存2G ,所以可以采取以下配置: set JAVA_OPTS=-server -Xms1024m -Xmx1024m
    -XX:PermSize=256M -XX:MaxNewSize=512m -XX:MaxPermSize=512m

  • 服务器内存1G ,所以可以采取以下配置: set JAVA_OPTS=-server -Xms512m -Xmx512m -XX:PermSize=128M
    -XX:MaxPermSize=256M -XX:MaxPermSize=256m

  • 服务器内存512M ,所以可以采取以下配置: set JAVA_OPTS=-server -Xms256m -Xmx256m
    -XX:PermSize=256M -XX:MaxNewSize=128m -XX:MaxPermSize=128m

堆区参数配置说明

堆区:通常会将-Xms 与-Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源

参数名含义
-Xmsjava虚拟机初始化时的最小内存
-Xmxjava虚拟机可使用的最大内存

非堆区:代码、常量、外部访问(比如流在传输数据时所占用的资源)等,java垃圾回收机制只作用于堆区,非堆区不会被java垃圾回收机制进行处理

参数名含义
-XX:newSize表示对象创建初始内存的大小,应小于-Xms的值
-XX:MaxnewSize表示对象创建可被分配的内存的最大上限,应小于-Xmx的值
-Xmnjdk1.4后对-XX:newSize、-XX:MaxnewSize两个参数同时进行配置

永久代:在配置之前一定要慎重的考虑一下自身软件所需要的非堆区内存大小,因为此处内存是不会被java垃圾回收机制进行处理的地方。并且更加要注意的是最大堆内存与最大非堆内存的和绝对不能够超出操作系统的可用内存。

参数名含义
-XX:PermSize表示非堆区初始内存分配大小(方法区)
-XX:MaxPermSize表示对非堆区分配的内存的最大上限(方法区)
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

徐州蔡徐坤

又要到饭了兄弟们

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值