面试笔记:JVM

常量池

直接内存(堆外内存)

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据

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

数组类和非数组类在类加载过程有什么不同?

非数组类是通过类加载器来加载到内存中,而数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构建出来的。不过,数组的元素类型最终还是靠类加载器来完成加载的。

简述一下Java虚拟机的类加载机制

加载:
通过类的全限定名获取定义此类的二进制字节流(Class文件),将字节流代表的静态存储结构转换为方法区的运行时数据结构,最后在内存中生成一个代表这个类的对象,作为方法区数据的访问入口。
加载完成后,二进制字节流的数据就按虚拟机规定的格式存储在方法区中了,同时会在堆内存中实例化该类的对象,作为程序访问方法区中数据的外部接口
验证:
验证是为了保证Class文件的字节流中的信息是符合《Java虚拟机规范》的,不会危害虚拟机安全。会进行:文件格式验证,元数据验证,字节码验证(主要对方法体进行校验)和符号引用验证。
准备:
为变量(静态变量分配内存设置初始值。JDK7及之前,HotSpot用永久代实现方法区,所以静态变量存放在方法区。而JDK8之后,类变量(即静态变量)会随Class对象一起存放在Java堆中
注意:这里的变量仅仅指类变量,实例变量会在对象实例化时再随对象一起分配到Java堆中。另外,这里的初始值通常时数据的零值,而不是代码中赋予的初始值,代码赋予的初始值会等到初始化阶段才会进行。
解析:
将常量池中的符号引用替换为直接引用。主要针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用。
初始化:
类加载的最后一个步骤。Java虚拟机真正开始执行类中编写的Java代码。此时会执行类构造器<clinit>()方法,给类中的所有类变量赋值,还有静态语句块的运行。
多线程环境下进行类初始化的话可能会引起死锁
初始化是线程安全的。

  • 两个类初始化互相依赖,一个线程调用A的方法,开始初始化A,获得A的class锁,另一个线程调用B的方法,开始初始化B,获得B的class锁。此时,A的初始化中有静态代码块new B()来获得B的对象,但发现B还没初始化,尝试获取B类的锁,而B中也有静态代码块new A(),也需要获取A的锁来初始化A。这时,就会陷入死锁,因为A和B都需要对方的对象,都需要对方先初始化。这优点像spring中bean的循环依赖问题。
  • 父类中静态变量new子类,会导致死锁。

不同的类加载器对类的加载有什么影响?

对于任意一个类,必须由类及其类加载器来确定其唯一性。也就是说,来源同一个Class文件的两个类,如果类加载器不同,其也不会相等。这里的相等包括类的equals方法,isInstance()方法等。

双亲委派模型的好处?

Java中的类随着类加载器一起具备了一种带优先级的层次关系。即,对于一个类(如java.lang.Object),无论哪个加载器要加载这个类,最终都会委托给模型最顶端的加载器尝试进行加载,因此该类在各种类加载器中都能保证是同一个类

如何破坏双亲委派模型?

首先,双亲委派模型并不是强制性约束,有少部分类加载器可能并不遵循这一约束。

  • JDK1.2前,也就是双亲委派模型出现前,已经存在用户自定义的类加载器代码可能会覆盖loadClass()方法,最后不得不添加一个findClass(),让类可以调用它自己完成加载。
  • 基础类回调用户代码。通过java.lang.Thread类的setContextClassLoader()方法加载用户代码。
  • 为了热部署。例如OSGi用自定义类加载器实现模块化热部署,此时类加载器不再是双亲委派模型的树状结构,而是更复杂的网状架构。

HotSpot虚拟机对象的创建过程?(注意区分类加载)

对象的创建

下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
Java创建对象的过程

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式“指针碰撞”“空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

内存分配的两种方式

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

3.2 对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

Hotspot 虚拟机的对象头包括两部分信息第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

3.3 对象的访问定位

建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有①使用句柄②直接指针两种:

  1. 句柄: 如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

对象的访问定位-使用句柄

  1. 直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

对象的访问定位-直接指针

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值