JVM内存分配小总结

JVM中各个变量存放的位置

需要储备的知识点

Java 中的变量根据不同的标准可以分为两类,以其引用的数据类型的不同来划分可分为 “原始数据类型变量和引用数据类型变量”,以其作用范围的不同来区分可分为 “局部变量,实例变量和静态变量”。

实例方法和普通方法

程序| 进程 | 线程

  • JVM 是一份本地化的程序,本质上是可执行的文件,是静态的概念。程序运行起来成为进程,是动态的概念。
  • JAVA进程( java 程序):一个 JVM 实例其实就是 JVM 跑起来的进程。
  • 各个 JVM 实例之间是相互隔离的。eg:登录了多个QQ
  • 进程:是并发执行的程序在执行过程中分配和管理资源的基本单位。
  • 线程:线程有时又被称为轻权进程或轻量级进程,也是 CPU 调度的一个基本单位。线程之间共用一个进程的内存空存空间 (即 jvm 堆内存),各个线程也有自己独立专有的内存空间 (即 jvm 栈帧空间)。看下图就明白了

首先确定一下jkd1.8之后的JVM内存模型

JVM内存模型

方法变量内存分配图

img
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Fruit {
static int x = 10;
static BigWaterMelon bigWaterMelon_1 = new BigWaterMelon(x);
int y = 20;
BigWaterMelon bigWaterMelon_2 = new BigWaterMelon(y);

public static void main(String[] args) {
final Fruit fruit = new Fruit();

int z = 30;
BigWaterMelon bigWaterMelon_3 = new BigWaterMelon(z);

new Thread() {
@Override
public void run() {
int k = 100;
setWeight(k);
}

void setWeight(int waterMelonWeight) {
fruit.bigWaterMelon_2.weight = waterMelonWeight;
}
}.start();
}
}

class BigWaterMelon {
public BigWaterMelon(int weight) {
this.weight = weight;
}

public int weight;
}

结合上图可得:

  • 每个 Java 方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。

本地方法栈

  • 本地方法栈为虚拟机使用到的 native 方法服务

JAVA Stack

​ 栈中有栈帧,栈帧内存:

  • 8 中基本类型变量 (byte(8)、short(16)、int(32)、long(64)、float(12)、double(64)、char(16)、boolean;)
  • 对象的引用变量(Fruit fruit ) 、
  • 实例方法(方法出口)、
  • 局部变量表;
  • 操作数栈(程序计数器)、
Java.lang.StackOverflowError 栈内存溢出

一个JVM只存在一个堆内存,堆内存的大小是可以调节的。

  • 储存:运行时常量池(字符串拼接得到的P141)、实例变量eg:new Fruit、(也叫成员变量)、静态变量
  • 两个区域:新生代(8:1:1)、老年代()

PCR

  • ​ Java 虚拟机可以支持多个线程同时执行,每个线程都有自己的程序计数器。在任何时刻,每个线程都只会执行一个方法的代码,这个方法称为该线程的当前方法(current method)。

    如果线程正在执行的是 Java 方法(不是 native 的),则程序计数器记录的是正在执行的 Java 虚拟机字节码指令的地址。如果正在执行的是本地(native)方法,那么计数器的值是空的(undefined)。

元空间(以前的永久代/方法区)

  • Caused by: java.lang.OutOfMemoryError: Metaspace

  • 储存信息

    • 类加载器读取了类文件后,需要把Class metadata(类元数据)、方法模板(构造方法)、常变量放到元空间中;

Java 中有哪几种常量池?

class 文件常量池、运行时常量池、字符串常量池。

class 文件常量池

class 文件常量池(class constant pool)属于 class 文件的其中一项,class 类文件包含:类的版本、常量池、访问标志、字段表集合、方法表等信息。

常量池用于存放编译期间生成的各种字面量(Literal)和符号引用(Symbolic References)。

运行时常量池

class 文件常量池是在类被编译成 class 文件时生成的。而当类被加载到内存中后,JVM 就会将 class 文件常量池中的内容存放到运行时常量池中。

Java 虚拟机规范中对运行时常量池的定义如下:

A run-time constant pool is a per-class or per-interface run-time representation of the constant_pool table in a class file.

运行时常量池是 class 文件中每一个类或接口的常量池表(constant_pool table)的运行时表示形式。

因此,根据规范定义,可以说运行时常量池是 class 文件常量池的运行时表示,每个类在运行时都有自己的一个独立的运行时常量池。

字符串常量池

简单来说,HotSpot VM 里的字符串常量池(StringTable)是个哈希表,全局只有一份,被所有的类共享。

StringTable 具体存储的是 String 对象的引用,而不是 String 对象实例自身。String 对象实例在 JDK 6 及之前是在永久代里,从 JDK 7 开始放在堆里。

根据 Java 虚拟机规范的定义,堆是存储 Java 对象的地方,其他地方是不会有 Java 对象实体的,如果有的话,根据规范定义,这些地方也要算堆的一部分。

jdk1.8之后永久代的变化

永久代在 Java 8 被移除。根据官方提案的描述,移除的主要动机是:要将 JRockit 和 Hotspot 进行融合,而 JRockit 并没有永久代。

而据我们所了解的,还有另外一个重要原因是永久代本身也存在较多的问题,经常出现 OOM,还出过不少 bug。

根据官方提案的描述,永久代主要存储了三种数据:

1)Class metadata(类元数据),也就是方法区中包含的数据,除了编译生成的字节码被放在 native memory(本地内存)。

2)interned Strings,也就是字符串常量池中驻留引用的字符串对象,字符串常量池只驻留引用,而实际对象是在永久代中。

3)class static variables,类静态变量。

移除永久代后,interned Strings 和 class static variables 被移动了堆中,Class metadata 被移动到了后来的元空间。

为什么引入元空间?

在 Java 8 之前,Java 虚拟机使用永久代来存放类元信息,通过 - XX:PermSize、-XX:MaxPermSize 来控制这块内存的大小,随着动态类加载的情况越来越多,这块内存变得不太可控,到底设置多大合适是每个开发者要考虑的问题。

如果设置小了,容易出现内存溢出;如果设置大了,又有点浪费,尽管不会实质分配这么大的物理内存。

而元空间可以较好的解决内存设置多大的问题:当我们没有指定 -XX:MaxMetaspaceSize 时,元空间可以动态的调整使用的内存大小,以容纳不断增加的类。

元空间(metaspace)

元空间在 Java 8 移除永久代后被引入,用来代替永久代,本质和永久代类似,都是对方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存(native memory)。

元空间主要用于存储 Class metadata(类元数据),根据其命名其实也看得出来。

可以通过 -XX:MaxMetaspaceSize 参数来限制元空间的大小,如果没有设置该参数,则元空间默认限制为机器内存。

-------------本文结束感谢您的阅读-------------