JVM学习——(二)内存模型

一、运行时数据区各区域的联系

1.1 栈帧

上一篇说过虚拟机栈一个线程执行的区域,是线程私有的。

可以这样理解,每个线程对应一个虚拟机栈,这个线程中的每个方法对应一个栈帧。

 

那么栈帧中是什么内容呢?

每个栈帧中包含局部变量表、操作数栈执行运行时常量池的引用、方法返回地址和附加信息。

局部变量表:方法中定义的局部变量以及方法的参数存放在这张表中。

局部变量表中的变量不可直接使用,如果需要使用,必须通过相关指令将其价值至操作数栈中,作为操作数使用。

操作数栈:以压栈和出栈的方式存储操作数。

方法返回地址:当一个方法开始执行后,只有两种方式可以退出,一是遇到方法返回的字节码指令;二是遇见异常,并且异常没有在方法体内被处理。

 

1.2 栈指向堆

如果在栈帧中有一个变量,类型为引用类型。比如Object  obj=new Object(),这种情况就是典型的栈中元素指向堆中的对象。

1.3 方法区指向堆

方法区中会存放静态变量、常量等数据。如下面的例子,就是典型的方法区中元素指向堆中的对象。

private static Object obj=new Object();

1.4 堆指向方法区

方法区中会包含类的信息,堆中会有对象,那怎么知道对象是那个类创建的呢?

对象的类的信息存在方法区中,这种情况就是堆指向方法区。

 

1.5 Java对象内存布局

一个Java对象在内存中包括3个部分:对象头、实例数据和对齐填充。

二、内存模型

2.1 图解

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

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

Young区分为两大块,一块是Survivor区,一块是Eden区。

Survivor区又划分为s0和s1两块。s0和s1一样大,也可以叫From区和To区。

Eden:s0:s1=8:1:1。

 

2.2 对象创建所在区域

根据之前对Heap的描述已经知道了对象和数组的创建会在堆中分配内存空间,堆中这么多区域,那么一个对象的创建到底在哪个区域呢?

一般新创建的对象都会被分配到Eden区,一些特殊的大的对象会直接分配到Old区。

举个例子,比如对象A、B、C创建在Eden区,但是Eden区的内存空间是有限的,比如有100M。假如已经使用了100M或者达到一个设定的临界值,这时候就需要对Eden区的内存空间进行清理,即垃圾收集(Garbage Collect),对于这样的GC称之为Minor GC,Minor GC指的就是Young区的GC。

经过GC之后,有些对象就会被清理掉,有些对象可能还存活,对于存活的对象需要将其复制到Survior区,然后再清空Eden区中这些对象。

 

2.3 Survivor区详解

由图解可以看出,Survivor区划分为两块,S0和S1,也可以称为From和To。

接着Minor GC继续说,在同一时间点上,S0和S1只能有一个区域有数据,另外一个是空的。
比如一开始只有Eden区和From中有对象,To中是空的。
此时进行一次GC操作,From区中对象的年龄就会+1,Eden区中所有存活的对象都会被复制到To区,
From区中还能存活的对象会有两个去处。
若对象年龄达到设定的年龄阈值,此时对象会被移动到Old区。
若Eden区和From区中没有达到阈值的对象,则会被复制到To区。
此时Eden区和From区已经被清空了(被GC掉的对象已经没了,没有被GC掉还存活的对象都有了去处)。
这时From和To交换角色,之前的From变成了To,之前的To则变成From。
也就是说,无论如何都要保证下一次名为To的Survivor区域是空的。
Minor GC一直重复这样的过程,直到To区被填满,然后会将所有对象复制到老年代中。

概括的说,就是对Eden区和From区进行清理,年龄达限的对象直接进入老年代Old区,未达限的对象都进入To区。

From区被清空后和To区转换角色,空了的From区将作为下一次的To区,供下一次GC后存放存活的对象使用。

 

2.4 Old区详解

从之前的分析可以看出,一般Old区都是年龄比较大的对象,或者超过了设定阈值的对象。

在Old区也会有GC的操作,Old区的GC我们成为Major GC。

 

2.5 对象的一辈子

图解对象的GC过程:

 

2.6 问题归纳

如何理解Minor/Major/Full GC?

Minor GC:新生代的GC

Major GC:老年代的GC

Full GC:新、老年代的GC

 

为什么需要Survivor?只有Eden不行吗?
如果没有Survivor,Eden区每进行一次Minor GC,并且没有年龄限制的话,存活的对象都会被送入老年代。
这样老年代很快就会被填满,从而触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。
老年代内存空间远大于新生代,进行一次FullGC消耗的时间比Minor GC长的多。
执行时间长有什么坏处?频繁的FullGC消耗的时间很长,会影响应用程序的执行和响应速度。
 
那么如果增加或者减少老年代的空间可以解决吗?
假如增加老年带空间,更多存活对象才能填满老年代。虽然降低了FullGC的频率,但是随着老年代空间变大,一旦发生FullGC,执行所需要的时间更长。
假如减少老年代的时间,虽然FullGC所需时间减少,但是老年代很快被存活对象填满,FullGC频率增加,也会出问题。
 
为什么需要两个Survivor区?
最大的好处就是解决了碎片化。
假如只有一个Survivor区,
刚刚新建的对象在Eden中,一旦Eden满了,触发一次MinorGC,Eden中的存活对象就会被移动到Survivor区。
这样继续循环下去,下一次Eden满了的时候,问题来了,
此时进行MinorGC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,这两部分对象所战友的内存是不联系的,导致了内存碎片化。
永远有一个SurvivorSpace是空的,另一个非空的SurvivorSpace无碎片。
 
新生代中Eden:S1:S2为什么是8:1:1?
新生代中的可用内存:复制算法用来担保的内存为9:1
可用内存中Eden:S1为8:1
即新生代中Eden:S1:S2=8:8:1
 

四、模拟内存溢出

 

4.1 堆内存溢出

设置堆内存大小参数比如-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

4.2 虚拟机栈溢出

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左右。
线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更
大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章