内存结构

组成结构

  • 线程私有:程序计数器、本地方法栈、虚拟机栈

  • 线程共享:方法区、堆

程序计数器

线程私有,存储下一条字节码指令的行号

  1. 也叫 PC 寄存器,存储下一条要执行的指令行号(如果下一条指令是 native 方法,记录的行号是 undefined)
    1. class 最终被编译成字节码指令,CPU 能执行的内容
    2. 每个线程是要拥有 CPU 时间片才会执行的,时间片结束就不能执行了,如果程序还没执行完,就要先挂起,并记录行号,等下一次拥有时间片的时候继续执行(上下文切换的体现)
    3. 再比如条件判断、循环等操作,下一条指令是哪个条件,或者是否跳出循环等,都需要记录在线程内部
  2. 线程私有,生命周期跟随线程的创建和结束,线程一定有自己的程序计数器,线程销毁,程序计数器也被销毁
  3. jvm 中 唯一一个不会出现 oom 的区域

虚拟机栈

线程私有,存放栈帧,一个栈帧就是一个方法,栈帧里面主要存储当前方法需要操作的局部变量和操作数栈

  1. 在线程增加一个方法调用叫做栈帧入栈,调用完一个方法,这个方法就出栈
  2. 既然栈帧对应方法调用,那么栈帧里面存储的就是方法调用的相关信息
    1. 局部变量表:存储方法形参和定义在方法体内部的局部变量
    2. 操作数栈:对局部变量进行操作的行为列表,比如读取局部变量表的数据到操作数栈,对局部变量表的数据进行运算比较等
    3. 动态链接
    4. 方法返回地址
  3. 运行速度很快,但是空间不大,jdk5 之前一个线程默认 256k,jdk5 之后 默认1 M(可自定义每个线程默认大小)

局部变量表

  1. 非静态方法还有 this,静态方法没有 this,所以静态方法不用用 this 的原因就是栈帧里面没有 this 这个变量
  2. 局部变量也不会赋初值,成员变量才会赋值(意思就是方法里面定义的变量一定要赋值)
  3. 局部变量表的对象变量是 GC Root 对象

操作数栈

  1. 比如局部变量表有一个 a,操作数栈可以是读取a,把a+1,然后再更新回局部变量表的值
  2. load、puah 指令是把局部变量压入操作数栈(读取变量,操作数栈深度+1)、store 把新的变量写入局部变量表(修改变量,操作数栈深度-1)

常见面试问题

问题1:栈会内存不足吗?

  1. JVM 默认一个线程大小是固定的,可以指定大小,当超过指定的大小就会发生栈溢出,StackOverflow
  2. JVM 也能允许配置线程大小是动态扩展的,当物理内存不够发生 OOM

问题2:真内存越大越好吗?

不是,因为增加栈大小,就会减少栈的数量

问题3:栈会垃圾回收吗?

不会,为什么不会?去问高斯林,JVM 更多的是规范,因为作为应用开发,这些都属于底层理论知识

问题4:为什么要区分堆和栈,栈速度很快,何不把堆的数据放到栈中?

  1. 栈空间不大,要是存放真实对象很容易内存不足
  2. 有些对象是线程共享的
  3. 栈管运行,堆管存储

问题4:方法中定义的局部变量是否是线程安全的?

不是,局部变量如果是一个对象,局部变量表的变量是一个引用地址,别的线程可能也引用这个变量

本地方法栈

和虚拟机栈类上,不过栈帧是 native 方法

方法区

  1. 存放类信息,比如类型、方法、成员变量等(常量池)
    User user = new User(); User:方法区;user:栈;new User():堆
  2. 逻辑上属于堆,实际上不属于堆
  3. 垃圾回收效率最差(新生代最快,老年代次之,方法区最差)
  4. 1.7 之前叫永久代;1.8开始叫元空间(什么地方存放类信息,这块区域就叫方法区),永久代使用的堆内存,元空间使用的是本地内存
  5. 不管永久代还是元空间,静态变量和运行时常量池都是放在堆中的(运行时常量池,就是字符串字面量;还有个常量池,这是描述类的信息,别混淆了)

一个jvm实例只有一个堆,所有线程共享;jvm 管理的最大的一块内存,垃圾回收的主要区域

常见面试题

问题1:所有栈帧的引用对象都在堆上吗?

不是,TLAB

问题2:方法调用结束,方法引用的对象立马就回收吗?线程销毁后,线程里引用的对象立马被回收吗

不是,仅在 GC 时且对象被判定为垃圾时才回收

问题3:java 堆的结构是什么样子的?

  1. 新生代:伊甸园、幸存区(幸存区又分为两个区,s0 和 s1,或者 from 和 to )
  2. 老年代
  3. 方法区(逻辑上属于堆,实际上不属于堆)

问题4:java 为什么要分成新生代,老年代,持久代?新生代中为什么又要分为 eden 和 survivor?

为了垃圾回收性能和效率,如果不分区,所有对象在一次,每次回收都要扫描很大的内存空间

  1. 老年代都是存活比较长的对象,并且区域也很大所以回收效率不高
  2. 大多数对象都是在新生代创建和回收,所以这里 GC 频率很高,且回收效果也很明显
  3. 幸存区分为两个区的原因是,幸存区使用标记复制的垃圾算法最合适
posted @ 2023-04-14 10:26  CyrusHuang  阅读(115)  评论(0编辑  收藏  举报