内存分配
当一个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来说堆也是分代的
堆的结构
共分成了两代,一代区域叫新生代,一代区域叫老年代
之前的持久代,用来存放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剩余空间容量的百分比 |
Comments NOTHING