一:整体架构
1、内存结构理论
而我们所说的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 异常
实际中产生的原因:
- 一次从加载过多数据到内存
- 代码不合理,集合引用太多对象没有及时清空,或者死循环不断创建对象
- 堆内存分配不合理
栈内存:
大小设置:-Xss128k栈内存是线程私有,在创建线程时候确定。 所以栈内存不足有两种表现形式:
- 线程创建成功(栈深度申请成功),执行过程中栈深度不足:java.lang.StackOverflowError 。 这种情况通常是递归调用造成
- 线程创建失败(申请栈深度时,内存不足): java.lang.OutOfMemoryError ,内存溢出。 比如多线程申请时候
对于实际项目,一般情况下关注以上两个就可以了。