zoukankan      html  css  js  c++  java
  • JVM内存区域

    本文聊聊Java的内存分区,其实在谈java内存区域划分时事实上指的就是JVM的内存区域划分。此外,这些分区是基于JVM规范的,不同的虚拟机都可以有各自略微不同的实现。


    先来看看java代码执行的基本流程:

    java源代码被编译器编译为.class的字节码文件,然后由虚拟机接管,通过类加载器加载完后交给执行引擎执行。执行过程中,用到的一些数据放在一个叫运行时数据区的地方,这个地方就是JVM的内存,即本文讨论的地方。


    运行时数据区

    包含几个部分:

    1. PC程序计数器

      占用很小的内存,可以看作是当前线程所执行的字节码的行号指示器。

      每个线程都有独立的PC空间,因为Java虚拟机的多线程是通过轮流切换cpu来实现的,所以为了各个线程互不影响,每个线程都要有一个pc空间。这种每个线程都有的空间我们叫做“线程私有”内存。

      如果线程正在执行一个java方法,pc记录的就是正在正在执行的虚拟机字节码指令的地址,如果执行的是Native方法,这个pc则为空。

      此内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError的区域。

    2. 虚拟机栈

      这个区域也是线程私有的。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,栈帧放在虚拟机栈中。栈帧中存放局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到被执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。在活动线程中,只有栈顶的栈帧才是可操作的,该帧称为当前栈帧,正在执行的方法称为当前方法,执行引擎的所有指令都只能针对当前栈帧进行操作。

      栈帧:

      1)局部变量表:

      存放方法参数,局部变量。存放的数据类型有8种基本数据类型,对象引用类型(用于指示对象位置,根据不同虚拟机,reference可能时指针也可能是句柄),以及returnAddress类型(指向一条字节码指令的地址)。局部变量表的最小容量以变量槽为单位(即一个字长,32位虚拟机变量槽即为32位)。并且整个表相当于一个数组,通过索引访问。
      如果是非静态方法,则在表头存放的是方法所属对象的实例引用,一个引用变量占4字节。字节码中的STORE指令即是将操作栈中计算完成的局部变量写回局部变量表中。

      2)操作栈:

      也称为操作数栈,一开始是空的。java的执行引擎的操作主要就在操作栈执行。语句的执行归根到底就是计算的过程,而计算就在操作栈中进行。
      举个例子:执行iadd指令,java字节码和流程图如下:
      1.iload_0 //从局部变量表[0]读取数据压入操作栈
      2.iload_1 //从局部变量表[1]读取数据压入操作栈
      3.iadd //执行add操作,结果也压入操作栈
      4.istore_2 //操作栈栈顶元素存到局部变量表[2]位置

      3)动态连接:

      一个指向运行时常量池(JVM 运行时数据区域)中该栈帧所属性方法的引用,用于支持方法调用过程中的动态链接。

      4)方法出口:

      又叫方法返回地址。方法退出后需要返回到方法进入时的地址,正常退出的情况下,调用者的程序计数器的值就是返回地址,而异常退出时,返回地址是通过异常表来确定,栈帧中一般不会保存这部分信息。

      虚拟机栈的异常:

      1)StackOverflowError:线程请求的栈深度超过虚拟机允许的栈深度时抛出。一般是递归过深。

      2)OutOfMemoryError:如果虚拟机栈允许动态扩展,当扩展时无法申请到足够的内存时抛出。一般是对象创建太快快于回收速度,或者程序设计不合理创建对象过多。

    3. 本地方法栈

      作用和虚拟机栈类似,不过是为虚拟机使用到的本地Native方法服务。有的虚拟机将本地方法栈和虚拟机栈合并了,如Sun HotSpot。

    4. 堆区

      一般来说,这块区域最大。堆区是所有线程共享的区域,在虚拟机启动时创建。该区唯一目的即是存放对象实例,几乎所有的对象实例都在这里分配内存。由于垃圾回收主要管理的就是堆区,堆区也被称为GC堆。

      堆区又分为新生代和老年代,这样分类有利于垃圾回收。堆区在物理上不必连续。

    5. 方法区

      方法区用于存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

      jdk8之前方法区使用永久代实现,8.0开始使用元数据区(Metaspace)实现方法区,去掉了永久代概念。jdk8将常量池和静态变量放到堆中,我们应该这么理解:虽然物理上移动了,但逻辑上还是属于方法区。即可以说常量池在堆中也可以说在方法区。即根据不同的虚拟机,不同的版本,各种数据存放的位置是有变化调整的,不是完完全全按照jvm规范而来。

      元数据区并不像永久代在虚拟机中,而是使用本地内存。

      永久代迁移到元空间的优点:a)常量池迁移到堆中,避免溢出;b)永久代为GC带来了很多复杂性,使得回收效率低。


    • 几个概念:

      1. 运行时常量池

        常量池分两种形态:静态常量池和运行时常量池。

        静态常量池:即class文件中的常量池,包含字面量(如文本字符串、声明为final的常量等)和符号引用量(类和接口的全限定名、字段名称和描述符、方法名称和描述符)。

        运行时常量池:即类加载到虚拟机中后,class文件的常量池加载到内存,并保存在方法区。两种常量池只是通过它们的状态命名,内容都是一样的。

        运行时常量池相对于 静态常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,用得比较多的便是 String 类的 intern() 方法。

        常量池避免了一些常用对象的频繁创建,并提供了共享。

      2. 直接内存Direct Memory

        直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。

        在 JDK 1.4 中新加入了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

        显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError 异常。


    补充:

    1. 动态连接过程:

      动态连接是一个将符号引用解析为直接引用的过程。当java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用

      那么虚拟机就必须解析这个符号引用。在解析时,虚拟机执行两个基本任务

      1.查找被引用的类,(如果必要的话就装载它)

      2.将符号引用替换为直接引用,这样当它以后再次遇到相同的引用时,它就可以立即使用这个直接引用,而不必花时间再次解析这个符号引用了。


    2. 方法区,元数据区,永久代

      方法区是JVM规范中的概念,元数据区和永久代是不同虚拟机对方法区的实现。


    模拟内存溢出异常

    1. 堆溢出

      堆存放对象实例,所以只要一直新建实例,并且保证gc不会回收就会溢出。

      设置虚拟机参数:Xms20m -Xmx20m (-XX:+HeapDumpOnOutOfMemoryError)

      参数解释:Xms设置初始堆大小,Xmx设置最大堆大小,内存不够不会再扩容,括号内表示可选参数,HeapDumpOnOutOfMemoryError可以让出现内存溢出时Dump出当前内存堆转储存快照以便事后分析。

      测试代码:

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

      运行结果:

      Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
      	at java.base/java.util.Arrays.copyOf(Arrays.java:3511)
      	......
      	at java.base/java.util.ArrayList.add(ArrayList.java:467)
      	at JVM.OOMObject.main(OOMObject.java:15)
      

      错误提示给出了是heap space的oom,以及一些简略信息。

    2. 虚拟机栈溢出

      栈溢出就是递归过深,内存用尽。设置参数:-Xss128k(表示每个线程的堆栈大小128k)

      测试代码:

      public class JVMStackOF {
          private int stackLength = 1;
          public void stackLeak(){
              stackLength++;
              stackLeak();
          }
      
          public static void main(String[] args) throws Throwable{
              JVMStackOF sof = new JVMStackOF();
              try {
                  sof.stackLeak();
              }catch (Throwable e){
                  System.out.println("stack length:"+sof.stackLength);
                  throw e;
              }
          }
      }
      

      运行结果:

      stack length:23604  //栈深度23604时溢出(若增加本地变量,使得本地变量表变大,则溢出时栈深度也减小)
      Exception in thread "main" java.lang.StackOverflowError
      	at JVM.JVMStackOF.stackLeak(JVMStackOF.java:11)
      	at JVM.JVMStackOF.stackLeak(JVMStackOF.java:11)
      	......
      

      有的博客说虚拟机栈有两种溢出:StackOverflow栈溢出和OutOfMemory内存溢出。但实际上栈溢出和内存溢出其实本质上都是内存不够,因为栈并没有规定多大就溢出,而是内存不够了才溢出。上述代码就只能抛出栈溢出错误,而不抛出内存溢出,但本质上就是内存不够了。

      操作系统为每个进程分配的内存都是有限的,这些内存主要被堆+方法区+栈瓜分。有一种出现OOM的情形:多线程时,每个线程都要分配私有的栈容量,栈容量分配完后再有线程就会OOM。这种情况,如果仍然需要更多线程,可以减少堆容量和每个线程的栈容量来增加可建立线程数量。

    3. 方法区溢出

      针对jdk8,这里讲的其实是元空间溢出。元空间存储类名,修饰符,描述符等信息,所以不断创建类,就会溢出。

      参数设置:-XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m

      测试代码:

      /**
       * 使用CGLib产生大量类,模拟内存溢出
       */
      public class JavaMethodAreaCglibOom {
      
          public static void main(String[] args) {
              while (true) {
                  Enhancer enhancer = new Enhancer();
                  enhancer.setSuperclass(User.class);
                  enhancer.setUseCache(false);
                  enhancer.setCallback(new MethodInterceptor() {
                      @Override
                      public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                          return methodProxy.invokeSuper(object, args);
                      }
                  });
                  enhancer.create();
              }
          }
      
          static class User {
          }
      }
      

      运行结果:

      Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
      	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:348)
      	......
      
    4. 常量池溢出

      jdk8之前常量池在永久代,而8后移到了堆,网上搜了也没有常量池大小设置的参数,所以一般应该不存在常量池溢出,或是直接堆溢出。

    5. 本机直接内存溢出

      直接内存通过参数:-XX:MaxDirectMemorySize指定,若不指定,默认和堆最大值一样。


    参考:《深入理解Java虚拟机》

  • 相关阅读:
    from __future__ import with_statement
    第六章 Flask数据库(二)
    python面向对象基础(四)内置方法 __xx__之new与init
    MySQL用户自定义变量
    iOS不得姐项目--appearance的妙用,再一次设置导航栏返回按钮,导航栏左右按钮的封装(巧用分类)
    iOS边练边学--NSURLSessionDataTask实现文件真正的断点续传
    iOS边练边学--AFNetWorking框架GET、Post、Download、Upload,数据解析模式以及监控联网状态
    iOS边练边学--NSURLSession、NSURLSessionTask的介绍与使用以及url中包含了中文的处理方法
    iOS开发小技巧--边接受数据边写入文件的两种方法
    iOS边练边学--文件压缩和解压缩的第三方框架SSZipArchive的简单使用
  • 原文地址:https://www.cnblogs.com/cpcpp/p/14016937.html
Copyright © 2011-2022 走看看