Java教程

Java时间差计算,HR的话扎心了

本文主要是介绍Java时间差计算,HR的话扎心了,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
## 一、Java 虚拟机架构 (JVM Architecture) 在我看来,不管学习什么样的知识或技术,首先要做的就是从全局上去认识它,这样才能避免盲人摸象,事倍功半的情况发生。既然要学习 JVM,就要先了解它的整体架构,于是我画了个 JVM 架构图来帮助大家认识它。 ![](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307200621881.jpg) > Java 虚拟机架构图 对 JVM 还不太了解的同学第一次看到这张花里胡哨的图肯定会一脸懵逼,不用怕,其实我们只需要重点理解并掌握其中一部分 (同时也是面试重点) 就好了,比如运行时数据区、垃圾收集器、内存分配策略和类加载机制等,类文件结构也可以学习一下,其他的稍作了解即可。既然本篇文章是要带领大家认识 JVM 架构的,那就先把图中各个部分都介绍一下吧 (注:本文只做介绍,让各位先对 JVM 有个整体的认识,后续会做深入探讨)。 ### 1.1 Class 文件 (字节码文件) Java 之所以号称“一次编写,处处运行”,就是得益于虚拟机和 Class 文件 (注:CLass 文件、字节码文件和类文件是一个意思) 的组合机制。程序员并不需要自己去适配不同的操作系统,大家都知道我们平时编写的 java 代码在编译成 Class 文件后才能执行,而 Class 文件可以在任何操作系统上的 JVM 上执行,这样就做到了“平台无关性”。下面是一个最简单的 HelloWorld 程序及其对应的 Class 文件。 ![](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307200443880.jpg) > HelloWorld 程序及其编译后的 Class 文件 得益于 Class 文件,JVM 还可以做到“语言无关性”,也就是说不只有 Java 程序可以运行于 JVM 之上,很多其他语言例如最近在安卓开发者中大火的 Kotlin 语言,还有 Scala、Groovy 等语言也都是基于 JVM 平台的,这些语言的代码都可以编译成 Class 文件,然后在 JVM 上运行。 ![](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307201829480.jpg) > JVM提供的平台无关性和语言无关性 ### 1.2 类加载器子系统 (ClassLoader Subsystem) 要执行 Class 文件就需要先将其加载进内存,这一工作正是由类加载器 (ClassLoader) 完成的,系统为我们提供了三种类加载器,分别是启动类加载器 (Bootstrap ClassLoader)、扩展类加载器 (Extension ClassLoader) 和应用程序类加载器 (Application ClassLoader),如果有必要,我们也可以加入自定义的类加载器。类加载过程如下: ![](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307201437453.jpg) > 类加载过程 类加载过程分为加载、连接和初始化三个阶段,其中的连接阶段又分为验证、准备和解析三个阶段 (详细的类加载机制在后续文章中进行介绍)。 ### 1.3 Java 虚拟机运行时数据区 (JVM Runtime Data Area) 这部分内容较多,放在本文第二部分单独进行介绍。 ### 1.4 执行引擎 (Execution Engine) 字节码被加载进运行时数据区后,执行引擎会进行读取并执行,执行引擎主要包含以下模块: * 解释器 (Interpreter):相信大家很久以前就听过“计算机只认识0和1”这句话,时至今日,计算机依然只认识0和1,所以任何编程语言的代码最终都要转化成机器码 (二进制代码)才能执行,Java 也不例外,而解释器的工作正是将编译得到的字节码再转化成机器码,然后才能执行。正因为如此,Java 才被称为解释型语言,也正是因为边解释边执行的特点,Java 程序在执行时才会慢于 C++ 之类的编译型语言。 * 即时编译器 ,为了弥补解释执行带来的速度劣势,JVM 引入了即时编译器,它的作用就是把热点代码,比如重复调用的方法和循环代码等,编译成机器码并存放在 code cache 中,这样之后再用到这些代码就不用重新解释执行了,可以提高程序运行效率。 * 垃圾收集器 (Garbage Collector):Java 程序员可以不用手动释放内存,全是垃圾收集器的功劳,这也是 JVM 中尤其重要的内容,后续会有多篇文章对其进行介绍。 ### 1.5 本地库接口 (JNI,Java Native Interface) 如果你经常看 JDK 源码的话,一定会注意到 native 这个关键词,被它修饰的方法是没有方法体的,是因为它调用了计算机本地的方法库 (通常是 C 或 C++ 代码)。JDK 源码中有很多类的方法,特别是一些需要操作计算机硬件的方法,都调用了本地方法库,毕竟与硬件打交道还是用 C 和 C++ 更方便,比如下面这些方法: ``` // 例一:这是 Thread 类中的 currentThread 方法,用于获取当前正在执行的线程 public static native Thread currentThread(); // 例二:这是 FileInputStream 类中 open0 方法,用于打开指定文件 private native void open0(String name) throws FileNotFoundException; ``` ### 1.6 本地方法库 (Native Method Library) 本地库接口所调用的对象正是位于这个库中,一般是位于计算机本地的 C 或 C++ 语言代码。 ## 二、Java 虚拟机运行时数据区 Java 虚拟机运行时数据区是我们需要重点了解并熟悉的部分,因为这与我们写的程序息息相关,平时常见的 StackOverflowError 和 OutOfMemoryError 也几乎都是来自这个区域。说“几乎”是因为当本机直接内存不够用时也会抛出 OutOfMemoryError。如下图所示,程序计数器、Java 虚拟机栈和本地方法栈是线程私有的,堆和方法区是线程共享的,其中方法区又包含了运行时常量池。下面就对这个部分做个详细的介绍吧 (注:本部分引用内容来自《深入理解Java虚拟机》)。 ![](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307202250840.jpg) > Java 虚拟机运行时数据区 ### 2.1 程序计数器 (Program Counter Register) 怕有些小伙伴不清楚,提示一下:下面这样的段落格式就是 Markdown 里的引用格式,,一般用于引用他人的文章或别处的内容。 > 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念里,字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 > > 由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 > > 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地 (Native) 方法,这个计数器值则应为空 (Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。 这里引用了《深入理解Java虚拟机》书中的内容,其实不难理解,程序计数器的作用就是保存线程的执行状态,引用部分的第三段中说“*如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址*”,这个地址就是字节码执行到的位置。我们平时说的 Java 多线程上下文切换就需要程序计数器的辅助,当 CPU 从一个线程切换到另一个线程时,要从程序计数器中读取线程执行状态从而恢复现场。后面又说“*如果执行的是本地 (Native)方法,这个计数器值为空(Undefined)*”,这是为何呢?是因为本地方法执行的是 C / C++ 代码,在原生平台直接运行,也就不存在 Java 虚拟机的概念,自然也无法保存字节码指令地址,此时要想记录代码运行状态的话,只能使用原生 CPU 的 PC 寄存器。 ### 2.2 Java 虚拟机栈 (JVM Stacks) > 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 > > 局部变量表存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double)、对象引用 (reference 类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置) 和 returnAddress 类型(指向了一条字节码指令的地址)。 > > 这些数据类型在局部变量表中的存储空间以局部变量槽 (Slot) 来表示,其中64位长度的 long 和 double 类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。请读者注意,这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间 (譬如按照1个变量槽占用32个比特、64个比特,或者更多)来实现一个变量槽,这是完全由具体的虚拟机实现自行决定的事情。 > > 在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常。 Java 虚拟机栈的内部结构如下图所示: ![](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307202632628.jpg) > Java 虚拟机栈 #### 2.2.1 局部变量表 局部变量表是存放方法参数和局部变量的区域。 局部变量没有准备阶段, 必须显式初始化。如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。 #### 2.2.2 操作数栈 操作数栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。下面使用 i++ 和 ++i 的区别来帮助理解操作数栈: **i++ 和 ++i 的区别:** 1. i++:从局部变量表取出 i 并压入操作栈,然后对局部变量表中的 i 自增 1,将操作栈栈顶值取出使用,最后,使用栈顶值更新局部变量表,如此线程从操作栈读到的是自增之前的值。 2. ++i:先对局部变量表的 i 自增 1,然后取出并压入操作栈,再将操作栈栈顶值取出使用,最后,使用栈顶值更新局部变量表,线程从操作栈读到的是自增之后的值。 之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。 #### 2.2.3 动态连接 每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。 #### 2.2.4 方法出口 方法执行时有两种退出情况: 1. 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等; 2. 异常退出。 无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式: 1. 返回值压入上层调用栈帧。 2. 异常信息抛给能够处理的栈帧。 3. 程序计数器指向方法调用后的下一条指令。 ### 2.3 本地方法栈 (Native Method Stacks) > 本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地 (Native) 方法服务。 > > 《Java虚拟机规范》对本地方法栈中方法使用的语言、使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机 (譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失 败时分别抛出 StackOverflowError 和OutOfMemoryError 异常。 这部分比较好理解,就不做解析了。 ### 2.4 Java 堆 (Heap) > 对于Java应用程序来说,Java 堆 (Java Heap)是虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 世界里“几乎”所有的对象实例都在这里分配内存。Java 堆是垃圾收集器管理的内存区域,因此也常被称为“GC 堆”。 > > 根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大 对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。 > > Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,Java 虚拟机将会抛出 OutOfMemoryError 异常。 Java 堆的唯一作用就是存放对象实例,这也是垃圾收集器最关注的内存区域,因为大多数对象实例的存活时间都很短,比如在方法内部创建的实例在方法执行完之后就没有存在价值了,所以这个区域的垃圾回收性价比最高。关于垃圾回收的详细内容,见后续文章。 ### 2.5 方法区 (Method Area) > 方法区 (Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与 Java 堆区分开来。 > > 说到方法区,不得不提一下“永久代”这个概念,尤其是在JDK 8以前,许多 Java 程序员都习惯在 HotSpot 虚拟机上开发、部署程序,很多人都更愿意把方法区称呼为“永久代”(Permanent Generation),或将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的 HotSpot 虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得 HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。但是对于其他虚拟机实现,譬如 BEA JRockit、IBM J9 等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java虚拟机规范》管束,并不要求统一。但现在回头来看,当年使用永久代来实现方法区的决定并不是一个好主意,这种设计导致了 Java 应用更容易遇到 内存溢出的问题(永久代有-XX:M axPermSize 的上限,即使不设置也有默认大小,而 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 一样在本地内存中实现的元空间(Metaspace)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。 > > 《Java虚拟机规范》对方法区的约束是非常宽松的,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,甚至还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域的确是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收有时又确实是必要的。 > > 根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。 这部分引用内容对方法区的介绍十分全面,切记不要将方法区和永久代混为一谈,从JDK 8 以后已经没有永久代的概念了。 ### 2.6 运行时常量池 (Runtime Constant Pool) > 运行时常量池 (Runtime Constant Pool) 是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表 (Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。 > > 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存 时会抛出OutOfMemoryError异常。 常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。 # 分享 这次面试我也做了一些总结,确实还有很多要学的东西。相关面试题也做了整理,可以分享给大家,了解一下面试真题,想进大厂的或者想跳槽的小伙伴不妨好好利用时间来学习。学习的脚步一定不能停止! **[需要这份资料的朋友戳这里免费下载](https://docs.qq.com/doc/DSmxTbFJ1cmN1R2dB),整理出的内容大概如下:** ![薪酬缩水,“裸辞”奋战25天三面美团,交叉面却被吊打,我太难了](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307202359399.jpg) Spring Cloud实战 ![薪酬缩水,“裸辞”奋战25天三面美团,交叉面却被吊打,我太难了](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307203968865.jpg) Spring Boot实战 ![薪酬缩水,“裸辞”奋战25天三面美团,交叉面却被吊打,我太难了](http://www.www.zyiz.net/i/li/?n=2&i=images/20210703/1625307203641150.jpg) 面试题整理(性能优化+微服务+并发编程+开源框架+分布式)
这篇关于Java时间差计算,HR的话扎心了的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!