Halo
发布于 2022-05-12 / 135 阅读 / 0 评论 / 0 点赞

java内存结构

jvm 简介

Java内存结构是指 JVM运行时将数据分区域存储 ,简单的说就是不同的数据放在不同的地方.
java把内存分成:程序计数器, 本地方法栈, 虚拟机栈,堆, 方法区(1.8以后为元数据区)和编译产物。

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,由于JVM可以并发执行线程,因此会存在线程之间的切换,而这个时候就程序计数器会记录下当前程序执行到的位置,以便在其他线程执行完毕后,恢复现场继续执行。

  • JVM会为每个线程分配一个程序计数器,与线程的生命周期相同。
  • 如果线程正在执行的是应该Java方法,这个计数器记录的是正在执行虚拟机字节码指令的地址。
  • 如果正在执行的是Native方法,计数器的值则为空(undefined)
  • 程序计数器是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

本地方法栈

本地方法栈是调用本地native方法,可以认为是通过 JNI (Java Native Interface) 直接调用本地 C/C++ 库,不受JVM控制。
本地方法栈也会抛出 StackOverflowError 和 OutOfMemoryError 异常.

虚拟机栈

Java虚拟机栈是调用Java方法;
虚拟机栈 描述的是 Java 方法执行的内存模型.
每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  • 虚拟机栈是每个线程独有的,随着线程的创建而存在,线程结束而死亡。
  • 在虚拟机栈内存不够的时候会OutOfMemoryError,在线程运行中需要更大的虚拟机栈时会出现StackOverFlowError。
  • 虚拟机栈包含很多栈帧,每个方法执行的同时会创建一个栈帧,栈帧又存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
  • 在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。

当前栈帧

  • 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型(int、short、byte、char、double、float、long、boolean)、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

  • 操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。

  • 动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

  • 方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

栈数据共享

假设我们同时定义:
int a = 5;
int b = 5;

编译器先处理int a = 5;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有5这个值,如果没找到,就将5存放进来,然后将a指向5.

接着处理int b = 5;在创建完b的引用变量后,因为在栈中已经有5这个值,便将b直接指向5.这样,就出现了a与b同时均指向5的情况。

比较类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==

String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2); //true

//可以看出str1和str2是指向同一个对象的。
String str1 = new String ("abc");
String str2 = new String ("abc");
System.out.println(str1 == str2); // false
//用new的方式是生成不同的对象。每一次生成一个。

  • 堆内存用于存放由 new 创建的对象和数组
  • 每一个实体都有一个内存地址值
  • 实体中的变量都有默认初始化值
  • 实体不再被使用,会在不确定的时间内被垃圾回收器回收
  • 堆的目的就是存放对象,几乎所有的对象实例都在此分配。当然,随着优化技术的更新,某些数据也会被放在栈上等。
  • 堆内存最大.
  • 堆是被线程共享
  • 堆的GC操作采用分代收集算法。因为堆占用内存空间最大,堆也是Java垃圾回收的主要区域(重点对象),因此也称作“GC堆”(Garbage Collected Heap)。
  • 堆区分了新生代和老年代(永久代1.8以后被移动到元数据了)。新生代又分为:Eden空间、From Survivor(S0)空间、To Survivor(S1)空间。

数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。
这个也是java比较占内存的主要原因,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!

方法区(元数据)

  • 存储的是已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.
  • 方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是回收确实是有必要的。

内存回收和TLAB

内存回收主要发生在新生代, 即Eden,S0,S1(S代表survival)

Eden 的分区

为了有效管理内存, Oracle就提出一种TLAB机制, 全称Thread Local Allocation Buffers

  • 每个线程分配一个 Eden 区, 初始的时候一个Eden 区只有一个TLAB .
  • 每个线程的内存申请只能在自己的 Eden 区获取
  • 每个Eden 区都有一个free_list 指针指向下一个可能内存的起始点.
  • 当一个TLAB 被用完的时候, 需要申请多一个TLAB, 这个时候,free_list既要指向原来已经占满的内存,还要指向新的申请的内存的起始点. 随着线程内存使用的增多, free_list 会指向多个TLAB
  • TLAB 摆脱了锁的机制,因为在单个TLAB 移动free_list是单线程的, 只有在新增TLAB到线程Eden 的时候才需要枷锁.

设置堆大小

  • 默认初始内存是物理内存的1/64, 可由-Xms指定
  • 默认最大内存是物理内存的1/4, 可由-Xmx指定
  • 当空余堆内存小于当前已分配内存的40%时,JVM就会增大到最大限制
  • 当空余堆内存大于当前已分配内存70%时,JVM会减少到 -Xms的最小限制
  • 新生代的大小可以由 -Xmn 指定。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn

垃圾回收算法

年轻代是采用复制算法进行垃圾回收, 老年代使用标记整理或者标记清除

复制算法

  1. 把内存分为2块等同大小的内存空间(A和B),使用A进行内存的使用
  2. 当A部分的内存不足以分配对象而引起内存回收时,就会把存活的对象从A内存块放到B内存块中,后把A内存块中的对象全部清除,在B内存块中使用
  3. 当B内存不足以分配内存时,就会把B中存活的对象放到A内存块中,然后把B中对象全部清除,如此循环。

优点:

  • 适合存活对象少,需要回收的对象多的场景. 因为两块内存完全相同大小一样,仅仅只需调整根对象的引用即可
  • 使用这种方式可以避免出现内存空间碎片

缺点:

  • 浪费了一半的内存,空间的使用率低
  • 存活对象多会非常耗时

标记清除算法

  1. 首先标记出所有需要回收的对象,
  2. 在标记完成后,统一回收掉所有被标记的对象

也可以反过来,标记存活的对象,统一回收未标记的对象。标记的过程就是对象是否属于垃圾的判定过程。

优点:

  • 适合存活对象多,需要回收的对象少的场景
  • 简单直接,速度块

缺点:

  • 容易出现内存空间碎片
  • 存活对象少会非常耗时, 需要大量标记和清理, free_list需要做大量调整

标记整理算法

  1. 首先标记出所有需要回收的对象,
  2. 在标记完成后,统一回收掉所有被标记的对象
  3. 让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存

优点:

  • 避免内存碎片
  • 避免浪费了一半的内存

缺点:

  • 在整理的过程中会产生一个对象拷贝的代价.
  • 三种垃圾回收算法中性能最低的一种,因为标记整理法在移动对象的时候不仅需要移动对象,还要额外的维护对象的引用的地址

评论