一、概述内存模型区域
1.内存区域认识
a.内存模型区域分为两大块,一块是非堆区(
Metaspace
),一块是堆区(Heap
)
b.堆区分为两大块,一块是Old区
(老年代),一块是Young区
(年轻代)
c.Young区也区分为两大块,一块是Survivor区
(s0+s1),一块是Eden区
,Eden:s0:s1 = 8 :1:1,s0和s1一样大也可以叫做From和To
2.创建对象所在的区域
Java创建的对象存放于堆内存,一般情况下,新创建的对象都会分配到
Young区
中的Eden区
,一些特殊的、大的对象会直接分配到Old区
例如有对象A、B、C,已创建,存放于Eden区,但是内存空间都有大小限制,Eden区有80M,但是已经全部使用完,当再次新建对象时,就会对Eden存放的对象进行清理(即:垃圾回收,请看垃圾回收篇)。清理之后大部分对象不再存在,但是仍然会有部分对象存活,这时就会把存活对象分配到Survivor区(即:s0或s1)。
3.Survivor区详解
有上图可知,Survivor又分为s0和s1,又可以叫做From和To,同一时间节点s0和s1两个区域中只有一个区域会存在数据。
从上一点得知Eden区空间不足时会进行垃圾回收(Eden区的垃圾回收叫
Minor GC
),每Minor GC
一次会把Eden区的存活对象放进From区
,下一次Minor GC
又会把Eden区
和From区
的存活对象放入To区
,所以保证了s0或则s1其中一个区域一定有一个为空,并且每次Minor GC
后存活的对象的分代年龄+1,当存活对象的分代年龄到达阀值(官方设置15),则会进入Old区
,s0或则s1满了之后其中的所有对象也会进入Old区
.。
4.Old区
从上文可知
Old区
都是分代年龄较大的对象或则到达Survivor区
阀值的对象,并且当Old区
空间不足时也会进行垃圾回收,称作:Major GC
。
5.非堆区
非堆区在JDK1.8只有被称为元空间(即:Metaspace),可以理解为方法区的实现。用于存放类的各种信息,另外非堆区也包含了运行时数据常量池。
6.对象分配流程图理解
7.jvisualVm查看内存模型
执行命令打开jvisualVm工具
jvisualvm
安装插件:
https://visualvm.github.io/pluginscenters.html —>选择对应版本链接—>Tools—>Visual GC
验证查看图形化界面和描述一致
二、内存溢出
1.堆内存溢出
List<User> userList= new ArrayList<User>();
@GetMapping("/heap")
public String heap() throws InterruptedException {
while (true){
userList.add(new User());
Thread.sleep(1);
}
}
设置启动参数:-Xmx20M -Xms20M
稍等片刻即可看到oom,也可以用jvisualVm观察堆内存情况
2.方法区内存溢出
pom.xml添加依赖
<dependency>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
<version>3.3.1</version>
</dependency>
public class MetaspaceUtil extends ClassLoader {
public static List<Class<?>> createClasses() {
List<Class<?>> classes = new ArrayList<Class<?>>();
for (int i = 0; i < 10000000; ++i) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
"java/lang/Object", null);
MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"()V", null, null);
mw.visitVarInsn(Opcodes.ALOAD, 0);
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"<init>", "()V");
mw.visitInsn(Opcodes.RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();
MetaspaceUtil test = new MetaspaceUtil();
byte[] code = cw.toByteArray();
Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
classes.add(exampleClass);
}
return classes;
}
}
List<Class<?>> list = new ArrayList<Class<?>>();
@GetMapping("/metaSpace")
public String metaSpace() throws Exception {
while (true) {
list.addAll(MetaspaceUtil.createClasses());
Thread.sleep(5);
}
}
设置启动参数:-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
3.栈内存溢出
@GetMapping("/stack")
public String stack(){
stackMethod(1);
return "stack";
}
public void stackMethod(long number){
System.out.println(number);
stackMethod(++number);
}
执行结果
执行结果详解
Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。
-Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线 程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有 限制的,不能无限生成,经验值在3000~5000左右。
线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更 大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。