JVM 50个基础知识,值得收藏

1.什么是JVM?

JVM——Java虚拟机,它是Java实现平台无关性的基石。

Java程序运行的时候,编译器将Java文件编译成平台无关的Java字节码文件(.class),接下来对应平台JVM对字节码文件进行解释,翻译成对应平台匹配的机器指令并运行。

image.png

(Java语言编译运行)


同时JVM也是一个跨语言的平台,和语言无关,只和class的文件格式关联,任何语言,只要能翻译成符合规范的字节码文件,都能被JVM运行。

image.png

JVM跨语言

2.能说一下JVM的内存区域吗?

JVM内存区域最粗略的划分可以分为,当然,按照虚拟机规范,可以划分为以下几个区域:

image.png

Java虚拟机运行时数据区

JVM内存分为线程私有区和线程共享区,其中方法区是线程共享区,虚拟机栈本地方法栈程序计数器是线程隔离的数据区。

1、程序计数器

程序计数器(Program Counter Register)也被称为PC寄存器,是一块较小的内存空间。

它可以看作是当前线程所执行的字节码的行号指示器。

2、Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。

Java虚拟机栈描述的是Java方法执行的线程内存模型:方法执行时,JVM会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等。

image.png

Java虚拟机栈


3、本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

Java 虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。


4、Java堆

对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java里“几乎”所有的对象实例都在这里分配内存。


Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”(Garbage Collected Heap,)。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现新生代老年代Eden空间From Survivor空间To Survivor空间等名词,需要注意的是这种划分只是根据垃圾回收机制来进行的划分,不是Java虚拟机规范本身制定的。

image.png

Java 堆内存结构

5.方法区

方法区是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

它特别在Java虚拟机规范对它的约束非常宽松,所以方法区的具体实现历经了许多变迁,例如jdk1.7之前使用永久代作为方法区的实现。

3.说一下JDK1.6、1.7、1.8内存区域的变化?

JDK1.6、1.7/1.8内存区域发生了变化,主要体现在方法区的实现:

  • JDK1.6使用永久代实现方法区:

image.png

JDK 1.6内存区域

  • JDK1.7时发生了一些变化,将字符串常量池、静态变量,存放在堆上

image.png

JDK 1.7内存区域

  • 在JDK1.8时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。

    image.png

4.为什么使用元空间替代永久代作为方法区的实现?

Java虚拟机规范规定的方法区只是换种方式实现。有客观和主观两个原因。

  • 客观上使用永久代来实现方法区的决定的设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会出问题),而且有极少数方法 (例如String::intern())会因永久代的原因而导致不同虚拟机下有不同的表现。

  • 主观上当Oracle收购BEA获得了JRockit的所有权后,准备把JRockit中的优秀功能,譬如Java Mission Control管理工具,移植到HotSpot 虚拟机时,但因为两者对方法区实现的差异而面临诸多困难。考虑到HotSpot未来的发展,在JDK 6的 时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的计划了,到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta-space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

5.对象创建的过程了解吗?

在JVM中对象的创建,我们从一个new指令开始:

  • 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用

  • 检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程

  • 类加载检查通过后,接下来虚拟机将为新生对象分配内存。

  • 内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。

  • 接下来设置对象头,请求头里包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

这个过程大概图示如下:

image.png

对象创建过程

6.什么是指针碰撞?什么是空闲列表?

内存分配有两种方式,指针碰撞(Bump The Pointer)、空闲列表(Free List)。

image.png

图片指针碰撞和空闲列表

  • 指针碰撞:假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
  • 空闲列表:如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”。
  • 两种方式的选择由Java堆是否规整决定,Java堆是否规整是由选择的垃圾收集器是否具有压缩整理能力决定的。

7.JVM 里 new 对象时,堆会发生抢占吗?JVM是怎么设计来保证线程安全的?

会,假设JVM虚拟机上,每一次new 对象时,指针就会向右移动一个对象size的距离,一个线程正在给A对象分配内存,指针还没有来的及修改,另一个为B对象分配内存的线程,又引用了这个指针来分配内存,这就发生了抢占。

有两种可选方案来解决这个问题:

image.png

堆抢占和解决方案

  • 采用CAS分配重试的方式来保证更新操作的原子性

  • 每个线程在Java堆中预先分配一小块内存,也就是本地线程分配缓冲(Thread Local Allocation

    Buffer,TLAB),要分配内存的线程,先在本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。

8.能说一下对象的内存布局吗?

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

image.png

对象的存储布局

对象头主要由两部分组成:

  • 第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为Mark Word,它是个动态的结构,随着对象状态变化。
  • 第二部分是类型指针,指向对象的类元数据类型(即对象代表哪个类)。
  • 此外,如果对象是一个Java数组,那还应该有一块用于记录数组长度的数据

实例数据用来存储对象真正的有效信息,也就是我们在程序代码里所定义的各种类型的字段内容,无论是从父类继承的,还是自己定义的。

对齐填充不是必须的,没有特别含义,仅仅起着占位符的作用。

9.对象怎么访问定位?

Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

  • 如果使用句柄访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图所示:Clipboard Image.png

图片通过句柄访问对象

  • 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图所示: