JVM学习笔记(四)堆内存

6. 堆内存模型

6.1 概述和图解

一块是非堆区,一块是堆区。

堆区分为两大块,一个是old区,一个是Young区。

Young区分为两大块,一个是 Survivor 区(S0+S1),一块是 Eden 区。Eden:S0:S1=8:1:1

S0和S1一样大,也可以叫From和To。

图解

Q:一个对象的创建在那个区域?

6.2 对象创建区域

一般情况下,对象创建会被分配给 Eden 区,一些特殊比较大的对象会分配到 Old 区。

比如有对象A、B、C等需要创建在Eden区,Eden初始大小为100M,假如已经使用了100M或达到一个上限,这时候就需要清理Eden区的内存进行清理,即垃圾回收(Garbage Collect) 这样的GC我们称之为 Minor GC,Minor GC 指的是Young去的GC。

经过GC清理之后,有些对象会被清理,有些对象还会存活,存活的对象需要将其复制带Survivor区,然后在清理掉Eden区的对象。

6.3 Survivor 区详解

由图解可知,Survivor 区分为两块S0和S1,也可以叫From和To。

在同一个时间点上,S0和S1只能有一个区有数据,另一个为空。

接着上面的GC来说,比如一开始Eden区和From区有数据,To中为空。

此时进行一次GC操作,From区中的对象年龄就会+1,Eden区所存活的对象会被复制到 To 区,

Form区中还能存活的对象有两个去处。

  • 若对象年龄达到之前设置好的年龄阈值,此时对象就会被移动到 Old区

  • 如果Eden区和Form区没有达到阈值的对象会被复制到To区

此时Eden区和Form区已经被清空。

这时From和To角色互换,之前From->To,To->From。

也就是无论什么时候都要保证名为To的Survivor区域为空。

Minor GC 会一直重复执行这样的过程,直至To区被填满,然后将所有的对象复制到Old区。

6.4 Old区详解

从上面的分析可以看出,一般Old区都是年龄比较大对象或超过了某个阈值的对象。

Old区也会有GC的操作,我们称之为 Major GC或 Full GC。

6.5 对象的一辈子理解

我是一个普通的Java对象,我出生在Eden区,Eden区有很多跟我一样的小伙伴,有些爸妈会带他们离开Eden区,有些跟我一起慢慢长大,直至有一天,Eden区人实在太多了,我们就搬家到 Survivor区的 From区,自从去了 From区,我就一直居无定所,在Survivor的“From区”和“To区”来回漂泊。直至我18岁成年了,该出去闯荡闯荡。

于是我来到了 Old区,这边人很多,年龄都比较大,我在生活了20年(GC一次加一岁),然后被回收了。

6.6 常见问题

  • 如何理解 Minor/Major/Full GC

Minor GC : 新生代
Major GC :老年代
Full GC : 新生代+老年代
  • 为什么需要 Survivor 区? 只有Eden区不行吗?

为了减少被送到 Old区的对象,进而减少 Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代存活的对象,会被送到 Old区。
  • 为什么需要两个Survivor区?

最大好处就是解决了碎片化问题。假设只有一个S区,模拟下流程:
新建的对象在Eden区中,一旦Eden满了,出发 Minor GC,Eden中存活的对象就会被移动到 Survivor区。这样循环下去,下次Eden满了,问题来了,此时进行MinorGC,Eden和Survivor各存活了一些对象,如果强行将存活对象移动到Survivor 区,两部分对象所占有的空间是不连续 ,就会导致内存碎片化,浪费内存空间。
所以永远有一个Survivor space 是空的,另一个非空的S区 无碎片。
  • 新生代中 Eden:S1:S2 为什么是8:1:1?

因为新创建的对象基本上是“朝生夕死”的。所以Eden区的内存分配占比比较高。
新生代中的可用内存:复制算法来担保内存为9:1
可用内存中Eden:S1区为8:1
即新生代Eden:S1:S2 = 8:1:1

6.7 体验与验证

6.7.1 使用jvisualvm查看

下载插件地址: https://visualvm.github.io/pluginscenters.html

6.7.2 堆内存溢出

代码演示

运行时设置参数 -Xmx20M -Xms20M

@RestController 
public class HeapController { 
  List<Person> list=new ArrayList<Person>(); 
   
 @GetMapping("/heap") 
  public String heap() throws Exception{ 
    while(true){ 
        list.add(new Person()); 
        Thread.sleep(1); 
        } 
    } 
}
​

结果

Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded

6.7.3 方法区内存溢出

向方法区中添加 Class的信息

添加依赖包 asm

<dependency>
  <groupId>asm</groupId> 
  <artifactId>asm</artifactId> 
  <version>3.3.1</version> 
</dependency>

代码

public class MyMetaspace 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(); 
      Metaspace test = new Metaspace(); 
      byte[] code = cw.toByteArray(); 
      Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length); 
      classes.add(exampleClass); 
    }
    return classes; 
  } 
}
​

访问类

@RestController 
public class NonHeapController { 
  List<Class<?>> list=new ArrayList<Class<?>>(); 
  @GetMapping("/nonheap") 
  public String nonheap() throws Exception{ 
  while(true){ 
  list.addAll(MyMetaspace.createClasses()); 
  Thread.sleep(5); 
  } 
} 
}

设置Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M

运行结果

java.lang.OutOfMemoryError: Metaspace 
  at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_191]
  at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_191]

6.7.4 虚拟机栈溢出

public class StackDemo { 
  public static long count=0; 
  public static void method(long i){ 
    System.out.println(count++); 
    method(i); 
  }
  public static void main(String[] args) { 
   method(1); 
  } 
}
​

运行结果

解释和说明

Stack Space用来做方法的递归调用时压入Stack Frame(栈帧)。所以当递归调用太深的时候,就有可能耗尽Stack Space,爆出StackOverflow的错误。 ​

-Xss128k:设置每个线程的堆栈大小。JDK 5以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。根据应用的线 程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有 限制的,不能无限生成,经验值在3000~5000左右。 ​

线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更 大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。


When I let go of what I am , I become what I might be.
走出舒适圈,遇见更好的自己。

发布了91 篇原创文章 · 获赞 63 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/qq_38423105/article/details/104731041