JVM的基础知识点Java的内存模型

阅读文本大概需要3分钟。

      Java虚拟机是Java工程师必学的进阶功课,这段时间开始死磕JVM。今天梳理一下JVM的基础知识点Java的内存模型!

程序计数器

是什么:程序计数器是很小的一块内存空间,它是当前线程所执行的字节码的行号指示器。

有什么用:解释器通过这个计数器来选取下一条需要执行的字节码指令。
存储什么内容:如果线程执行的是Java方法,存储的是正在执行的虚拟机字节码指令的地址;如果是native方法,计数器值为空(undefined)。

为什么是线程私有的:多线程是线程轮流切换并分配处理器执行时间片的方式来实现的,在任何确定的时刻,一个处理器(对于多核处理器来说就是一个内核)都只会执行一条线程,所以,为了线程在切换后能恢复到正确的执行位置,每个线程应该独立拥有一个程序计数器。

会出现什么异常情况:唯一一个无内存溢出异常的区域。

Java虚拟机栈

是什么:虚拟机栈是Java方法的内存模型,每一个Java方法从调用到执行完成就对应着一个栈帧在虚拟机栈中的入栈和出栈。

存储什么内容:每个方法的执行就会创建一个栈帧,这个栈帧会存储这个Java方法的局部变量表,操作数栈,动态链接,方法出口等信息。

为什么是线程私有的:每个线程所执行的方法可能是不一样的。

会出现什么异常情况:如果线程请求的栈深度>虚拟机允许的深度,抛出栈溢出异常;如果扩展时无法申请到足够的内存,抛出内存溢出异常。

本地方法栈

是什么:本地方法栈的作用和虚拟机栈非常像是,只不过本地方法栈是native方法的内存模型,每一个native方法从调用到执行完成就对应着一个栈帧在本地方法栈中的入栈和出栈。

存储什么内容:同虚拟机栈。

为什么是线程私有的:同虚拟机栈。

会出现什么异常情况:同虚拟机栈。

Java堆

是什么:Java堆是Java虚拟机管理的内存中最大的一块,Java堆是在虚拟机启动的时候创建的。

存储什么内容:存放对象实例,几乎所有的对象实例都在这个内存区域分配内存。

为什么是线程共享的:所有的线程都可以访问不同的对象。其实从内存分配的角度来看,线程共享的Java堆可能其实是多个线程私有的分配缓冲区,不同的线程将各自的对象实例放在看似共享的Java堆的各自的缓冲区上,这样划分可以更好的回收内存,也可以更好点分配内存。

会出现什么异常情况:Java堆可以处于物理上不连续的内存空间上,但逻辑上一定是连续的,在堆中没有内存可以完成对象实例的分配,且无法再扩展时,会抛出内存溢出异常。

方法区

是什么:和堆一样,是各个线程共享的内存区域。很多人把方法区称为永久代,但是本质上这两个不等价,Java虚拟机将GC分代收集扩展至方法区,使用永久代来实现方法区,这样GC收集器就能像管理Java堆一样管理方法区而不需要再写一套GC收集来管理方法区。当然在方法区里也可以设置不进行GC收集。

存储什么内容:已被虚拟机加载的类信息,类常量,类的静态变量,即时编译器编译后的代码等。运行时常量池也是方法区的一部分。

为什么是线程共享的:各个线程都可以访问虚拟机加载的类。

会出现什么异常情况:内存溢出异常。

直接内存

是什么:直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机定义的内存区域,但也经常被使用。JDK1.4加入了NIO类,一种基于通道与缓冲区的新I/O方式,NIO可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为直接内存的引用来操作直接内存,这样可以避免在Java堆和native堆来回复制数据,从而提高了性能。

会出现什么异常情况:受机器总内存的影响,会出现内存溢出异常。


Java虚拟机中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常;

  • 如果在虚拟机中无法申请到足够多的内存空间,将抛出OutOfMemoryError异常。

我们都知道Java虚拟机各个内存区域(除了程序计数器)都有发生内存溢出的可能,但到底什么样的操作或程序才会导致内存溢出或栈溢出的异常呢?我们分不同的内存区域来解释这个问题。


0x01、对于Java堆内存区域

Java堆中只会产生OutOfMemoryError异常。

先搞清楚Java堆内存放的是什么,还不清楚的可以回顾下这篇文章《死磕JVM-Java内存模型》,从这篇文章里我们知道Java堆内存存放的是对象实例,所以原理上只要我们不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象,也就是说当Eden区满的时候,GC被触发时,让GC误以为内存中的对象还存活着,那么在对象数量达到最大堆容量限制的时候就会产生内存溢出的异常。如下代码就会产生内存溢出的异常:

public class 堆溢出{
     static class OOMError{}
     public static void main(String[] args){
          List<OOMError> list =new ArrayList<OOMError>();
          while(true){
               list.add(newOOMError());
          }
     }
}

运行结果:

Exceptionin thread "mainjava.lang.OutOfMemoryError:Java heap space
     at java.util.Arrays.copyOf(Arrays.java:3210)
     at java.util.Arrays.copyOf(Arrays.java:3181)
     at java.util.ArrayList.grow(ArrayList.java:261)
     at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
     at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
     at java.util.ArrayList.add(ArrayList.java:458)
     at com.intelligentler.jvm.堆溢出.main(堆溢出.java:13)

“Java heap space”提示着产生OutOfMemoryError异常的Java虚拟机的内存区域,也就是Java堆内存。

如何解决发生在Java堆内存的OutOfMemoryError异常呢?

首先我们要分清楚产生OutOfMemoryError异常的原因是内存泄露还是内存溢出,如果内存中的对象确实都必须存活着而不像上面那样不断地创建对象实例却不使用该对象,则是内存溢出,而像上面代码中的情况则是内存泄露。

如果是内存泄露,我们可以通过一些内存查看工具来查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的路径与GC Roots相关联并导致GC无法自动回收这些泄露对象,掌握了这些信息,我们就能比较准确地定位出泄露代码的位置。

如果不是内存泄露,也就是说内存中的对象确实都还必须存活,那么应该检查虚拟机的堆参数,看看是否还可以将机器物理内存调大,同时在代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况。


0x02、对于虚拟机栈和本地方法栈

在这一部分内存区域,可能产生OutOfMemoryError异常和StackOverflowError异常。

如果定义大量的本地变量,增大此方法帧中本地变量表的长度或者设置-Xss参数减少栈内存容量,这两种操作都会抛出StackOverflowError异常,如下面的代码:

public class 栈溢出{
     privateint stackLength =1;
     publicvoid addStackLength(){
          stackLength++;
          addStackLength();
     }

     public static void main(String[] args)throws Throwable{
          栈溢出 oom =new 栈溢出();
          try{
               oom.addStackLength();
          }catch(Throwable e){
               System.out.println("stack length:"+ oom.stackLength);
               throw e;
          }
     }
}

运行结果:

stack length:18388Exceptionin thread "mainjava.lang.StackOverflowError
     at com.intelligentler.jvm.栈溢出.addStackLength(栈溢出.java:9)
     at com.intelligentler.jvm.栈溢出.addStackLength(栈溢出.java:9)
     at com.intelligentler.jvm.栈溢出.addStackLength(栈溢出.java:9)

所以,如果在单线程的情况下,无论是栈帧太大还是虚拟机栈容量太小,当内存无法再分配的时候,虚拟机抛出的是StackOverflowError异常。

如果在多线程下,不断地建立线程可能会产生OutOfMemoryError异常。


0x03、对于方法区

方法区中只会产生OutOfMemoryError异常。

由于运行时常量池是方法区的一部分,我们可以通过String.intern()方法来构建一个运行时常量池的OutOfMemoryError异常。

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含了一个等于该String对象的字符串,则返回这个String对象,否则,将此String对象包含的字符串添加到常量池中,并返回这个字符串的String对象的引用。如下面代码:

public class 方法区溢出{

     public static void main(String[] args){
          List<String> list =newArrayList<String>();
          int i =0;
          while(true){
               list.add(String.valueOf(i++).intern());
          }
     }
}

运行结果:

Exceptionin thread "mainjava.lang.OutOfMemoryError:PermGen space
    at java.lang.String.intern(NativeMethod)

PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,也就是说运行时常量池属于方法区(也就是虚拟机永久代)中的一部分。

另外,方法区是存放Class的相关信息的,运行时如果有大量的类来填满方法区,就会产生OutOfMemoryError异常。




往期精彩



01 Sentinel如何进行流量监控

02 Nacos源码编译

03 基于Apache Curator框架的ZooKeeper使用详解

04 spring boot项目整合xxl-job

05 互联网支付系统整体架构详解

关注我

每天进步一点点

喜欢!在看☟