Java内存结构简介

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动存在,有些区域依赖用户线程的启动和结束而建立和销毁。
当我们调用 Java 命令运行某个 Java 程序时,该命令将会启动一条 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程里。同一个 JVM 的所有线程、所有变量都处于同一个进程里,它们都使用该 JVM 进程的内存区。

类的加载

类的加载本文只是稍提,详情参见其余文章。
当Java程序需要使用某个类时,如果该类还未被加载到内存中,JVM会通过加载、连接(验证、准备和解析)、初始化三个步骤来对该类进行初始化。
在这里插入图片描述

加载

加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。

连接

连接分为三个阶段
验证
这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info等类型的常量。

初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

JVM内存结构

只要满足Java虚拟机规范,可以实现不同的JavaJVM。openJDK中的虚拟机默认是HotSpot虚拟机,下文以这种虚拟机来介绍java虚拟机内存结构。
在这里插入图片描述注意:方法区属于堆的一部分,该图画的不好。

JVM是按照运行时数据的存储结构来划分内存结构的,JVM在运行java程序时,将它们划分成几种不同格式的数据,分别存储在不同的区域,这些数据统一称为运行时数据。运行时数据包括java程序本身的数据信息和运行Java程序需要的额外数据信息,比如要记录当前程序指令执行的指针(PC指针)。

PC寄存器(程序计数器)

程序计数器也是线程私有的。
是一块较小的内存空间,它可以看做是当前线程所执行字节码的行号指示器。字节码解释器工作时是通过这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理等基础功能都需要依赖这个计数器来完成。
java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任一时刻,一个处理器内核只会执行一条线程中的指令。一个线程的执行时间片用完之后,会保存该线程的执行进度,切换执行的线程。所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。

虚拟机栈

Java虚拟机栈也是线程私有的,它的声明周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型。··
每当创建一个线程,JVM就会为这个线程创建一个对应的java栈。在这个栈中又会含有多个栈帧,线程调用的每一个方法会创建一个栈帧。栈帧存储局部变量表、操作数栈、动态 链接、方法返回值等信息。每一个方法从调用到执行的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。
Java栈的栈顶的栈帧就是当前正在执行的活动栈,方法执行完成,栈帧会弹出栈帧的元素作为这个方法的返回值,然后清除这个栈帧。当在这个栈帧调用另一个方法时,新的栈帧被创建,新的栈帧会变为当前活动栈。
局部变量表存放了编译期可知的各种基本数据类型、对象引用类型(一个指向对象地址的引用指针)等。
由于java栈是与线程对应起来的,这个数据不是线程共享的,所以我们不用关心它的数据一致性问题,也不会存在同步锁的问题。
在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于当前虚拟机所允许的深度,抛出 StackOverflowError;在Java虚拟机栈可以动态扩展可以动态扩展的情况下,如果扩展时无法申请到足够的内存,会抛出 OutOfMemoryError 。

Java堆是被所有线程共享的一块内存区域,一般是Java虚拟机管理的内存中最大的一块,在虚拟机启动时创建。此区域唯一的目的就是存放对象实例,不存放基本类型和对象引用。几乎所有的对象实例都在这里分配内存,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有对象都在堆上分配内存没那么绝对了。
堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,由于要在运行时动态分配内存,存取速度较慢。
堆是垃圾收集器管理的主要区域。由于现在垃圾收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代,再细致一点有Eden空间、From Survivor空间、To Survivor空间等。不得不提的是,Java堆空间的分类与JVM的实现方式有关,不同厂家可能会选择不同的堆结构。比如,华为公司就有自己的HuaweiJDK。
根据Java虚拟机规范,Java堆可以处理物理上不连续的内存空间中,只要逻辑空间连续即可。可以在启动 JVM 时手动指定堆的大小,不过初学阶段的我目前不必掌握。如果堆中没有内存完成实例分配,并且堆也无法扩展时,会抛出 OutOfMemoryError 异常。

方法区

方法区也属于堆的一部分,这个区域可以被所有的线程共享。
JVM方法区是用于存放类结构信息的地方。在解析class文件之后,每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码都存储在这个区域。
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集。HotSpot JVM 的设计团队把GC分带收集扩展到了方法区,或者说用永久代实现了方法区,但是但是这种方式其实更容易导致内存溢出问题。
当项目中存在对类的动态编译(反射),需要观察方法区大小。当方法区无法满足内存分配需求时,会抛出 OutOfMemoryError 异常。

常量池

常量池分为Class文件常量池和运行时常量池。Class文件常量池也叫静态常量池,而运行时常量池是我们平时常说的那个常量池。
javap -v 可以查看常量池

Class文件常量池

该常量池可存放字符串字面量,类信息,方法的信息等,占用了class文件较大部分的空间。其存储内容主要分为字面量和符号引用。

在这里插入图片描述

字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
符号引用包括:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。

运行时常量池

运行时常量池是方法区的一部分。运行时常量池是当Class文件被加载到内存后,Java虚拟机会将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个)。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
在这里插入图片描述

这种特性被开发人员利用比较多的便是 String 类的 intern() 方法。这个方法首先在常量池中查找是否存在一份equal相等的字符串如果有的话就返回该字符串的引用,没有的话就将它加入到字符串常量池中,所以存在于class中的常量池并非固定不变的,可以用intern方法加入新的。不过新的JVM已经把字符串常量池从方法区中拿了出来,但是要记得常量池仍是具备动态性的。反射方法仍然可以在运行期间将新的常量放入运行时常量池。

字符串常量池

字符串池,英文也叫String Pool。 在工作中,String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间。
在这里插入图片描述

采用字面值的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"aaa"这个对象,如果不存在,则在字符串池中创建"aaa"这个对象,然后将池中"aaa"这个对象的引用地址返回给字符串常量str,这样str会指向池中"aaa"这个字符串对象;如果存在,则不创建任何对象,直接将池中"aaa"这个对象的地址返回,赋给字符串常量。
采用new关键字新建一个字符串对象时,JVM首先在字符串常量池中查找有没有"aaa"这个字符串对象,如果有,则不必池中再去创建"aaa"这个对象了,直接在堆中创建一个"aaa"字符串对象,然后将堆中的这个"aaa"对象的地址返回赋给引用str1,这样,str1就指向了堆中创建的这个"aaa"字符串对象;如果没有,则首先在字符串常量池池中创建一个"aaa"字符串对象,然后再在堆中创建一个"aaa"字符串对象,然后将堆中这个"aaa"字符串对象的地址返回赋给str1引用,这样,str1指向了堆中创建的这个"aaa"字符串对象。
为何new String时也会在常量池中创建String?我们也不能在代码中显式获取到常量池中该字符串引用呀?
我的理解:字符串是经常使用的对象,例如new String(“hello”),JVM可能认为你后续还会使用该字符串。将其放入到常量池后,后续String hello = “hello” 时,可以直接取,无须再在常量池中创建了,创建对象也是需要消耗资源的。别看创建一个对象简单,要是一下子创建多个对象,要消耗的资源也是很大的。但是创建对象时,能找到的字符串便不必创建了,也能省一点资源。

本地方法栈

英文全称 Native Method Stacks。
和虚拟机栈的作用差不多,只不过是为JVM使用到的native方法服务的。
Java官方对于本地方法的定义为methods written in a language other than the Java programming language,就是使用非Java语言实现的方法,虚拟机规范未对实现语言作规定。通常指的一般为C或者C++,因此这个栈也有着C栈这一称号。一个不支持本地方法执行的JVM没有必要实现这个数据区域。本地方法栈基本和JVM栈一样,其大小也是可以设置为固定值或者动态增加,因此也会对应抛出StackOverflowError和OutOfMemoryError错误。

参考文献

详解JVM常量池、Class常量池、运行时常量池、字符串常量池(心血总结)
《深入理解Java虚拟机》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值