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

 阅读约 10 分钟

一、java内存模型

jvm-1

  1. java定义内存模型的目的是:为了屏蔽各种硬件和操作系统的内存访问之间的差异。
  2. java内存模型规定了所有的变量都存储在主内存中,每条线程拥有自己的工作内存,工作内存保存了主内存中变量的副本。
  3. 线程对变量操作只能在工作内存中进行,不能直接读写主内存的变量。
  4. 不同线程之间的变量访问需要通过主内存来完成。

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

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


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

因为jvm的运行时数据区域一直在改善,所以不同jdk版本之间会有不同。

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

jvm-2

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

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

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

jvm-3

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

4、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把存活对象都想内存空间的一端移动,然后直接清理掉端边界以外的内存。

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

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

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

2、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

3、字符串String生成时的内存分配情况

参考文章:字符串常量池

4、生成对象时的内存情况

下面来分析一下我们常见的生成对象或基本数据类型变量的内存模型。这样可以对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者之间的关系大概就是上面的分析所示。


3、各种异常分析

1、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());
   }
 }

2、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

阅读 742发布于 2018-10-15
推荐阅读
mmmming
用户专栏

学过前端,主攻Java。

4 人关注
24 篇文章
专栏主页
目录