Java 栈内存和堆内存和方法区超全讲解

文章目录

  • 内部存储器(Internal Storage)
    • 1.线程私有存储区(Thread-Private Storage)
      • 1.1 栈内存(Stack)
        • 1.1.1 虚拟机栈(VM Stack)
        • 1.1.2 本地方法栈(Native Method Stack)
    • 2 线程公有存储区(Thread-Public Storage)
      • 2.1 堆内存(Heap)
        • 2.1.1 新生代(Young)
          • 2.1.1.1 伊甸园(Eden)
          • 2.1.1.2 从生存者(From Survivor/S0)
          • 2.1.1.3 至生存者(To Survivor/S1)
        • 2.1.2 老年代(Old)
          • 2.1.2.1 tentired
      • 2.2 方法区(Non-Heap)
        • 2.2.1 常量池(Constant Pool)
        • 2.2.2 静态域(Static Field)
        • 2.2.3 其他

本片博文本来是打算写一篇关于栈内存和堆内存的博文,也相当于自己的一个学习记录,结果一不小心学多了…就一直写到了内存的讲解了,本来还写了寄存器和一些硬件存储的,结果篇幅太大了就先删掉了,这里就先说下内存好了o(  ̄︶ ̄)o, 文中肯定有许多有问题不足的地方,需要各位网友大大们评论补充!

内部存储器(Internal Storage)

1.线程私有存储区(Thread-Private Storage)

1.1 栈内存(Stack)

                 /|————————————————|\
            栈 /  |____局域变量区____|  \
            帧 \  |_____操作数区_____|    \
           /     \|_____帧数据区_____|      \
         /                                   \
       /          |—————————————————|          \  <_________________________>\
 栈  /            |____局域变量区_____|            \<_______PC Register_______> \包含寄存器的
 内存\            |_____操作数区______|            /<________JVM Stack________> /单线程内存区
       \          |_____帧数据区_____|          /  <___Native Method Stack___>/
         \                                   /
           \      |—————————————————|      /
             \    |____局域变量区____|    /
               \  |_____操作数区_____|  /
                 \|_____帧数据区_____|/

  • 存储类型

    1.基本数据类型 2.对象的引用 3.方法的调用

  • 共享机制

    栈内数据共享,但线程私有

  • 作用时期

    在代码运行时,每当调用一个方法,都在在栈内存中创建一个栈内存块(栈帧),方法结束后栈内存变成未使用的状态

  • 描述

    存储基本数据类型以及对象的引用,当在一个代码块中定义一些变量的时候,栈内存中为这些变量分配空间,按照 FILO 的原则,当代码块结束时,JVM 会自动释放分配的空间。栈内存不是 GC 回收的,堆内存是的。它是由编译器自动分配和释放。可能会抛出 OutOfMemoryErrorStackOverFlowError 异常。它的存取速度很快仅次于寄存器,与堆内存相比较栈内存大小很小。可用 -Xss JVM 的选项来定义栈内存大小

  • 特点

    存取速度很快,比堆要快,速度仅次于寄存器,存在栈中的数据有明确的生存周期和数据大小。存储其中数据可共享,线程私有

  • 一些问题

    1. 栈内存存储方法的调用怎么理解?

      答:如f1()方法,其中调用了f2()方法,那么栈内存中最下层是f1(),再上面是f2()

    2. 栈内存存储对象的引用怎么理解?

      答:栈中保存的是对象的引用,该引用指向的是堆内存中对象中的数据。对象的引用是指向一个已创建的对象或者null,其只能是唯一指向,对象的引用某一时刻只能有唯一指向,对象本身可以同时被多个引用变量引用

    3. 栈内存如何存储数组?

      答:和对象存储一致

    4. 什么叫做栈中数据是共享的?

      答:假如有有如下代码

      int a = 1;
      int b = 1;
      

      java 编译器先处理a = 1,栈中会去找有没有值是 1 的空间,若有将 a 的引用直接指向这个空间,没有就在栈中开辟一个空间存储 1,然后执行b = 1,这时候直接让 b 的引用指向 a 指向的空间,这样 a 和 b 的引用指向同一个空间,但对象实际上是不同的,因为假使两个对象的引用指向的是同一个对象,如果一个对象的引用变量改变了,那么这个对象的内部状态实际上也改变了

    5. 为什么栈内存线程私有?

      答:栈内存不适合多线程共享(注意这和栈中数据共享是两个概念),因为若栈内存采用多线程共享,会非常凌乱

    6. StackOverFlowErrorOutOfMemoryError区别?

      答:StackOverFlowError栈溢出是单个线程请求栈内存大于虚拟机允许内存时候报出的,而OutOfMemoryError内存溢出是指整个虚拟机内存耗尽,无法再申请新的内存时候报出

    7. 堆内存由 GC 管控,那栈内存回收呢?为什么没用 GC 管控?

      答:栈内存也是 JVM 自动管控的,但非 GC,栈内存分为很多栈帧,栈内存实际就是很多栈帧的组合,按照方法的执行顺序 FILO,每调用一个方法分配一个栈空间(栈帧),方法结束该方法的栈内存(栈帧)被释放,由于栈内存数据结构的特性无需用 GC 管控

1.1.1 虚拟机栈(VM Stack)

  • 存储类型

    1.基本数据类型 2.对象的引用 3.Java 方法的调用

  • 共享机制

    栈内数据共享,但线程私有

  • 作用时期

    在代码运行时,每当调用一个方法,都在在栈内存中创建一个栈内存块(栈帧),方法结束后栈内存变成未使用的状态

  • 描述

    分为三部分:1.基本类型变量区域 2.执行环境上下文 3.存放操作指令区域,其实际就是存放线程调用方法时存储局部变量表,栈大小由 -Xss 调控,若方法调用层次太多容易造成栈溢出,比如说,无限递归中,递归每次存一个新的基本数据类型(注意栈内存中数据是共享的),然后就栈溢出了,线程太多也会占满栈(栈内存非线程共享)

  • 特点

    见上方栈内存讲解

  • 一些问题

    1. -Xss是什么?

      答:其是指定启动每个线程栈内存的大小,从 JDK 5 以后默认是 1 M,如使用命令java -Xss1024k

1.1.2 本地方法栈(Native Method Stack)

  • 存储类型

    1.native方法的调用

  • 共享机制

    栈内数据共享,但线程私有

  • 作用时期

    在代码运行时,每当调用一个方法,都在在栈内存中创建一个栈内存块(栈帧),方法结束后栈内存变成未使用的状态

  • 描述

    和虚拟机栈(VM Stack)很相似,不同是 Native Method Stack 服务的对象是 JVM 执行的 native 方法(Java 调用的非 Java 代码接口)

  • 特点

    见上方栈内存讲解

  • 一些问题

    1. 什么是 Java 的 native 方法?

      答:最直接说法就是 Java 调用的非 Java 代码接口。这不仅仅是 Java 有这种 native 方法的概念,其他编程语言都有这个机制。举出下面的代码例子:

      public class NativeExample{
          // 指定 dll 路径
          static{
              System.load("D:" + File.separator + "function1.dll");
          }
          // native function1
          native private void function1(int x);
          // native function2
          native synchronized static public double function2();
          // native function3
          native void protected function3(long[] arr) throws Exception;
          // main
          public static void main(String[] args){
              // 调用 native function1
              function1();
          }
      }
      

      很像abstact的用法对不对?native暗示了这些方法实际上是有实体的,但非 Java 语言实现,所以显然不能与abstract连用,必经一个有实体一个无实体。这些实体比如说想去调用一个 C 的类库时。若子类继承了含有native方法的类时,native方法是允许被重写的,若其被final标识同样子类也不能重写它。native非常有用,它有效的扩充了 JVM,在 sun 的 Java 并发机制中许多与操作系统的接触都用到了native方法,这使 Java 程序超越了 Java 运行时的界限,Java 程序可以做到任何应用层次的任务。上述代码将 64 位的动态链接库dll放在指定路径,运行main之后,C 代码输出就出来了,这就是简单的 Java 使用native方法

2 线程公有存储区(Thread-Public Storage)

2.1 堆内存(Heap)

|-------------|-----|-----|--------------------------------------|
|             |     |     |									     |
|             | S0  | S1  |									     |
|  Eden 8/10  |1/10 |1/10 |                 Old 			     |
|             |     |     |									     |
|             |     |     |									     |
|-------------|-----|-----|--------------------------------------|
 <---- Young 1/3 Heap ---> <----------- Old 2/3 Heap ----------->
 <--------------------------- Heap ----------------------------->
  • 存储类型

    1.存储对象(不是对象的引用)

  • 共享机制

    堆内数据非共享,但线程共享

  • 作用时期

    运行代码,当new一个对象时候,现在栈内存中分配空间存放对象的引用指向堆内存,再在堆内存中分配空间存放对象数据

  • 描述

    堆内存的数据结构和数据结构中的堆完全是两回事,分配方式类似于链表,实际上是一种优先队列的数据结构,第一个元素有最高优先权。操作系统有一个记录空闲内存地址的链表,系统收到程序申请空间时,会遍历该链表,寻找第一个空间大于申请空间的堆结点,然后将该结点从空闲结点链表中删掉,由于是大于,多余的空间放入空闲结点链表中,然后将该结点空间分给程序使用,对于大多数操作系统会在这个内存空间首地址记录本次分配的大小,这样代码的 delete 方法可以顺利释放本内存空间。它是由程序员自己申请自己释放(GC 自己可回收),它是垃圾收集器的主要回收区域,因此其又称为 GC 堆。我们可以使用 -Xms JVM 的选项来定义堆内存的启动大小,用 -Xmx JVM 的选项来定义堆内存的最大大小。新生代 + 老年代 = 堆内存大小

  • 特点

    堆内存获得空间比较灵活,也比较大,速度比较慢,容易产生内存碎片,使用方便,生存周期无需告诉编译器,垃圾收集器自动回收堆内存,线程共有

  • 一些问题

    1. 堆内存什么时候被回收?

      一个对象死亡时,对其进行回收,那如何判别对象是死亡的呢?JVM 中有一条长长的引用链,根节点是 GC root,其中主要包含如下几种引用结点:1.虚拟机栈中对象的引用 2.本地方法栈中native方法的调用 3.方法区的静态域中静态变量 4.方法区的常量池中的常量。从这些结点向下搜索,当一个对象没有关联一个引用链节点时候就可证明此对象死亡,于是 GC 知道它可以回收了。从这里也可以看出 GC 实际上也可以对方法区进行回收

    2. 内存溢出是怎么造成的?

      答:程序需要分配的内存超过了系统可以分给你的时候就会内存溢出,比如说死循环中集合类不断add(),由于程序还在循环,JVM GC 又没法自动回收

    3. 为什么堆内存线程共有?

      答:这点可以与栈内存作比较,堆内存实现线程共享是为了快速高效(线程中数据交换方式:1.共享内存方式(SM)优势是快,如共享堆内存 2.消息传递方式(MP)优势是安全)

    4. GC 是指?

      答:GC(Gabage Collection)垃圾收集,内存在存储时是程序中很容易出问题的地方,小小的内存异常可能就会导致系统崩溃,JVM 自带的垃圾回收功能可以自动监测堆内存存储的数据是否超过了作用周期从而自动回收空间。Java 并没提供释放出已分配堆内存的显示操作方法

    5. -Xms是指?

      答:-XmsJVM 启动时申请的最小内存,运行系统为了避免运行时频繁调整堆内存大小,通常将-Xms-Xmx设成一样,默认空余堆内存小于 40% 时,JVM 会自动增大堆内存到-Xmx-Xms默认值 3670 k

    6. -Xmx是指?

      答:-XmxJVM 启动时申请的最大内存,运行系统为了避免运行时频繁调整堆内存大小,通常将-Xms-Xmx设成一样,默认空余堆内存大于 70% 时,JVM 会自动减小堆内存到-Xms-Xmx默认值 64 m

2.1.1 新生代(Young)

  • 存储类型

    同堆内存,但存储的是历经小 GC 小于 15 次的对象

  • 共享机制

    同堆内存

  • 作用时期

    分配堆内存给对象,小 GC 的作用期间

  • 描述

    新生代属于堆内存一部分,之所以这样划分是因为 JVM 更好管理堆内存中对象,新生代占堆内存 1/3,新生代 = 伊甸园 + 从生存着 + 至生存者,新生代中每次只会用伊甸园和其中一个生存者空间为对象存储进行服务,也就是说总会有一处生存者空间时空闲的,新生代实际可用空间是 90%,而非100%。GC 分为小 GC(Minor GC)大 GC(Major GC)小 GC 采用复制算法发生在新生代中,新生代是垃圾回收的频繁区域。当对象需要在新生代中产生,但是新生代空间不够用,虚拟机就会使用一次小 GC 来清除无用对象,若有用对象仍存活,就采用复制算法将这些还存活的对象复制到另一个生存者区域并将其年龄设为 1,然后清掉前面的区域,有用的对象每次历经一次小 GC 未被清除,年龄就会加 1,超过默认值 15 岁时,对象转为了堆内存中的老年代,但是对于一些较大的对象会直接进入老年代

  • 特点

    小的对象历经小 GC 还未被清掉,超过 15 次转放入老年代,大的对象则可能会直接进入老年代

  • 一些问题

    1. 为什么总有一个生存者区域是空闲的?或者说为什么要搞两个生存者区域?

      答:因为伊甸园经历小 GC 存活的对象复制一个生存者区时,若只有一个生存者区或者说这个生存者区域不是空的(含有上次历经小 GC 后被复制进来的对象),那么这次复制过来必将和上次的对象地址不连续,必将产生碎片,浪费生存者空间,又由于生存者空间很小,这样就容易引发下一次小 GC,造成小 GC 过于频繁而且内存空间未被充分利用。当我们有两个生存者区域时候,保证其中一个必是空闲的,这样每次经历小 GC 复制对象的时候,地址就是连续的,不会产生碎片,是的伊甸园空间和其中一个生存者空间比较充足,不会频繁使用小 GC

2.1.1.1 伊甸园(Eden)
  • 存储类型

    同新生代

  • 共享机制

    同新生代

  • 作用时期

    堆内存产生对象的空间在伊甸园中

  • 描述

    伊甸园属于新生代的一部分,伊甸园占新生代的 8/10

  • 特点

    产生对象空间,只有一块

  • 一些问题

    1. Eden 的含义

      答:如伊甸园这个名字一样,代表从无到有的过程,即对象空间的第一次产生

2.1.1.2 从生存者(From Survivor/S0)
  • 存储类型

    新生代

  • 共享机制

    新生代

  • 作用时期

    堆内存中给对象分配好空间之后每经历过一次小 GC 之后存储的地方,但是次数小于 15 次

  • 描述

    从生存者属于新生代的一部分,伊甸园占新生代的 1/10

  • 特点

    复制对象后到达的空间,有两块(S0S1

  • 一些问题

    1. Survivor 的含义

      答:如生存者这个名字一样,代表历经小 GC 幸存下来的“生存者”

2.1.1.3 至生存者(To Survivor/S1)
  • 存储类型

    同从生存者

  • 共享机制

    同从生存者

  • 作用时期

    同从生存者

  • 描述

    至生存者属于新生代的一部分,伊甸园占新生代的 1/10,同从生存者

  • 特点

    同从生存者

  • 一些问题

    同从生存者

2.1.2 老年代(Old)

  • 存储类型

    同堆内存,但存储的是历经小 GC 大于 15 次的有用对象,不包括较大的对象

  • 共享机制

    同堆内存

  • 作用时期

    小 GC 超过 15 次,大 GC 执行期间

  • 描述

    老年代属于堆内存的一部分,新生代:老年代为 1:2 的关系,老年代占堆内存的 2/3,老年代中才用的标记-清除算法大 GC 不那么频繁,执行时间也比小 GC 慢一个数量级,大 GC 执行会产生较多不连续空间碎片,使得以后连续的较大存储对象分配空间容易找不到空间,一旦找不到就触发一次大 GC 操作

  • 特点

    有用的对象历经 15 次小 GC 后转入老年代

  • 一些问题

    1. 为什么老年代区域这么大,是新生代区域的两倍?

      答:这与较大对象存入老年代中以及小 GC 经过 15 次之后到达老年和性能有关,这个比例实际上也是可以通过命令来调整 JVM 的,老年代比新生代大有些像缓冲的概念

2.1.2.1 tentired

这里资料蛮少的,还需要广大网友留言来帮助补充!Thanks♪(・ω・)ノ

2.2 方法区(Non-Heap)

  • 存储类型

    1.存储 Class 二进制文件(含有虚拟机加载的类信息,常量,静态变量等数据)

  • 共享机制

    线程共享

  • 作用时期

    一个类会先加载到方法区的内存中,然后静态域被加载于main之前执行

  • 描述

  • 特点

  • 一些问题

    1. 栈内存无 GC 垃圾回收,堆内存有,那在方法区存在 GC 垃圾回收吗?

      答:JVM 规范中的确说过不要求其在方法区实现垃圾收集,而且方法区做垃圾收集效率非常之低,但实际上方法区也是有垃圾回收的

2.2.1 常量池(Constant Pool)

  • 存储类型

    1.基本数据类型以 final 声明的常量值和final String

  • 共享机制

    常量池内数据是共享的,一个 Class 字节码文件对象对应一个常量池,线程共享

  • 作用时期

    常量存储

  • 描述

    它是方法区的一小块内存,在 Class 文件中,常量池实际上是最复杂也最值得关注的,常量池技术是为了方便快捷创建对象而产生的,Java 基本类型的类型的包装类大部分都实现了常量池技术

  • 特点

    一次写入,处处运行

  • 一些问题

2.2.2 静态域(Static Field)

  • 存储类型

    1.静态数据成员变量

  • 共享机制

    线程共享

  • 作用时期

    随着类的加载而加载并初始化,优先于对象而存在,在类被加载到方法区之后

  • 描述

    位于方法区的一块内存,它的内存是在程序编译的时候就已经分配好的。

  • 特点

    static代码块或static变量

  • 一些问题

    1. 全局变量存在哪里?

      答:Java 全局变量存储在栈内,存堆内存中

2.2.3 其他

你可能感兴趣的:(#,Java)