从一道面试题深入了解java虚拟机内存结构

  • 2019 年 10 月 3 日
  • 筆記

记得刚大学毕业时,为了应付面试,疯狂的在网上刷JAVA的面试题,很多都靠死记硬背。其中有道面试题,给我的印象非常之深刻,有个大厂的面试官,顺着这道题目,一直往下问,问到java虚拟机的知识,最后把我给问住了。
我当时的表情是这样的:

后来我有机会面试别人了,也按照他的思路出面试题,很多已经工作了2年的程序员,结果也和我当年一样,都败在java虚拟机知识上。

我们先看面试题:

String str1 = "hello Alunbar";  String str2 = new String(str1);

会创建几个对象?

网上给出的解释是创建2个对象,str1对象在常量池中,str2对象在堆中。

下面是我和面试官的对话。
面试官:上面的代码创建了几个对象?
我:2个。
面试官:为什么是2个呢?
我:str1对象在常量池中,str2对象在堆中。用“=”等号创建String对象时,会先从字符串常量池中查找是否已经存在字符串对象,存在就直接返回引用地址,否则创建字符串对象并返回引用地址。

面试官:为什么会在常量池中创建字符串对象?
我:。。。我思考了半分钟,尴尬的回答不知道。
面试官:说说jvm虚拟机的内存结构。
我:。。。我再次面露难色,场面一度非常尴尬。

这次面试结束之后,我就回去疯狂查找资料,了解jvm虚拟机的相关知识。

这也是我的第一次面试,给我的印象非常之深刻。

下面我们来说说面试官的两个问题。
1、为什么会在常量池中创建字符串对象。
2、java虚拟机的内存结构。

先来看第一个问题。
为什么会在常量池中创建字符串对象?

字符串在所有编程语言中都是最常用的类型,其他的数据类型都可以转换为字符串类型,像int、long等基本数据类型和String都是可以互相转换的。为了提高字符串的使用效率,jvm虚拟机中特别开辟了一个常量池的内存空间,用于存储基本数据类型的对象,常量池中的对象是可以相互共享的,当然也包括了String。

我们一般将储存字符串的常量池成为字符串常量池。字符串常量池中会存在很多已经创建好的字符串对象,由于String类是用final修饰的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。

我们来看一段的代码:

String s1 = "Hello";  String s2 = "Hello";

这段代码只创建一个对象,s1和s2是同一个对象。根据上面的解读,java String s1 = "Hello"这行代码会先在字符串常量池查找Hello对象,没有发现,然后创建Hello对象并将引用返回给s1。java String s2 = "Hello"这行代码,也先去字符串常量池中查找Hello对象,发现已经存在,则直接返回给s2。因此s1和s2是同一个对象。

接着说说使用new创建字符串对象。
通过new创建字符串对象,会在堆中开辟一块新的内存空间,存储String字符串对象,因此使用new方式都会生成新的字符串对象,不管字符串的内容是否一致,使用new创建字符串时存在堆中,堆中的对象会被回收,而使用“=”创建字符串对象,是存放在常量池中,不会被回收,因此建议使用“=”的方式创建字符串对象,避免不必要的java对象创建和销毁的开销。

我们来看下面的创建字符串对象时的内存结构图:

s1和s2是通过“=”创建的字符串对象,它们的内存地址都一样,s3是使用new方式创建的字符串对象,s3和s1、s2的内存地址不一样。

现在接着看第二个问题。

java虚拟机的内存结构
虚拟机内存结构是一个很复杂的问题,这里只能讲一个大概,主要讲各个内存区域的作用。

java虚拟机由类加载器、运行时数据区和执行引擎构成。如下图所示:

平时我们说的java虚拟机内存结构,就是讲运行时数据区。

java虚拟机在执行java程序时,会将内存分为几个区域:程序计数器、方法区、虚拟机栈、本地方法栈、堆。

其中,方法区和堆是线程共享,程序计数器、虚拟机栈、本地方法栈时线程不共享。

1、程序计数器
只要学过汇编语言,对这个程序计数器都好理解,就是记录下一条将要执行的字节码指令。

通过操作系统知识我们知道启动一个程序时,就会创建一个进程,因此在执行java程序时,就会创建一个进程,java虚拟机就是一个进程。

一个进程中由多个线程组成,在任何一个时刻,java虚拟机只能执行一条线程中的指令。

java虚拟机通过读取某一个线程中的程序计数器决定该线程需要执行哪个基础功能,例如循环、读取数据库、跳转、异常处理、线程恢复等。

因此每个线程的程序计数器是相互独立,互不影响的。

2、java虚拟机栈
就是我们常说的java栈,在执行方法时,会在java栈中创建一个栈帧,用于存储局部变量表、操作数栈、方法出口等信息。

局部变量表中又会存放执行方法需要的boolean、char等各种基本数据类型,对象引用等。局部变量表大小在代码编译期间就已经确定。java栈也是线程私有。

创建线程时同步创建java栈,线程结束,java栈也同时销毁,释放占用的内存。

3、本地方法栈
和java虚拟机栈功能类似,有的虚拟机会将java虚拟机栈和本地方法栈合并。本地方法栈主要为虚拟机执行Native方法提供服务。

4、java堆
虚拟机中最大的一块内存区域,虚拟机启动时创建,主要用于存放对象实例,这块内存区域由所有线程共享。这个区域内的对象,可以被所有的线程访问。

这个区域也是java虚拟机重点管理的对象,当这块区域中的对象没有被引用,达到回收标准时,就会被java垃圾收集器回收,释放占用的内容空间。

java堆分为新生代和老年代,新生代又分为Eden空间、From Survivor空间和To Survivor空间。

使用new操作创建对象时,就会在这个区域开辟一块内存用于存储对象。

上面提到的java String str1 = new String("Hello")创建字符串,就会在java堆中开辟一块内存用于存储str1对象。

5、方法区
方法区主要存储被虚拟机加载的类信息、常量、静态变量等数据,我们也将这个内存区域称为永久代,这个区域不会进行内存回收。
方法区和java堆一样,所有线程共享。

方法区中包含一个运行时常量池,上面提到的java String str = "Hello"创建字符串,就是在运行时常量池中创建“Hello”对象。

小结:
1、两种创建字符串对象的差异。
2、java虚拟机内存区域的作用。