Java - JVM内存区域的划分

2023-12-13 05:09:50

Java 程序运行时,需要在内存中分配空间。为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

分配:通过关键字new创建对象分配内存空间,对象存在堆中。
释放 :对象的释放是由垃圾回收机制决定和执行的

JVM的内存可分为3个区:

堆(heap)
栈(stack)
方法区(method,也叫静态区)

?

堆区: 程序员自己申请,运行时线程公有

通过new生成的对象都存放在堆中,对于堆中的对象生命周期的管理由Java虚拟机的垃圾回收机制GC进行回收和统一管理


jvm只有一个堆区(heap),且被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身和数组本身;
优点是可以动态分配内存大小,缺点是由于动态分配内存导致存取速度慢。

虚拟机栈: 系统自己分配,运行时线程私有

栈内存主要是存放一些基本类型的变量和对象的引用变量。最典型的就是我们new一个对象时,对象名作为变量就存放在栈内存中


栈内存有一个很重要的特殊性——在栈中的数据可以共享(已存在的值不会再次创建)
int a = 3;
int b = 3;
(编译器先处理int a =3;首先它会在栈中创建一个变量为a的引用,然后查找栈中是否有3这个值,如果没找到,就将3存放进来,然后将a指向3。接着处理int b)


每个方法执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等。局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress类型


每个方法从调用直至完成的过程,都对应着一个栈帧从入栈到出栈的过程。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,方法的调用过程也是由栈帧切换来产生结果。


在JVM规范中,对这个区域规定了两种异常情况:
1.如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;
2.如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈:


和虚拟机栈所发挥的作用基本一致,不同的是:

虚拟机栈为虚拟机执行Java方法(字节码)服务
本地方法栈则为虚拟机使用到的Native方法服务
本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

方法区(静态区):

是各个线程共享的内存区域,它用于存储class二进制文件,包含所有的class和static变量,包含了虚拟机加载的类信息、常量(常量池)、静态变量(静态域)、即时编译后的代码等数据。


方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。


被所有的线程共享,方法区包含所有的class(class是指类的原始代码,要创建一个类的对象,首先要把该类的代码加载到方法区中,并且初始化)和static变量。

常量池:在编译期间就将一部分数据存放于该区域,包含以final修饰的基本数据类型的常量值、String字符串。(在java6时它是方法区的一部分;1.7又把他放到了堆内存中;1.8之后出现了元空间,它又回到了方法区。)


静态域:存放类中以static声明的静态成员变量。

程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。
程序计数器:
一个非常小的内存空间,用来保存程序执行到的位置(线程私有),它可以看作是当前线程所执行的字节码的行号指示器。

Metaspace元空间:

在JDK1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了MetaSpace(元空间)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
元空间的大小仅受本地内存限制,可以指定元空间大小。


Java8为什么要将永久代替换成Metaspace?
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

?基本数据类型

int a = 3;
int b = 3;

编译器先处理 int a = 3;首先它会在栈中创建一个变量为 a 的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将 a 指向3的地址。

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

对象

public class Person {
? ? private String name;
? ? private int age;

? ? public Person(String name, int age) {
? ? ? ? this.name = name;
? ? ? ? this.age = age;
? ? }

? ? public String getName() {
? ? ? ? return name;
? ? }

? ? public void setName(String name) {
? ? ? ? this.name = name;
? ? }

? ? public int getAge() {
? ? ? ? return age;
? ? }

? ? public void setAge(int age) {
? ? ? ? this.age = age;
? ? }
}

一个类通过使用new运算符可以创建多个不同的对象实例,这些对象实例将在堆中被分配不同的内存空间,改变其中一个对象的状态不会影响其他对象的状态:

? ? ? ? Person one = new Person("小明",15);
? ? ? ? Person two = new Person("小王",17);

在堆内存中只创建了一个对象实例,在栈内存中创建了两个对象引用,两个对象引用同时指向一个对象实例。

? ? ? ? Person one = new Person("小明",15);
? ? ? ? Person two = one;

数组
数组是一种引用类型,数组用来存储同一种数据类型的数据。

一旦初始化完成,即所占的空间就已固定下来,即使某个元素被清空,但其所在空间仍然保留,因此数组长度将不能被改变。

int[] array = new int[5]

首先会在栈中创建引用变量,在堆中开辟5个int型数据的空间,该引用变量存放数组首地址,即实现数组名来引用数组。

?

文章来源:https://blog.csdn.net/m0_65228708/article/details/134907907
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。