JVM

JVM学习笔记一

Java内存区域与内存溢出异常

Posted by Emo on July 19, 2022

JVM

EMO’s Blog

运行时数据区域

  • 程序计数器(Program Counter Register) [线程私有]
  • Java虚拟机栈(Java Virtual Machine Stack) [线程私有] Java方法涉及的基本类型数据以及对象的引用存在这里
  • 本地方法栈(Native Method Stack) [线程私有] 本地方法涉及的基本类型数据以及对象的引用存在这里
  • Java堆(Java Heap) [线程共享] 所有的对象和数组都在这里分配内存
  • 方法区(Method Area) [线程共享] 用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等数据

程序计数器的理解

在java中线程是切换运行的,所以当一个线程发生切换的时候,我们需要利用程序计数器去记录其运行到的位置,以便切换回来的时候继续运行。

栈的理解

本地方法栈和Java虚拟机栈本质是差不多的。 从代码的角度来理解

public class Test {
    public static void main(String[] args) {
        int a = 5;
        func(a);
        Syatem.out.println(a)
    }

    public void func(int a) {
        a++;
        Syatem.out.println(a);
    }
}
/*运行结果
6
5
*/ 

根据上诉代码,我们可以提出一个问题,为什么两次打印的a的值不一样呢?

其实是因为每个方法都会开辟自己的栈空间来用于存储自己方法中涉及到的变量和对象引用,所以mainfunc中都存有自己的a=5变量信息。所以当func中执行a++后,只有func持有的a的值变化了,main当中的值没有受到影响。其实更简单来说,就是func调用的时候产生了a的备份,而func中的操作都是在对这个备份进行修改,值的变化发生在栈中。

堆的理解

从代码的角度来理解

public class Test {
    public class people {
        public int age;
        public People(int age) {
            this.age = age;
        }
    }
    
    public static void main(String[] args) {
        People people = new People(5);
        func(people);
        Syatem.out.println(people.age)
    }

    public void func(People people) {
        people.age++;
        Syatem.out.println(people.age);
    }
}
/*运行结果
6
6
*/ 

根据上诉代码,我们可以提出一个问题,为什么两次打印的a的值又是一样的呢?

因为这个栈当中只能存对象的引用,所以当调用func时,栈中产生的是people实例的引用的备份。而后我们在func中使用这个引用去修改people.age的时候,值的改变发生在堆中,而这个变量是大家共享的。所以后续main当中访问到的people.age就是被func改动后的值。

方法区的理解

方法区本质和Java堆是一样的,用于存储一些常量,全局的变量,类的信息等。

JVM(HotSpot)对象探秘

对象的创建

对象的创建需要到堆中中去分配内存,内存的分配有两种方式

  1. 指针碰撞: 当内存规整时,只需要用一个指针指出已分配和未分配的界限点,而后分配内存的时候只需要移动指针即可
  2. 空闲列表:用一个列表来记录哪些块可用,而后通过修改列表上的记录来分配内存。
graph LR
    A{内存是否规整}
    B((指针碰撞))
    C((空闲列表))
    D((GC收集器))

    A --> |是| B
    A --> |否| C
    D --> |决定| A

内存是否规整很多时候是由GC是否拥有空间压缩(Compact)能力来决定的。

因为堆是线程共享的,在并发场景下,内存分配不是线程安全的,所以有两种解决方法:

  1. 对内存分配进行同步
  2. 为每个线程预分配内存分配缓冲区,这样保证每个线程在分配内存的时候是不会互相干扰的,而后只有在扩充缓冲区的时候才需要进行同步。

堆中为对象划分好内存后,还需要为对象设置好一些基础信息,最后交由构造函数去进行特定的初始化,其实在Java中所有的元数据都是已经被赋予了初始默认值了。

对象的内存布局

  • 对象头
    • Mark Word
    • 类型指针
  • 实例数据
  • 对齐填充

对象的访问定位

对象在堆中创建后,一般会在栈中创建对象的引用,通过这个引用来定位对象。引用的类型由JVM决定,一般有两种:

  • 句柄,堆中还需构建句柄池,句柄池中记录对象的具体地址信息。
  • 直接指针,需要考虑对象的内存布局,如何放置访问类型数据的相关信息。这种方式开销小。

OutOfMemoryError异常

堆溢出

如果Java堆发生了OutOfMemoryError,需要对堆转储快照进行分析,理清到底发生的是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。如果有该被清理的对象没有被清理掉,那么就是内存泄漏,需要检查GC到对象的路径,排查原因。如果是内存溢出,那么就需要考虑对象设计还有没有优化的空间或者是否要对堆扩容。

栈溢出

两种异常:

  • StackOverflowError,线程请求的栈深度大于虚拟机所允许的最大深度
  • OutOfMemoryError,如果栈空间允许动态扩展,那么这意味着在动态扩展的时候栈申请内存空间失败。如果不能动态扩展,那么抛出的依旧是StackOverflowError。

{ % if page.mermaid % } { % endif % }