面试必备基础知识 — JVM

概述

JVM (Java Virtual Machine)Java虚拟机,是一种用于计算机设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。(百度百科)

Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

运行时数据区

对于Java运行时涉及到的存储区域主要包括程序计数器、Java虚拟机栈、本地方法栈、java堆、方法区以及直接内存等等。

程序计数器

记录正在执行的虚拟机字节码指令的地址。(如果正在执行的是本地方法则为空Undefined)。
在Java里面主要是取下一条指令的字节码文件。
(每个线程都是有一个程序计数器的,是线程私有的,相当一个指针)

Java 虚拟机栈

每个 Java 方法在执行的同时会创建一个栈帧 (Stack Frame)用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在Java虚拟机栈 中 入栈 和 出栈的过程。

可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小

java -Xss2M HackTheJava

Java虚拟机栈 区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈与Java虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务

本地方法一般使用其它语言(C、C++或者汇编语言等)编写,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。

(heap)所有对象都在这里分配内存,是垃圾收集的主要区域(“GC堆”)。

垃圾收集器 基本都是采用 分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。

可将堆分成两块:

  • 新生代 (Young Generation)
  • 老年代(Old Generation)

堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出OutOfMemoryError异常

可以通过 -Xms-Xmx 两个虚拟机参数 来指定一个程序的对内存大小,第一个参数设置初始值,第二个参数设置最大值。

java -Xms1M -Xmx2M HackTheJava

方法区

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出OutOfMemoryError 异常。

对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。

运行时常量池

运行时常量池是方法区的一部分。

Class文件中的常量池,用于存放编译器生成的各种字面量符号引用,这部分在类加载后进入方法区的运行时常量池中。

在运行期间,也可以向常量池中添加新的常量(动态生成),如String类的intern()方法。

每个运行时常量池都是从 JVM 的方法区中分配的。

直接内存

JDK 1.4中新引入了 NIO 类,它可以使用 Native函数库直接分配堆外内存(直接内存),然后通过一个存储在堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。
避免了Java 堆内存 和 堆外内存来回拷贝数据的时间,更高效。

垃圾收集

垃圾收集主要是针对方法区 进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有 的 ,只存在于线程的声明周期内,线程结束后就会消失,所以不需要对这三个区域进行垃圾回收。

垃圾收集器在对 堆区 和 方法区进行回收前,首先要确定这些区域的 对象 哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法。

引用计数算法

在这种方法中,堆中的每个对象实例 都有一个 引用计数器 。当对象增加一个引用时计数器 加1 ,引用失效时计数器 减1,引用计数器为0 的对象可被回收。

在两个对象出现 循环引用 的情况下,此时引用计数器永远不为0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

class MyObject{
     MyObject object;
}

public class abc_test {
    public static void main(String[] args) {
        MyObject object1=new MyObject();
        MyObject object2=new MyObject();
        //父对象有一个子对象的引用
        object1.object=object2;
        //子对象反过来引用父对象
        object2.object=object1;
        
        //设置为null 说明二者指向的对象已经不能再被访问
        object1=null;
        object2=null;
        //但彼此互相引用,计数器不为0,导致GC无法回收
    }
}

可达性分析算法

从一个节点 GC Root 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

可达性测试分析算法
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含一下内容:

  • 虚拟机栈 中局部变量表中引用的对象
  • 本地方法栈 中JNI (Native方法)中引用的对象
  • 方法区 中类静态属性引用的对象
  • 方法区 中的常量引用的对象

方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能。

类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的 ClassLoader 已经被回收。
  • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

finalize()

当一个对象可被回收时,如果执行对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,那么后面回收时不会再调用该方法。

引用类型

无论是通过 引用计数算法 判断对象的引用数量,还是通过 可达性分析算法 判断都对象是否可达,判定对象是否可被回收都与 引用 有关。
Java提供了四种强度不同的引用类型:强引用软引用弱引用虚引用

强引用

被强引用关联的对象不会被回收,(也就是强引用只要还存在,垃圾收集器永远不会回收掉被引用的对象)。

使用 new 一个对象的方式 创建 强引用

Object obj = new Object();

软引用

被软引用关联的对象只有在内存不够的情况下才会被回收。

使用 SoftReference 类来创建软引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使对象只被软引用关联

弱引用

被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收之前。

使用 WeakReference 类来创建弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

虚引用

又称为 幽灵引用 或 幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。

为一个对象设置虚引用的 唯一目的 是能在这个对象被回收时,收到一个系统通知。

使用 PhantomReference 来创建虚引用:

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;

垃圾收集算法

标记 - 清除

标记 - 整理

复制

现在的商业虚拟机都采用 复制收集算法 回收 新生代 ,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间 和 两块较小的 Survivor 空间,每次使用 Eden 和 其中一块 Survivor 。

在回收时,将Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。

HotSpot 虚拟机 的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。

分代收集

现在的商业虚拟机采用 分代收集算法 ,他根据对象存活周期 将内存或分为几块,不同块采用适当的收集算法。

一般将 分为 新生代老年代

  • 新生代: 复制算法
  • 老年代:标记 - 清除 或者 标记 - 整理 算法

CYC

内存分配策略与回收策略

Minor GC 和 Full GC

  • Minor GC :回收 新生代,因为 新生代对象 存活的时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC :回收 老年代新生代 ,老年代对象存活的时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。

内存分配策略

  1. 对象 优先在 Eden 上分配内存:大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
  2. 大对象 直接进入老年代:大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
  3. 长期存活的对象 进入老年代

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而Full GC 相对复杂,有以下条件:

  • 调用 System.gc() :只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
  • 老年代空间不足
  • 空间分配担保失败
  • 永久代空间不足

CYC Java虚拟机
JVM的垃圾回收机制

类加载机制

类是运行期间第一次使用时动态加载的,而不是一次性加载所有的类。如果一次性加载的话,那么会占用很多的内存。

类的生命周期

类的生命周期 包括7个阶段:
加载、验证、准备、解析、初始化、使用 和 卸载。
其中,解析和初始化交换顺序,可实现 Java的动态绑定。

类的加载过程

包含了 加载链接初始化阶段。

  • 加载 :将 class文件读入到内存,并为之创建一个 Class 对象
  • 链接 :链接阶段又包括 验证准备解析 三个阶段
    • 验证 : 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    • 准备:类变量 是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
    • 解析:将常量池中的 符号引用 替换为 直接引用 的过程。解析过程在某些情况下,可以在初始化阶段之后再开始,这是为了支持Java 的动态绑定。
  • 初始化:初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始化值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

类初始化的时机

  • 创建类的实例
  • 访问类的静态变量、或者为静态变量赋值
  • 调用类的静态方法
  • 使用反射方式来强制创建某个类或接口的java.lang.Class对象
  • 初始化某个类的子类
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值