JVM内存区域详解


1. 为什么要有JVM?

这个问题可以对比C/C++语言,C++在管理内存时需要自行对new/malloc出来的内存进行delete/free,因此内存的管理权限是交给了JVM的,这样的话使得程序员能够更加专注于业务程序的开发,而不是底层的内存管理等操作。

正因如此,Java程序员需要了解底层的内存管理机制才能够较好的排查内存溢出和内存泄漏问题,否则的话无从下手

2. 了解JVM的运行时数据区吗?

首先先来聊聊什么是运行时数据区,所谓的运行时数据区其实就是Java虚拟机在执行Java程序到的过程中把它所管理的内存划分为若干个不同的数据区域,这些区域有各自的用途

  • 有些区域随着虚拟机的进程的启动而消亡
  • 有些区域则是依赖用户线程的启动和结束而建立和销毁

一个Java程序运行时就对应一个进程,那么进程内部就有共享的部分和私有的部分

其中共享的部分就是线程都能够访问到的部分,私有的部分就是线程所私有的

还有一个区域是在进程之外的可访问的本地内存区域

3. 什么是程序计数器?它是私有的吗

程序计数器(PC)是一块较小的内存空间,它在OS中也有相关的概念,其最主要的作用就是为线程指示了所执行字节码的行号,以便完成循环、分支、跳转、异常处理等动作,可以简述为程序控制流的指示器

JVM中是允许多线程并发执行的,因此在多线程进行CPU的轮转的时候,这时候就要求每个线程在让出CPU时保存现场,其中这个现场就包括了当前线程所执行的字节码到了哪一个位置,因为每一个线程所运行的task是不一致的,如果程序计数器是公有的,那么就会导致线程之间互相覆盖之间的任务进度,导致线程切换的时候上下文丢失,最终导致运行失败。

因此在JVM的设计中,各个线程都需要有一个独立的程序计数器,各条线程之间计数器是互不影响的,独立存储,这种区域就叫做线程私有的内存。同时它也不会产生OOM的问题

4. 什么是虚拟机栈?它是私有的吗

栈:线程运行需要的内存空间,栈中的元素称之为栈帧,一个栈帧就涉及到一次的函数调用,为了保证方法的执行是独立的,因此又需要为这些方法分配内存,栈帧内通常有参数,局部变量,返回地址,然后当方法的执行的时候,就可以在自己的这块独立空间中执行了。

每个栈由多个栈帧组成,对应着每次方法调用时所占的内存,每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

虚拟机栈可以看成是线程的活动记录,因此它的声明周期和线程的声明周期是相同的,随着线程的创建而创建,随着线程的死亡而死亡,实际上虚拟机栈就是用来描述当前线程的运行状态的

线程的执行可以看成是函数的互相调用,那么函数的互相调用存在的问题是

  • 怎么传参?
  • 函数执行完后,返回值如何传递?如何回到函数的原本执行位置?

而这就涉及到栈帧了,一般来说,可以概述为以下的过程:参数入栈,返回地址入栈,代码区跳转

注意在代码区跳转的时候,会先保存当前函数的执行状态,其具体的过程就是将当前的栈指针ebp的值入栈,以便后续的使用

然后在函数执行完毕后,它先保存被调用函数的返回值到eax寄存器中,然后从栈中取出esp,恢复前一个函数的执行状态。


总结一下,虚拟机栈就是线程的活动记录,它保存了线程在执行过程中的函数调用关系,其中最重要的包括有参数传递,返回值传递,函数执行流程保存,函数执行流程恢复四个步骤

栈是由一个个栈帧组成的,一个栈帧可以看做是函数执行过程中的活动记录

那么函数执行需要什么?

  • 函数执行可能需要接收外部传过来的变量:因此有局部变量表
  • 函数执行可能需要中转站来保存中间的计算结果:因此需要有操作数栈
  • 函数执行可能需要跳转到别的函数进行执行

在计算机组成原理中,我们学习到的函数地址均是实际的内存地址,因此只需要直接将内存地址取出并且跳转即可,但是在Java中,在.java文件被编译为.class文件的时候,所有的变量和方法引用都是作为符号引用的,简单来说,就是为了保证.class的可读性,为这些内存地址取了一个别名,然后这个别名是被保存在.class文件的常量池里面的

那么当我要根据这个别名去找到它实际的内存地址的时候,就执行一个动态链接的过程,它的作用是将常量池中指向方法引用转化为内存地址中的直接引用。

补充:Java方法有两种返回方式,一种是return语句的正常返回,一种是抛出异常式的返回,但是无论如何,函数的返回都是线程的活动记录,因此每次函数的调用完毕,都涉及到一个栈帧的弹出。

常见的关于栈的异常及其原因分析

  • StackOverFlowError:这种情况是栈的内存大小不允许动态扩展,是定死的,当线程请求栈的深度超过当前Java虚拟机的最大深度的时候,就会抛出这个异常
  • OutOfMemoryError:如果栈的内存大小可以动态扩展,如果虚拟机在动态扩展栈的时候无法申请到足够的内存,那么就会抛出这个异常

虚拟机栈为什么是私有的?

这是因为虚拟机栈说白了就是线程的活动记录,它用来记录线程在执行流程中的运算操作,函数调用操作,那么必然是每一个线程都具有一个自己的活动记录。

垃圾回收是否会涉及栈内存

-Xss size:指定栈内存,stackSize

不会,这是因为栈帧在函数执行完毕后就会自动弹出,当线程运行完毕后,虚拟机栈就会被自动回收

这是因为函数执行的生命周期是十分明确的,不需要那些复杂的算法来判断线程是否结束运行。

方法内的局部变量是否是线程安全的?

栈帧内的局部变量表是只属于这个线程的,因此在函数中的局部变量是相互隔离的,因此可以说:方法内的局部变量就是线程安全的,但是针对于一个特殊的案例,就是

static int x = 0;

它意味着是一个类变量,因此的话是共享的,它的运行机制就是在对共享的变量写完之后,再将这个变量的值刷回到栈中,因此会产生线程安全问题。

  • 如果方法内的局部变量没有逃离方法的作用范围,那么它就是线程安全的
  • 如果方法内的局部变量引用了对象并且逃离了方法的作用范围,那么它可能产生线程安全的问题。

栈帧过多导致栈内存的溢出,也有可能是栈内的变量过大,从而导致栈所分配的内存被用完了

CPU占用过多要怎么排查?

  • 第一步,使用top -n 1命令,查看当前时刻下所有运行中的进程占用CPU的情况,定位占用CPU高的Java程序的PID
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                
  1 root      20   0  191180   3984   2532 S   0.0  0.0   2:17.00 systemd                                                                                
  2 root      20   0       0      0      0 S   0.0  0.0   0:00.31 kthreadd 
  • 通过ps H -eo pid,tid,%cpu | grep 32655,查询当前进程下的线程运行情况

首先PID是进程的ID,tid是线程的ID,%cpu是每个线程所使用CPU的占比,然后就可以查询到相关线程的占用情况了

  • 通过jstack性能监控工具,注意:jstack后接的线程ID要是十六进制的,因此要先进行换算,然后就能够定位到相关的问题代码了

程序运行时间过长要怎么排查?

一般来说,这种情况是发生了死锁,可以通过jstack接一个进程的ID,然后找到线程ID

然后就会显示死锁在哪里产生的,找到多少个死锁这样的问题了

5. 什么是本地方法栈?它是私有的吗?

要了解什么是本地方法栈,首先要了解什么是本地方法

本地方法:它指的是Java内置的一套使用其他语言编写的代码所对应的方法,那么为什么要使用这种方法呢?使用纯Java不行吗?这与操作系统有关,一般来说,操作系统都是基于c/c++语言开发的,因此提供的相关接口也都是c/c++语言才能调用的,因此为了发起系统调用,就需要额外使用本地方法进行函数的调用

那么既然是本地方法,那么其运行也肯定会产生活动记录,与Java方法类似,这个本地方法栈是为Native方法服务的,但是在HotSpot虚拟机中和Java虚拟机栈合二为一。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。

本地方法栈是私有的吗?

与虚拟机栈类似,是私有的

6. 什么是堆?它是私有的吗?

是在数据结构中是一种类似平衡二叉树的数据结构,能够以logn的复杂度进行数据的CRUD,通常用于大数据的处理,在JVM则是所有线程所共享的一块区域,在Java中,由于存在大量的对象,因此这个区域的唯一目的就是存放对象的实例

几乎所有的对象实例以及数组都在这里分配内存

还有一些情况,可以使用JIT编译器和逃逸分析技术进行优化,使得对象不在堆上分配,而是使用栈上分配或者标量替换的技术进行优化。从JDK1.7开始就已经开启了逃逸分析

如果某些方法中的对象引用没有被返回或者没有被外面使用,也就是没有逃逸出去,那么对象就可以直接在栈上分配内存

Java堆是垃圾收集器管理的主要区域,因此也被称作为GC堆(Garbage Collected Heap),从垃圾回收的角度来看,由于现在的垃圾收集器都采用的是分代垃圾收集算法,所以Java堆可还可以细分为老年代和新生代,再细致一点的有:EdenSurvivorOld等空间

进一步划分的目的是更好的回收内存或者更快地分配内存

这里的话可以记为三层结构

  • 第一层:新生代,可以细分为EdenSurvivor,其中Sruvivor可以分为S0S1
  • 第二层:老年代
  • 第三层:永久代,值得注意的是,在JDK8版本之后永久代已经被元空间所取代

元空间使用的是直接内存

大部分情况下,对象都会首先在Eden区进行分配,在一次新生代的垃圾回收之后,如果对象还存活,那么就进入到Survivor,可以通过设置JVM参数 -XX:MaxTenuringThreshold来设置,默认是在15次的新生代的垃圾回收之后,新生代中的对象就会晋升到老年代中

关于堆内存出现的错误类型及其原因分析

java.lang.OutOfMemoryError:GC overhead Limit Exceeded:当JVM花了很多时间执行垃圾回收并且只能回收很少的堆空间的时间,就会发生此错误

java.lang.OutOfMemoryError: Java heap space:假如在新建对象的时候,堆内存中的空间不足以存放创建的对象就会引发此错误和配置的最大对内存有关,而且受制于物理内存的大小,最大堆内存可以通过-Xmx来进行参数的配置

堆内存诊断

jps工具:这个可以用来查看当前系统中有哪些Java进程

jmap工具:这个可以用来查看堆内存的占用情况

jmap -heap JID#查看堆内存的占用

jconsole工具:图形界面的,多功能的检测功能,可以连续检测

垃圾回收后,内存占用依然很高,要怎么排查?

先通过jps工具来查看当前的Java进程ID

然后再通过jmap来查看这个进程的内存占用

jvisualvm:基于图形界面的工具监控JVM,通过其中的一个功能,可以截取当前的堆的内存分布情况的快照,包括有内存中有哪些对象,这些堆的内存等

7. 什么是方法区?它是私有的吗?

方法区只是一个概念,它是一种实现的规范,具体的规范是这样描述的:

当虚拟机要使用一个类的时候,首先虚拟机要读取它的.class文件,读取并且解析.class文件获取相关信息,然后再将相关的信息存入到方法区,方法区会存储已经被虚拟机加载的类信息、字段信息、方法信息、常量信息

那么在HotSpot中是否有方法区的存在呢?是没有的,而是使用了永久代或者元空间,方法区和永久代/元空间的关系就类似于接口与类的关系一样

  • 方法区:规定了数据的存储形式,数据的类型
  • 永久代/元空间:是具体存储数据的内存区域

为什么要将永久代替换为元空间呢?

永久代有一个JVM本身设置的固定上限大小,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间依然可能存在溢出的问题,但是出现的概率更小

JDK1.8出现方法区出现元空间溢出的错误的时候

java.labg.OutOfMemoryError:MetaSpace

可以使用-XX:MaxMetaspaceSize来限制最大元空间的大小,默认只受系统内存的限制

-XX:metaspaceSize:调整标志定义元空间的初始大小,如果没有指定这个标志,MetaSpace将根据运行时的应用程序需求动态调整大小

当元空间的大小到达metaspaceSize的时候,就会触发Full GC

元空间存放的是类的元数据,这样的话加载多少类的元数据就不由MaxPermSize控制了,而是由可用空间进行控制,这样能够加载的类就更多了

在JDK8以后合并HotSpotJRockit的代码的时候更加简便

总结:

  • 首先,永久代有一个JVM本身设置的固定大小上限,一旦进程启动就不能够修改,因此,一旦出现生产事故,比如说 序员对内存空间大小产生误判,那么就会导致系统宕机,不得不重启进程,这将会导致内存数据的丢失,这导致对于系统的运维十分不灵活,而使用了元空间来替代永久代,能够最大限度地发挥机器对Java进程的支持,在系统内存可用的情况下,使得Java程序能够尽量地运行下去
  • 其次,能够加载更多的类,元空间存放的是解析.class文件后产生的方法信息,常量信息等,如此一来,加载类的数量就不由maxPermSize决定,而是由系统可用空间决定了
  • 最后,这对于JDK的版本迭代是有好处的,比如说在合并HotSpotJRockit的时候,能够更加便捷地进行合并

关于方法区的常用参数调整

  • -XX:PermSize = N:方法区(永久代)初始大小
  • -XX:MaxPermSize = N:方法区(永久代的最大大小),超过这个值将会抛出OutOfMemoryError

JDK1.8的时候,原空间被启用,响应的参数被修改为

  • -XX:MetaSpcaeSize = N:设置Meta的初始大小,当达到这个大小的时候会触发FullGC
  • -XX:MaxMetaSpaceSize = N:设置Meta的最大大小

从上面的讲述来看,元空间/永久代存储的是类的元数据,每一个线程都可以访问这些元数据,因此它是公有的

9. 什么是运行时常量池?它是私有的吗?

运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息,还有一项信息是常量池表,运行时常量池也是每个类都有一个,存放的内容有:

  • 用于存放编译期生成的各种字面量
  • 符号引用

既然是方法区的一部分,那么就是公有的

10. 什么是字符串常量池?它是私有的吗?

二进制字节码:类的基本信息、类的常量池、类的方法定义、包含了虚拟机指令

那么常量池,就是一张表,虚拟机在执行的时候,指令并不包含有具体的参数,而是一些符号引用,那么这些符号引用具体的值到底在哪里呢?就是常量池中。

运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会被放入到运行时常量池中,并且把里面的符号地址变成真实地址。

字符串常量池是JVM为了提高性能和减少内存消耗针对字符串String类专门开辟的一块区域,主要的目的是为了避免字符串的重复创建

String aa = "ab";
//第一次:用字面量的方式创建一个字符串
//1. 在堆中创建字符串对象"ab"
//2. 将字符串对象"ab"的引用保存在字符串的常量池中
String bb = "ab";
//第二次:用字面量的方式创建一个字符串
//1. 检查字符串常量池中是否有字符串对象"ab"
//2. 有,那么直接返回字符串常量池中字符串对象"ab"的引用
//aa == bb == true

怎么来理解?通过阅读源码可知StringTable本质上就是一个HashSet<String>,其容量为stringTableSize,可以通过-XX:StringTableSize来进行设置

其中,HashSet<String>保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象

JDK1.7以前,字符串常量存放在永久代,JDK1.7字符串常量池和静态变量从永久代移动到了Java堆中

而在JDK1.7之后,将字符串常量池移动到了堆中

为什么要这样做?

这是因为永久代中的GC时机通常十分苛刻,通常是不得不执行GC的时候才会对永久代中的对象执行GC,然而在Java程序的运行过程中,通常会有很多字符串对象等待被GC,因此放在永久代中是不合适的,应该要放在堆中,这样的话无用的字符串常量才能被快速GC

从字符串常量池的用处来看,它是每个线程都可以访问的一部分,因此是公共的

11. 什么是直接内存?

直接内存不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分也被频繁使用。

本机直接内存的分配不会受到Java堆的限制,但是既然是内存就会受到本机总内存大小以及处理器寻址空间的限制

12. 对象的创建流程是怎么样的?

对于程序员来说,对象的创建一般就是三步,导入自己想要new的对象的类,使用new为这个对象分配内存,为这个对象的实例变量配置参数,初始化完毕

但是对于JVM来说,这个过程是比较复杂的,那么实际上也是差不多,但是JVM自动地为我们额外的两件事情,下面详细展开说说

new.class文件中是一条指令的形式,在执行实际的操作之前,必须先检查参数是否有效,如果无效,那么不能执行这个指令

类的加载检查

  • JVM检查new后接的指令参数是否能够在运行时常量中找到对应的符号引用,如果找不到对应的符号引用,证明没有这个类,抛出异常,如果找到了这个符号引用,那么就查询这个符号引用代表的类是否已经被加载过了?

如果已经被加载过了,那么就不需要执行的类的加载,否则的话就还需要执行类的加载

执行内存分配

  • 在检查到new后的参数是有效的之后,那么就开始执行new的具体过程,new本质上就是为对象分配内存,分配内存的方式有两种,分别是指针碰撞空闲列表,在操作系统中,我们学习的大部分内存调度算法都是基于空闲列表来使用的

指针碰撞风格:这种内存分配方式的特点是,它始终维护了这样的内存区域,使得内存区域的一边是被占用的内存,一边是没有被占用的内存,分配内存时,只需要将没有分配到的内存指针向下移动即可,使用这种分配方式的GC收集器有SerialParNew

优点:内存分配快速,不需要执行复杂的内存分配算法,比较好理解

缺点:当对象内存需要释放的时候,就会导致大量的内存的整理,维护开销大,可能带来严重的阻塞问题

空闲列表技术:虚拟机会维护一个空闲列表,这个列表维护着哪些内存区域是可用的,然后按照一定的内存分配算法,如NF等这一类的算法,为新对象分配内存,最后更新列表记录

优点:能够充分利用内存,只需要维护一个空闲列表即可

缺点:存在外碎片,每次分配内存都需要执行复杂的算法,分配内存速度取决于算法的优劣

选择以上两种方式的哪一种,是基于Java的堆内存是否规整的,而堆内存是否规整,取决于采用的算法是标记-整理算法还是标记-清除算法

共享内存线程安全问题

与操作系统中的模型一样,共享内存必然存在的是线程安全问题,JVM需要针对线程安全的问题做出一定的限制

线程安全问题的实例:

正在给对象A分配内存,指针还没来得及修改,对象B又使用了相同的指针进行内存的分配,而且它还修改成功了,回去的时候假如A没有察觉到,那么A就要和B使用同一块内存

一般来说有两种机制

第一种方式:CAS(乐观锁)机制,这种方式是基于CAS实现的,那么它是CAS的具体对象是内存中的指针或者是空闲列表,用CAS来检测是否产生冲突,如果产生冲突则重试

第二种方式:TLAB+同步加锁,TLAB就是线程本地缓冲的缩写, 它的工作原理是这样的,JVM为每一个线程预先在Eden区分配一块内存,JVM分配内存的时候先在TLAB进行分配,只有等TLAB中的内存不足以为对象进行分配的时候,才会对共享内存区域进行加锁,执行同步方法进行分配

初始化零值

我们平时的Java对象如果没有程序员人为的手动注入,那么它是有初值的,这个初值是谁给的呢?

就是在一步完成的,这一步是程序员察觉不到的

设置对象头

对象头对于程序员来说也是不可见的,因此这一步也是JVM来帮我们完成的

它的工作是就是设置当前对象的一些元属性,比如说它是哪个类的实例,怎么样才能知道哦这个类的元数据信息,对象的哈希码(只有在被用到的时候)才会执行计算算法,对象的GC分代年龄等,以及还有锁的相关字段等

执行init方法

至此,一个对象对于JVM而言,就被创建出来了,但是程序员还要求要按照规定的方法进行初始化,因此这时候就要执行init方法了

这个方法就是执行相应的构造方法

13. 对象的内存布局是怎么样的?

对象的内存布局包括有三部分:

第一部分是对象头,对象头中包含了对象的元数据,其中的主要内容有对象自身运行时的数据以及类型指针,对象自身运行时的数据包括有锁的状态信息,GC分代年龄等,而类型指针则是为了说明对象的类型所设定的指针,它的指向应该是在方法区的,对象头在HotSpot中的大小是8Byte

第二部分是实例数据

第三部分是字节填充,字节填充是为了满足HotSpot中对象8Byte对齐的要求。

14. 对象的访问定位是怎么样的?

这个问题就是讲,JVM中对象那么多,那么怎么样才能快速找到一个对象呢?是一个个遍历过去吗?

Java程序通过栈上的reference数据来操作堆上的具体对象,对象的访问方式由虚拟机实现而定,目前主流的访问方式有使用句柄直接指针

简述通过句柄访问对象的过程是这样的:首先在线程的栈中,会存储对象的引用,那么这个引用实际上就是存储的句柄,在Java进程中,将会在中开辟出一个句柄池,这个池子存储了所有的对象的对应的句柄

句柄可以表示为句柄:=<到对象实例数据的指针,到对象类型数据的指针>

然后通过这两个指针就能够找到实例数据和类数据

直接指针技术

如果使用直接指针访问reference中存储的直接就是对象的地址

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本

但是它存在一个问题,比如说在GC升级过程中,由于要将空间腾出来,那么就意味着要将数据原封不动的复制到另一个空间中,这也会带来性能的开销

15. 总结

JVM的内存区域可以分为两部分

第一个部分是线程共享的区域,有以下几个部分

  • 堆内存:堆内存位于进程之中,是受JVM进程管理的,它的主要作用就是为对象分配内存
  • 直接内存:直接内存不是在进程之中的,是受操作系统进行管理的,它的作用是为一些需要与内核频繁打交道的过程提供空间支持,比如说在NIO中,初始的select/poll模型是而这样工作的:

首先它会将所有的已连接的socket的对应的fd装载到一个文件描述符的set中,然后将这个数据拷贝到内核中,然后再由内核设定的程序对fd进行扫描,如果有准备好了的fd,然后再将这个set返回去。

那么我们可以看到涉及到两次的拷贝,第一次的拷贝是从用户空间拷贝到内核态,然后还要从内核态拷贝到用户空间,为什么不直接放在内核缓冲区呢?因此,这个直接内存就为这个减少拷贝提供了基础

在这种工作模式下,JVM申请的内存空间会以引用的方式提供给JVM进程使用,从而使得数据在用户空间和内核空间减少拷贝次数,DirectByteBuffer

  • 方法区:方法区实际上是一个概念,它规定了类在JVM中的存在形式,记录了类的元数据,比如说类信息,字段信息,常量静态变量等,因此,它实际上是一个接口,而实现这个接口的是永久代/元空间,它包含了一个重要的数据区,叫做运行时常量池,这个池中存储的是一些字面量和符号引用

第二部分是线程私有的区域,有以下几个部分

  • 程序计数器:程序计数器的作用就是标志了一个指针,这个指针是用来指明当前线程执行的字节码是哪一行,通过这个指针来标志当前线程的执行进度

  • 虚拟机栈:虚拟机栈的作用是保存每个线程的活动记录,它记录了每个线程的内部代码执行逻辑,比如说函数调用,函数参数传递,函数返回值传递等,都是基于虚拟机栈完成的,虚拟机栈中为了保存这些活动记录,有自己的一套执行组件,比如说有局部变量表,它用来记录当前线程执行时,局部变量的变化情况,有一个操作数栈,当要执行某些运算的时候,需要将中间结果保存下来,这就是操作数栈,动态链接实际上一个服务,它的作用是当一个方法要执行其他方法的时候,就需要将字节码中的符号引用转化为在内存地址中的直接引用,它的过程实际上就是拿到这个符号引用,然后到运行时常量池中寻找对应的方法引用,然后执行。

  • 本地方法栈:本地方法栈的作用本质上也是记录线程的活动记录,只不过它服务的对象是本地方法,当要执行本地方法的时候,就也需要有一套记录本地方法活动的栈,因此在线程内部还需要内置这个栈来辅助运行

16. 对运行时常量池的理解

首先要讲讲什么是常量池,常量池在JVM中的含义是:当Class文件被Java虚拟机加载进来后存放在方法区各种字面量和符号引用, 下面详细讲讲这两个数据结构

  • 字面量:文本字符串,被声明为final的常量值,基本数据类型的值,其他
  • 符号引用:类和结构的完全限定名,字段名称和描述符,方法名称和描述符

那么运行时常量池是方法区的一部分,运行时常量池是Class文件被加载到内存后,Java虚拟机会将Class文件常量池里的内容转移到运行时常量池中,运行时常量池也是每个类都有一个

运行时常量池相对于Class文件常量池的一个重要区别就是它具备动态性,并非预置入Class文件中常量池的内容才能进入方法区的运行时常量池,在运行时也可能将新的常量放入池中

17. 对字符串常量池的理解

首先,为什么需要字符串常量池?这是因为字符串在在Java程序中的使用频率太高,如果不对字符串进行优化而是每一个字符串都创建一个新的对象,那么就会导致内存空间的浪费,因此有了字符串常量池

这个字符串常量池在JDK1.7以前是放在永久代里面的,而在之后修改为了放在堆内存中

这是因为字符串来得快去得也快,如果放在永久代的话将会容易导致永久代产生OOM,即使是后来的元空间也是如此

18. new一个String产生多少个对象?

这个问题比较复杂,可以通过分析各种创建字符串的形式来分析每种方式对应的实例如何存放在内存中?

第一种方式:以字面量的形式声明

String a = "haha";
String b = "haha";

这个形式,在.class常量池中就有haha这个常量,因此在创建运行时常量的时候,就会将这个常量加入到运行时常量中,比如说在a = "haha"的时候,就是先到常量池中找到有没有haha这个常量,如果有这个常量,那么就直接返回这个对象的引用

否则的话,就在常量池中创建这个对象, 再返回引用就可以了

第二种方式:new

String a = new String("haha");
String b = new String("haha");

通过new关键字创建的字符串引用,字符串常量和堆内存中都会有这个对象,没有的话就创建,最后返回的是堆内存中的对象引用。

具体的创建逻辑是这样的,因为存在haha这个字面量,因此它会先去字符串常量池中寻找有没有这个字符串

如果没有这个字符串,那么就会直接在字符串常量池中创建一个字符串对象,然后在堆内存中创建一个字符串对象

如果有这个字符串,那么就直接去堆内存中创建一个字符串对象

最终将堆内存的字符串返回

第三种方式:intern()

String a = new String("lizhi");
String b = a.intern();

System.out.println(a == b); // 结果为false

先通过new关键字创建创建一个字符串对象,此时字符串常量池和堆内存空间都有一个lizhi的字符串引用。

当调用这个方法的时候,会先判断字符串常量中是否有该对象的引用,通过equals()方法进行判断, 如果存在就返回字符串常量池中的引用

如果不存在,那么就会将这个字符串直接添加到字符串的常量池中,然后返回引用


文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录