【jvm-1】内存管理

本文介绍JVM内存结构,包括程序计数器、虚拟机栈、堆、方法区和本地方法栈的功能与作用,并探讨内存溢出的原因及解决方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一:整体架构

1、内存结构理论

JVM分为五大模块:类装载器子系统 运行时数据区 执行引擎 本地方法接口 垃圾收集模块。
 

而我们所说的jvm内存管理,就是针对运行时数据区。根据jvm规范,内存可划分为五大部分:

注意: 

  • 方法区在jdk7中的体现是永久代,存在于jvm内存中。 而在jdk8中的体现是元空间,转移到了本地内存中,已经不属于jvm内存范畴了。
  • 因此,永久代PermGen中的类元信息转移到了metaspace中,而其他的常量池信息,静态变量等转移到了堆中。

为什么要替换呢?

  • 字符串存在永久代难回收,容易出现性能问题和内存溢出。
  • 类信息和方法信息比较难确定大小,因此不好分配永久代大小。 太小容易溢出,太大则浪费不必要空间,可能造成老年代溢出。
  • 永久代GC复杂,回收率低。

 

下面来看看jvm内存具体划分:

程序计数器:

也叫pc寄存器,线程私有。 程序在执行时候,是通过执行一行行的字节码指令,通过改变和记录计数器的值,就能知道线程执行到哪一行代码了。

虚拟机栈:

线程私有,与线程同时创建,用于存储栈帧。每一个方法调用到执行完成,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。

  • 栈帧:

一个栈帧包含一个执行方法的内容,包括:局部变量,操作数栈,动态链接,方法返回地址

可以使用-Xss设置jvm启动每个线程时,为其分配的内存大小。不过一般不会手动设置,默认1M。 下面看一下栈帧中的内容:

  • 局部变量表

存放方法参数,方法内内定义的局部变量。 包括:基本数据类型,对象引用(reference类型)和 returnAddress类型(指向一条字节码指令的地址,这样才会知道方法结束后,线程下一次执行哪一条字节码)。

  • 操作数栈

也是一个栈结构,就是方法执行中的操作计算过程。 随着字节码指令的执行,会从局部变量表等,复制常量或者变量写入操作数栈,用于计算,再将结果出栈到局部变量表或调用者。

如图执行100 + 98的过程:

iload_0: 加载局部变量表0位置的100到操作数栈;

iload_1: 加载局部变量表1位置的98到操作数栈;

iadd:相加结果为198;

istore_2:结果出栈存储到局部表里表。

  • 动态连接:

Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池

那么,如果要描述方法A调用了方法B,就是通过常量池中的符号引用来表示的(比如符号为#1,表示调用了#1对应的方法)。

而在栈帧中,就有这样的符号引用,在运行时找到真正的方法或变量,转换为直接的引用。 这就是动态链接。

  • 方法返回地址

存放调用该方法的pc寄存器值,在方法结束时返回,以便知道执行下一条指令。

堆:

  • 内存大小设置:-Xmx/-Xms
  • 堆内存划分:
    • 年轻代(Young Gen):
      • 年轻代用来存放新创建的对象,内存较小,GC相对频繁
      • 分为eden、survivor0 和 survivor1三个区。第1次gc时,将eden中存活的对象放到survivor0,清空eden。 第2次gc,将eden和survivor0中存活的对象放到survivor1,清空eden和survivor0。如此反复,两个survivor相互交换。 当存活对象达到一定gc次数后,转移到老年代。 
    • 老年代(Tenured Gen)内存较大,回收不频繁。
  • 设置新生代和老年代大小: 默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3。  -XX:SurvivorRatio=8 代表Eden空间和另外两个Survivor空间占比分别为8:1:1

元空间Metaspace方法区落地的一部分,存储类的元数据信息。 

  • -XX:MetaspaceSize,初始空间大小,达到该值后会扩容
  • XX:MaxMetaspaceSize,最大空间。 如果不设置,会无限增大,知道耗尽物理内存。
 
另外,在gc之后,也会对根据空闲空间占比,动态调整元空间大小。
  • -XX:MinMetaspaceFreeRatio: gc后,如果空闲空间的百分比小于该值,就会调高元空间的大小。默认40(也就是40%)
  • -XX:MaxMetaspaceFreeRatio:gc后,如果空闲空间的百分比大于该值,就会调低元空间的大小。默认70

本地方法栈:

线程私有,为了调用Native方法用的。

 

我们再来看一下几个概念:

  • 方法区
    • 方法区就是在class文件加载到内存后,存储类的信息,域信息,方法信息,常量池等,包括修饰符public,private,还有方法的返回类型什么的。
    • 在jdk7及以前,专门在jvm内存种开辟了一个空间,在jdk8后,将类信息等移到了本地内存metaspace中,常量池信息归到了堆内存中。
  • 运行时常量池
    • 常量池是存放编译期间生成的各种字面量与符号引用。 而运行时常量池,就是这些常量在运行时,转为直接引用的表现形式。
  • 直接内存

 

2、内存溢出实战

堆内存:

设置最大堆最小堆:-Xms20m -Xmx20m

public class HeapOOM {
    static class OOMObject {
    }

    public static void main(String[] args) {
        List<OOMObject> oomObjectList = new ArrayList<>();
        while (true) {
            oomObjectList.add(new OOMObject());
        }
    }
}

不断创建对象,java.lang.OutOfMemoryError: Java heap space 异常。 

  • 新生代满后会进行一次 Minor GC
  • 如果 Minor GC 后空间不足会把该对象 或 新生代满足条件的对象放入老年代,老年代空间不足时会进行 Full GC
  • 如果空间还不足则抛出 OutOfMemoryError 异常

 

实际中产生的原因:

  1. 一次从加载过多数据到内存
  2. 代码不合理,集合引用太多对象没有及时清空,或者死循环不断创建对象
  3. 堆内存分配不合理

栈内存:

大小设置:-Xss128k

栈内存是线程私有,在创建线程时候确定。 所以栈内存不足有两种表现形式:

  1. 线程创建成功(栈深度申请成功),执行过程中栈深度不足:java.lang.StackOverflowError  。 这种情况通常是递归调用造成
  2. 线程创建失败(申请栈深度时,内存不足): java.lang.OutOfMemoryError  ,内存溢出。 比如多线程申请时候

对于实际项目,一般情况下关注以上两个就可以了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值