一块是非堆区,一块是堆区。
堆区分为两大块,一个是old区,一个是Young区。
Young区分为两大块,一个是 Survivor 区(S0+S1),一块是 Eden 区。Eden:S0:S1=8:1:1
S0和S1一样大,也可以叫From和To。
图解
Q:一个对象的创建在那个区域?
一般情况下,对象创建会被分配给 Eden 区,一些特殊比较大的对象会分配到 Old 区。
比如有对象A、B、C等需要创建在Eden区,Eden初始大小为100M,假如已经使用了100M或达到一个上限,这时候就需要清理Eden区的内存进行清理,即垃圾回收(Garbage Collect) 这样的GC我们称之为 Minor GC,Minor GC 指的是Young去的GC。
经过GC清理之后,有些对象会被清理,有些对象还会存活,存活的对象需要将其复制带Survivor区,然后在清理掉Eden区的对象。
由图解可知,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区。
从上面的分析可以看出,一般Old区都是年龄比较大对象或超过了某个阈值的对象。
Old区也会有GC的操作,我们称之为 Major GC或 Full GC。
我是一个普通的Java对象,我出生在Eden区,Eden区有很多跟我一样的小伙伴,有些爸妈会带他们离开Eden区,有些跟我一起慢慢长大,直至有一天,Eden区人实在太多了,我们就搬家到 Survivor区的 From区,自从去了 From区,我就一直居无定所,在Survivor的“From区”和“To区”来回漂泊。直至我18岁成年了,该出去闯荡闯荡。
于是我来到了 Old区,这边人很多,年龄都比较大,我在生活了20年(GC一次加一岁),然后被回收了。
如何理解 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
下载插件地址: https://visualvm.github.io/pluginscenters.html
代码演示
运行时设置参数 -Xmx20M -Xms20M
@RestController
public class HeapController {
List list=new ArrayList();
@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
向方法区中添加 Class的信息
添加依赖包 asm
asm
asm
3.3.1
代码
public class MyMetaspace extends ClassLoader {
public static List> createClasses() {
List> classes = new ArrayList>();
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, "",
"()V", null, null);
mw.visitVarInsn(Opcodes.ALOAD, 0);
mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
"", "()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> list=new ArrayList>();
@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]
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.
走出舒适圈,遇见更好的自己。