JVM内存模型与运行时数据区域

1、java内存模型

jvm-1

  1. java定义内存模型的目的是:为了屏蔽各类硬件和操做系统的内存访问之间的差别。
  2. java内存模型规定了全部的变量都存储在主内存中,每条线程拥有本身的工做内存,工做内存保存了主内存中变量的副本。
  3. 线程对变量操做只能在工做内存中进行,不能直接读写主内存的变量。
  4. 不一样线程之间的变量访问须要经过主内存来完成。

一、java内存模型和java运行时数据区域的关系:主内存对应着java堆,工做内存对应着java栈。java

二、volatile关键字,使得变量的更新在各个工做内存中都是实时可见的。在DCL的单例模式中有运用到!面试


2、java运行时数据区域/内存区域

由于jvm的运行时数据区域一直在改善,因此不一样jdk版本之间会有不一样。算法

一、jdk1.7以前的jvm内存区域,拥有永久代

jvm-2

一、程序计数器的做用,由于.java文件被编译成.class文件,它做为当前线程所执行的字节码的行号指示器。当字节码解释器工做时,就是经过改变这个计算器的值来选取下一条要执行的字节码指令。每条线程都有一个独立的程序计数器。数组

二、本地方法栈就是执行本地native方法的栈,native方法由虚拟机实现!jvm

三、java虚拟机栈描述的是该线程执行java方法(method)时的内存模型。每个方法都对应一个栈帧,栈帧中的局部变量表存储了方法中的基本数据类型变量、对象引用变量。函数

jvm-3

如上图所示,局部变量表保存了方法中声明的8种基本类型变量和对象引用变量。每个栈帧中还有一个指向运行时常量池的引用,这是指String类型。 下面有一个经典的String对象生成的面试题!

四、java堆是JVM中内存最大的一块,被全部线程共享。几乎全部的对象实例都在这里分配,因此java堆也是JVM垃圾回收的主要区域。java堆又被分红了年轻代,老年代;年轻代进一步能够划分为Eden空间,From Survivor空间、To Survivor空间。性能

jvm-4

当咱们使用new关键字分配对象时,就是在java堆中生成对象。

下面分析一下对象生成时的状况。测试

  1. 由于Eden最大,因此新生成的对象都分配到Eden空间,当Eden空间快满时,进行一次Minor GC,而后将存活的对象复制到From Survivor空间。这时,Eden空间继续向外提供堆内存。
  2. 后面继续生成的对象仍是放到Eden空间,当Eden空间又要满的时候,这时候Eden空间和From Survivor空间同时进行一次Minor GC,而后把存活对象放到To Survivor空间。这时,Eden空间继续向外提供堆内存。
  3. 接下来的状况和2一致。Eden空间快满的时候,Eden空间和To Survivor空间进行一次Minor GC,而后存活的对象放到From Survivor空间。
  4. 接下来的状况和3一致。Eden空间快慢的时候,Eden空间和From Survivor空间进行一次Minor GC,而后存活的对象放到To Survivor空间。
  5. 就是说2个Survivor中的一个用来提供对象保存。当Eden空间和某一块Survivor空间GC后,另外一块Survivor空间放不下GC后存活的对象;或者是连续Minor GC15次左右的状况;就把这部分存活对象放入到老年代空间。
  6. 当老年代空间也放满的时候,进行Major GC,对老年代空间进行回收。(也叫作Full GC,Full GC的内存消耗很大,应该避免)

年轻代使用的是复制算法:每次Minor GC把Eden区和一块Survivor区的存活对象复制到另外一块Survivor区。老年代使用的是标记-整理算法:每次Major GC把存活对象都想内存空间的一端移动,而后直接清理掉端边界之外的内存。spa

大对象如数组、很长的字符串,直接进入老年代空间。

五、方法区用于存储JVM加载的类信息、final常量、static静态变量等数据,方法区中的数据都是整个程序中惟一的。方法区还包含了运行时常量池,主要存放编译期生成的字面量和符号引用(在类加载后放入)。String对象的字面量就会被放入到运行时常量池中。操作系统

垃圾回收在方法区主要是对常量的回收和对类型的卸载。

二、jdk1.8及以后的jvm内存区域,元空间取代了永久代

jvm-5

参考文章:java8的jvm内存区域

元空间和永久代的性质是同样的,都是对JVM方法区的实现,做用是同样的。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机JVM内存中,而是使用本地内存。

为何用元空间取代永久代呢?

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难肯定其大小,所以对于永久代的大小指定比较困难,过小容易出现永久代溢出,太大则容易致使老年代溢出。
  3. 永久代会为GC带来没必要要的复杂度,而且回收效率偏低。

直接内存

JDK1.4以后加入的NIO,引入了基于通道channel和缓冲区buffer的IO,直接使用native函数分配堆外内存,显著提升IO性能,避免了原来BIO的在java堆和naive堆中来回复制数据。

jvm-6

三、字符串String生成时的内存分配状况

参考文章:字符串常量池

四、生成对象时的内存状况

下面来分析一下咱们常见的生成对象或基本数据类型变量的内存模型。这样能够对JVM有一个更好的理解。

  1. int i =3;,一个方法对应一个栈帧,方法中的基本数据类型变量直接在栈帧中分配。若是是static、final类型的基本数据类型则存储在运行时常量池中,和String同样
  2. Object o1 = new Object();,对象引用(Object o1)存储在栈帧中,可是对象数据(new Object())存储在java堆中,对象类型数据(Class等信息)存储在方法区中。
  3. String s1 = new String("abcd");,使用new声明的对象,对象引用(String s1)存储在栈帧中,对象数据(new String(“abcd”))存储在java堆中,字符串值(“abcd”)存储在运行时常量池中。
  4. String s2 = “abc”,对象引用(String s2)存储在栈帧中,字符串值(“abc”)存储在运行时常量池中。

jvm-7

java栈、java堆、方法区这3者之间的关系大概就是上面的分析所示。


三、各类异常分析

一、java堆内存溢出错误OutOfMemoryError

若是java堆中分配的对象太多,且GC后内存空间仍是不够用。下面经过循环生成对象来消耗内存空间进行测试。

相关指令:VM Args: -Xms20m -Xmx40m,表示JVM分配的堆内存最小为20MB,最大为40MB。

public static void main(String[] args) {
   while (true) {
     List<Object> list = new ArrayList<>(10);
     list.add(new Object());
   }
 }

二、java栈堆栈溢出错误StackoverflowError

若是java栈的栈深度大于JVM容许的深度,就会抛出该错误。下面经过无限递归调用来进行堆栈进行测试。

相关指令:VM Args: -Xss128k,表示JVM分配的栈容量为128KB。

public class StackOOM {
    
    private int length = 1;
    
    public void stackLeak() {
        length++;
        stackLeak();
    }
    
    public static void main(String[] args) {
        StackOOM stackOOM = new StackOOM();
        stackOOM.stackLeak();
    }
}

jvm-8

相关文章
相关标签/搜索