JVM内存结构图

image.png
白色区域表示线程共享的,所有的线程都会共用堆内存和方法区;
红色表示线程独享的,每个线程都会有自己独立的虚拟机栈、本地方法栈和程序计数器;

线程共享

堆是JVM内存里面最大的一块内存空间,绝大部分对象都是存储在堆内存里面,堆内存细分为:新生代、老年代、持久代(JDK8以前)/元空间(JDK8及以后)
image.png
从JDK8起,持久代就被废弃了,由元空间代替,元空间并不是堆内存的一部分,而是一块本地内存

虚拟机栈(Java方法)

线程独享

每创建一个线程就会创建一个虚拟机栈,虚拟机栈由栈帧组成,每一次的方法调用都会创建一个栈帧,然后去压栈,当方法返回的时候则对应着栈帧的出栈操作,也就是说,每调用一个方法就会往栈帧里面压入一个元素,当方法执行完成后就会弹出一个元素;栈帧里面又存放着一系列的数据,有 局部变量表、操作数栈、指向运行时常量池的引用、方法返回地址和动态链接;
image.png

方法里面的代码在执行的时候会从局部变量表或者是对象实例的字段里面复制变量,或者是常量,然后放到操作数栈里;
当计算的时候会使用一系列的指令往操作数栈里面放入数据/或者是取走数据,可以认为操作数栈是用来存放一些临时数据的地方

本地方法栈(Native方法)

线程独享

本地方法栈和虚拟机栈的功能是类似的,虚拟机栈用来管理Java方法,而本地方法栈则用来管理Native方法,本地方法都是由C语言去实现的

程序计数器

线程共享

程序计数器用来记录各个线程执行的字节码的地址,像分支/循环/跳转/异常/线程恢复等等操作,都需要依赖程序计数器;
由于Java是多线程语言,当执行的线程数量超过CPU核心的时候,线程之间就会根据时间片去争夺CPU资源;
假如,某一个线程它的任务还没执行完,但是CPU资源就被其他的线程给抢了,那之后如果又轮到这个线程来执行任务,得知道从哪里继续执行,所以为每一个线程都分配了一个程序计数器,用来记录它下一条运行的指令等等

方法区

image.png
上述图主要是以JDK8及以上版本的内存分布,事实上不同JDK发行版、甚至是不同JDK的版本实现上是有差异的,比方说Oracle JDK(HotSpot虚拟机),HotSpot虚拟机在JDK1.7之前使用永久代来实现方法区,但是其他的虚拟机没有永久代这种东西;

方法区并不是一个物理上的区域,而是一个逻辑上的划分,这是一个JVM的规范,在JDK8或更高的版本里面,方法区有一部分是放在堆里面的,还有一部分是放在元空间里面

方法区主要包括了四部分:类信息、运行时常量池、字符串常量池和静态变量;

  • 方法区(存放在元空间):主要作用,是用来存放虚拟机加载的类相关的信息;方法区和堆之间是存在交集的
  • 静态常量池(存放在堆内存):也叫class文件常量池,主要存放
    • 字面量:例如文本字符串、final修饰的常量
    • 符号引用:例如类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
  • 运行时常量池(存放在元空间):当类加载到内存之后,JVM就会将静态常量池中的内容存放到运行时的常量池中;运行时常量池里面存储的主要是编译期间生成的字面量、符号引用等
  • 字符串常量池(存放在堆内存):可以理解成运行时常量池分出来的一部分,类加载到内存的时候,字符串会存放到字符串常量池里面

元空间替代永久代的原因

  • Oracle把HotSpot虚拟机和Jrocket虚拟机都收购了,而Jrocket虚拟机没有持久代这种东西,于是为了融合HotSpot虚拟机和Jrocket虚拟机,干脆把永久代给去掉了
  • 持久代在使用的过程中还是挺容易发生问题的,例如:java.lang.OutOfMemoryError:PermGen异常;JDK8之前使用持久代来实现方法区,而方法区存储的主要是一系列的常量和类以及方法的相关信息,这块区域其实比较难以准确确定大小的,因为每个项目所加载的类和方法是不一样的,持久代设置的太小的话就容易出现这块区域的内存溢出,而设置的太大又会导致内存上的浪费,于是就将持久代给去掉了,用元空间代替;元空间是一块本地内存,理论上取决于操作系统可以分配的内存大小,这样就解决了持久代空间难于分配的问题

巩固

image.png
image.png

当执行Demo demo = new Demo("aaa")的时候,首先会创建一个局部变量demo,放在栈里面,然后栈里面的demo会指向一个引用,而真正的demo对象存放在堆里面,最后在执行printName()方法;

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

那一年,我也变成了光!!