04-内存分配

nobility 发布于 2021-07-13 2850 次阅读


内存分配

当一个Class文件通过类加载器加载到虚拟机后,就是存放在内存区域,该内存区域又叫运行时数据区,具体的说应该是存放在内存区的方法区中,内存区域主要包括以下几块内容:

  • 方法区(Method Area):通常用来保存加载的类结构信息,通常也和元空间关联在一起(具体跟JVM实现和版本有关),是所有线程共享的区域
    • JVM规范将方法区描述为一个逻辑部分,但有一个别名,非堆(Non-heap),为了与Java堆区分开
    • 运行时常量池(Constant Pool):是Class文件每个类或接口的常量池表,在运行期间的表示形式通常包括类的版本、字段、方法、接口等信息,通常在加载类和接口到JVM后就创建相应的运行时常量池
    • 代码缓存区(Code Cache):JVM将其字节码存储为本机代码的一块区域,主要放置的就是JIT(Just In Time)即时编译,或者是JNI(Java Native Interface)编译的用于和其他语言写的代码进行交互的代码
  • 虚拟机栈(VM Stack):虚拟机栈是由一系列栈帧组成,所以又叫帧栈,每个线程都有一个私有的帧栈;栈的存储速度比堆快,仅次于寄存器,但是栈中数据大小、生存期是在编译阶段决定、缺乏灵活性
    • 栈帧:用来保存方法的局部变量、操作数栈(Java没有寄存器,所以参数传递使用操作数栈)、常量池指针、动态连接、方法返回值等;每次方法调用都会创建一个帧压入栈中,方法退出时修改栈顶指针即可将栈帧内容销毁
    • 局部变量表:用来存放编译期可知的各种基本数据类型和引用数据类型,每个slot存放32位数据,long和double占两个slot
  • 本地方法栈(Native Method Stack):用来支持native方法执行的栈就是本地方法栈
  • 堆(Heap):用来存放应用系统创建的对象和数组,是所有线程共享的区域,GC主要管理的就是堆空间(对分代GC来说堆也是分代的),运行期动态分配内存大小,自动进行垃圾回收,但是效率相对较慢
  • 程序计数器(Program Couter Register):每个线程都有一个私有的程序计数器(创建线程时就相应创建了程序计数器),用来存储执行下一条指令地址,是一块很小的内存空间,是不会出现内存溢出的,若执行的是本地方法,程序计数器的值为undefined

栈、堆、方法区交互关系

在Java程序运行时,由虚拟机栈存放的局部变量表,局部变量可以是引用类型,该引用类型的指针指向堆中的某个对象,对象中又存储着元数据信息和实例数据信息,元数据信息又指向方法区中类的定义

堆内存

用来存放应用系统创建的对象和数组,是所有线程共享的区域

  • 在JVM规范中在堆内存需要在逻辑上是连续的,堆内存大小可以是固定的也可以是可扩展的
  • Java堆是在运行期动态分配内存大小,自行进行垃圾回收,垃圾回收主要回收的就是堆内存,对分代GC来说堆也是分代的

堆的结构

Java堆内存结构

共分成了两代,一代区域叫新生代,一代区域叫老年代

之前的持久代,用来存放Class、Method等元信息的区域,从JDK8就去掉了,取而代之的是元空间(MetaSpace),元空间并不在虚拟机中,而是直接使用本地内存

  • 新生代:存放新创建的对象,新生代又包含Eden区和存活区,这两个区域的比例可通过Survivor Ration进行设置
    • Eden区:新创建对象会先放到该区域,之后转向存活区
    • 存活区:又分为from区和to区,from区和to区大小相同,并且使用时只会单独使用from区或to区,之所以这样是因为在垃圾回收算法中的复制算法
  • 老年代:当多次垃圾回收回收不掉的、需要存下的内容就会存放到老年代,以及新创建的大对象会直接存放到老年代中

对象内存布局

对象在内存中存储的布局分为(以HotSpot虚拟机为例):对象头、实例数据和对齐填充

  • 对象头包括标志词(Mark Word)和类型指针两个部分
    • 标志词:存储对象自身的运行数据,比如:HashCode、GC分代年龄、锁状态标志等
    • 类型指针:实例对象用来指向他的类元数据的指针
  • 实例数据用来真正存放对象实例数据的地方
  • 对齐填充:该部分比一定存在,也没有什么特别含义,仅仅是占位符,因为HotSpot要求对象其实地址都是八字节的真孰不,若不是就无法对齐

对象访问定位

在JVM规范中只规定了引用类型是一个指向对象引用,但没有规定引用具体如何定位、访问堆中对象的具体位置,因此对象的访问方式取决于JVM的实现,目前主流有使用句柄或指针两种方式

  • 指针方式:Java堆中会存放访问类元数据的地址,引用就直接存储对象地址,对象头中的类型指针存储类元数据的地址(HotSpot虚拟机实现方式)
  • 句柄方式:Java堆中会划分出一块内存来做为句柄池,引用中存储句柄地址,句柄中存储对象实例地址和类元数据的地址

Java内存分配参数

在JDK9以后修改了一些命令行参数,具体修改可从Oracle官网搜索Convert GC Logging Flags to Xlog关键字搜索到对应版本的该表对应查看即可,下面命令是JDK9以后的版本

IDEA中设置运行Java程序的参数,点击运行按钮左面的下拉框,选择Edit Configurations...进行程序运行配置,右侧找到VM options的输入框,在此处输入参数即可

Trace跟踪参数

参数名 描述
-Xlog:gc 打印GC的简要信息
-Xlog:gc* 打印GC的详细信息
-Xlog:gc+heap=日志等级 每次GC后都打印堆信息
-Xlog:gc:文件名 以文件输出GC信息,不会被覆盖,会重新生成一个新的日志文件

以默认垃圾收集器(G1)产生的日志格式为例,日志格式如下,从左至右依次是:

  • GC发生时间:JVM从启动以来经过的秒数
  • 日志等级
  • 日志类型标记:主要是GC的类型
  • GC识别号:触发的是第几次GC
  • GC类型和原因
  • 容量:格式为GC前容量->GC后容量(该区域总容量)
  • GC持续时间(单位秒):有的收集器会有更详细的描述,比如:user表示程序消耗时间,sys表示系统内消耗时间,real表示操作从开始到结束时间

Java堆参数

建议-Xms-Xms参数的值配置相等,这样就避免了每次GC过后都去调整堆的大小,从而减少了系统内存分配开销

建议-Xmn参数值为总堆大小的25%到50%之间,或直接使用默认值,分配较大时可能会出现不可预估的错误

参数名 描述
-Xms 初始堆大小,默认是物理内存1/64(必须是1024的倍数并且大于1MB)
-Xmx 最大堆大小,默认是物理内存1/4(必须是1024的倍数并且大于2MB)
-Xmn 新生代大小,默认是整个堆3/8
-XX:NewRatio 老年代与新生代的比值,默认是2,其他交由JVM自动计算,若设置-Xms=-Xmx且设置-Xmn的情况下,该参数无需设置
-XX:SurvivorRatio Ened区和存活区的比值
-XX:+HeapDumpOnOutOfMemoryError 监听OutOfMemoryError错误,出现错误会生成对应的内存快照文件,默认文件路径在运行路径上(项目根目录)
-XX:+HeapDumpPath 配合-XX:+HeapDumpOnOutOfMemoryError参数,可指定生成的文件路径
-XX:OnOutOfMemoryError 监听OutOfMemoryError错误,出现错误会执行一个脚本

使用 -Xms10m -Xmx10m -Xmn3m -Xlog:gc+heap=debug参数和下面代码进行测试

public class Main {
  public static void main(String[] args) {
    System.out.println("总内存(totalMemory):" + Runtime.getRuntime().totalMemory() / 1024 / 1024 + "MB");
    System.out.println("剩余内存(freeMemory):" + Runtime.getRuntime().freeMemory() / 1024 / 1024 + "MB");
    System.out.println("最大内存(maxMemory):" + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
  }
}

由于JDK9以后默认使用的是G1收集器,新生代、老年代并没有严格的划分,看的并不是很清晰,可使用-XX:+UseConcMarkSweepGC参数(JDK15该参数已经被废弃)指定以前的CMS收集器,并用下面代码进行测试

import java.util.ArrayList;
import java.util.List;

public class Main {
  private byte[] bytes = new byte[1024 * 1024];  //该对象创建会消耗1MB内存

  public static void main(String[] args) {
    List<Main> list = new ArrayList<>();
    int num = 0;
    try {
      while (true) {
        list.add(new Main());
        num++;  //分配次数
      }
    } catch (Throwable e) {  //当内存溢出时会抛出OutOfMemoryError错误,而不是异常
      System.out.println("分配次数,num:" + num);
      e.printStackTrace();
    }
    System.out.println("总内存(totalMemory):" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024.0 + "MB");
    System.out.println("剩余内存(freeMemory):" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024.0 + "MB");
    System.out.println("最大内存(maxMemory):" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024.0 + "MB");
  }
}

Java栈参数

参数名 描述
-Xss 通常几百K,决定函数调用栈的深度

元空间参数

一般用不上,作用不大

参数名 描述
-XX:MetaspaceSize 初始Metaspace空间大小,若达到该值会触发垃圾回收机制,同时GC对该值进行调整
-XX:MaxMetaspaceSize 最大Metaspace空间,默认没有限制
-XX:MinMetaspaceFreeRatio 在GC之后,最小的Metaspace剩余空间容量的百分比
-XX:MaxMetaspaceFreeRatio 在GC之后,最大的Metaspace剩余空间容量的百分比
此作者没有提供个人介绍
最后更新于 2021-07-13