前言
- 大家好,这里是IT学习日记,相信大家对今年IT的行情应该也有所了解了,从大厂到小厂,各种裁员消息。公司裁员我们无法决定,我们能做的就是不断提升自己,提前准备。
- 本系列文章主要分享了之前博主真实面试中遇到的一些问题,希望能够帮助准备就业或者跳槽的朋友。
起因
- 面试官: 嗯、谈谈你对JVM、JRE、JDK的认识吧。
- 我: (心里想)小样,就这问题,想难倒资深CURD工程师,还好我早有准备。回答道:JVM全称JAVA虚拟机,它可用于加载JAVA字节码文件,可以看做是JAVA的一个执行环境,JAVA跨平台特性也是因为有JVM。
- 面试官: 说完了吗、就这?
- 我: 一听面试官这语气,心里直冒火,钱是小事,面子可不能丢,敢看不起一个多年经验的CURD工程师,这面试不给你找回来,刚要继续回答!
- 面试官: 摆了摆手,有气无力道,好吧,今天先这样,后面有消息的话人事会通知你的。
- 我: 一听面试官这语气,我更生气了,便说道,三十年河东、三十年河西、莫欺少年穷!说完就站了起来,准备走出面试室。
- 面试官: 脸一抽,嘲笑说道,你以为你是萧炎?我TM还是萧站呢!
- 我: 我一听他这回答、这语气!摆明了是占我便宜啊,体内的洪荒之力直接控制不住,抡起桌面的水瓶就往面试官脸上招呼过去!
- 我: 正在我打的正爽,正在面试官连连向我求饶的时候,脚一抽,我突然醒了,发现自己躺在床上,时间是凌晨2点。原来是做梦啊!早知道就打狠一点了,我心里暗暗说道!
- 我: 此刻的我一点睡意都没有了,回想起在梦中看到的面试官嘴脸,我在心里暗暗发誓,我绝对不允许梦里的场景在现实重现,在面试官装B之前,我就要比他先装!
- 我: 从床上爬了起来,打开电脑,在界面输入: JVM、JRE、JDK有什么区别!
什么是JVM
(一): JVM
在认识JVM是什么之前,先回想一下平日里,如果我们想要在电脑上使用一个应用,应该具备什么条件?
首先,要有个环境(电脑就是),其次需要将这个应用安装到电脑上,然后,我们才能够使用应用来做我们想做的事情。
JVM也是类似,JVM全称Java Virtul Machine(JAVA虚拟机),它是一种计算设备的规范,可以理解成是一个虚构的计算机,它里面包含了和真实计算机相似的组件,用于模仿计算机的各种功能,然后运行在真实的计算机中
(二): JAVA为什么说是“平台无关的编程语言”
因为它包含了JAVA虚拟机(JVM),JVM知道底层硬件平台的指令长度和其他相关特性,它屏蔽了和具体平台的相关信息,使得JAVA语言开发的程序只需要一次编译生成JVM上能够运行的目标代码(字节码文件),从而可以在不同平台上运行时而不需要进行重新编译
(三): JVM内部结构
注:通常情况下我们使用的JDK是由Sun JDK和OpenJDK提供的,这个是应用最广泛的版本。而这个版本的虚拟机就是Hotspot(热点数据),所以通常情况下,我们讲的JAVA虚拟机默认就是指Hotspot。
JVM的内部可以具体区分为三大部分即: 类加载器、执行引擎、运行时数据区。
一:类加载器(Class Loader)
它的作用主要是将类的字节码文件(class文件)从磁盘加载到内存、然后对class文件进行数据校验、转换解析、初始化等操作,最终转成可以被虚拟机直接使用的Class对象。
二:执行引擎(Execution Engine)
JVM中的最核心组件之一,主要用于执行指令,用于将加载到JVM中的字节码文件解释或者编译成对应平台上的本地机器指令,简单来说,它充当着将高级语言翻译成机器语言的中间者
并且不同的JVM内部实现也不一样,执行引擎在执行JAVA代码的时候可能是解释执行(解释器执行)和编译执行(即时编译器产生本地代码执行),也可能两者都有。
三:什么是解释器(Interpreter),什么是JIT编译器?
(一) 解释器:
根据预定义的规范负责将加载到JVM中的字节码进行逐行解释的方式执行,将每条字节码翻译成对应平台的本地机器指令执行,每调用一次就需要编译一次。
(二) JIT(Just In Time Compiler)即时编译器:
它是一种提高程序运行效率的方法,它负责将虚拟机中的源代码直接编译成和本地机器平台相关的机器语言,然后将热点代码的机器码保存起来,后面再调用时直接执行这些机器码即可。
Hotspot(热点数据)虚拟机命名的由来,当虚拟机发现某个方法或者代码块运行的特别频繁时,就会将这些代码认定为"热点代码",为了提高热点代码的执行效率,在运行时,虚拟机会将这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,下次再调用时就可以直接执行机器码即可,JIT编译器就是用来完成这个任务的。
(三)特点:
1、解释器执行步骤可以抽象理解为:
字节码文件 -> 解释器进行解释执行 ->得到执行结果
2、JIT可以抽象理解为:
字节码文件 -> 编译器进行编译 -> 执行编译后的代码 -> 得到执行结果
JIT比解释器快,并不是指"编译"的动作比"解释"的动作快,而是指"执行编译后的代码"会比"使用解释器解释执行"的动作要更快。
如果是只执行一次的代码(只调用一次且没有循环)如:类的构造器,使用解释器的效率要比JIT编译执行更快,只有是频繁执行的热点代码,使用JIT方式才能够有更好的效率
四:堆(Heap)
线程共享、JVM虚拟机启动时创建、是占用JVM内存最大的一块区域,主要是用来存放对象,因为是线程共享,所以、存在此处的数据可能存在线程安全问题。
1、堆的内部结构划分
新生代(Young)和老年代(Old),它们的大小比例是:1:2 两部分。而新生代又划分为三个区域: Eden区、From Survivor区、To Survivor区、它们的大小比例是:8:1:1,具体模型如下:
2、为什么要将堆划分成这种结构
给堆进行分代的目的是为了提高内存使用率和垃圾回收的效率。 因为堆是占JVM中最大空间一块区域,所有的对象实例都存在这个地方,同时它也是垃圾回收最频繁的区域,如果没有对它进行分代管理,新创建的对象和生命周期很长的对象统一管理,则当程序需要进行垃圾回收的时候,每次回收都要对所有对象进行判断,这个判断是非常花费时间的,会严重影响GC的效率
有了分代管理机制,新创建的对象存在新生代,经过多次回收仍然还存活的对象则放入老年代(默认是15次),因为新生代的对象都是"朝生暮死",生命周期比较短,所以需要频繁进行GC,而老年代生命周期长,回收的频率会比新生代低,这样就避免了每次回收的时候还要去判断所有对象的状态,提高了GC的效率。
一个很简单的例子,其实跟我们平常生活中的年轻人和老年人参加的活动相似,老年人比较乐忠于广场舞、超市打折活动,年轻人比较乐忠于新科技、游戏活动,参加一个活动的时候,是有针对不同的用户群体的,你不可能有广场舞这种活动还一直拉着年轻人去参加吧,同理的,你也不可能觉老年人整天去玩游戏一个道理,分代管理可以针对不一样的场景用不同的管理方式。
3、 新生代(Young Generation)
新生成的对象优先存放在新生代中,新生代中的对象有着“朝生夕死”的特点,存活率很低。所以、在新生代中,每次进行GC回收都可以回收很大的空间、回收效率很高。
在上面的堆结构图中可以看到,Hotspot虚拟机将新生代划分成了三块,一块占据最大空间的Eden(伊甸)区和两块占据较小空间的Survisor(幸存者)区,默认的比例是:8:1:1。
将新生代划分为三区域的目的是因为在Hotspot中是采用复制算法来回收新生代的垃圾对象的(具体的垃圾算法会在下篇讲解,感兴趣的可以持续关注),使用这个默认的比例可以更加充分的利用内存空间,减少浪费,所以这个比例大小一般不要去变动。
特点: 新创建的对象默认是分配在Eden区(但是大对象除外,大对象是直接存放入老年代区域),如果Eden区没有足够的空间分配给对象时,则JVM会触发一起Minor GC来进行回收垃圾对象,释放空间。
4、 新生代GC回收的流程:
首先,在GC前,新生代中To Survivor区是空的(用于保留存活对象),对象只存在Eden区和From Survivor区。
开始GC时,JVM会将GC中存活的对象都复制到To Survivor区中,From Survivor区中的对象会根据存活年龄有不同的去向,如果年龄达到了阈值(Hotspot JVM默认是15,对象每经过一轮垃圾回收存活则年龄值加1,并且这个值是存放有对象头中)则会将这个对象放入老年代,如果没有达到年龄阈值则将该对象复制到To Survivor区。
完成复制操作后,GC线程会清空Eden区和From Survivor区,所有的存活对象都存放在To Survivor区。紧接着,From Survivor区和To Survivor区会交换指针,To Survivor则指向了From Survivor区,From Survivor区指向了To Survivor区既一轮GC后,无论如何To Survivor都是空的,它只是作为GC中的一个中间者。如果在一次GC中,To Survivor的控件无法存放新生代中存活的所有对象时,需要进行分配担保,将这个对象存放在老年代。
5、 老年代(Old Generation)
存放在老年代的对象有以下的情况:
1、在新生代中经历了多次GC,年龄值达到了JVM中的阈值(Hotspot默认是15)仍存活的对象
2、大对象既指需要比较大的连续控件的JAVA对象,如很长的数组对象和字符串对象,具体可以通过JVM指定-XX:PretenureSizeThreshold参数来设置大对象的大小,单位为字节(byte)
3、Minor GC后存活的对象占用空间的大小超过了To Survivor区的大小时,会将超出的对象进行老年代分配担保放入老年代中。
4、如果在Survivor区中,某一个年龄的对象总大小超过了Survivor区大小的50%,则会将这个大于或者等于这个年龄的对象全部转移到老年代。
5、在每次执行Minor GC前,JVM会进行检查老年代当前剩余的空间是否大于年轻代所有对象的总大小,目的是为了防止年轻代的对象在GC后全部满足进入老年代的条件,然后进行转移将老年代撑爆。只要老年代的连续空间大于年轻代对象的总大小或者小于历次晋升到老年代对象的年龄平均值则进行Minor GC否则进行Full GC。
6、 堆垃圾回收方式
1、Minor GC(YGC): 它主要是用来对新生代进行垃圾回收的方式,使用的复制算法,因为新生代的对象大多数都是"朝生暮死",生命周期很短的特点,所以GC的频率也会比较频繁,但是回收速度很快。
2、Major GC(YGC): 它是主要用于对老年代对象的垃圾回收方式,老年代的对象生命周期都是比较长的,所以对象不会轻易灭亡,Major GC的频率不会想Minor GC那么频繁,况且一次Full GC会比Minor GC需要花费更多的时间、消耗更大,通常出现一次Major GC一般也会出现一次Minor GC(但不绝对)。
3、Full GC(): Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC,但是它并不等于Major GC Minor GC,具体是要看使用什么垃圾收集器组合。一次Full GC 需要花费更多的时间、消耗更大,所以要尽减少Full GC的次数。
特点比较: Minor GC使用复制算法,需要一块空的内存空间,所以空间使用效率不高,但是它不会出现空间碎片的问题。而Full GC一般是采用标记-清除算法,容易产生空间碎片,如果再有对象需要请求连续的空间而无法提供时,会提前触发垃圾回收,所以它适合存活对象较多的场景使用也就是老年代的垃圾回收。
7、 堆垃圾回收算法
因为垃圾回收算法的细节比较多,所以专门使用一篇文章进行讲解,感兴趣的可以持续关注!
五:方法区(Method Area)
也称为非堆(No-Heap)、是线程共享的一块内存区域、主要是存放类加载后的字节码文件、class、method、field等元数据、静态常量、变量以及JIT编译器编译的机器指令等数据,除此之外,还包括“运行时常量池”。
1、方法区的实现(Method Area)
方法区是一个规范,可以理解成JAVA语言中的接口,它可以有不同的实现方式,常见的实现方式有: 永久代和元空间
永久代(Permanent Generation): 在JDK1.7及以前,它是方法区的实现,但是永久代的概念只有Hotspot虚拟机有,其他的虚拟机如J9、JRockit虚拟机就没有。
元空间(MetaSpace): Hotspot虚拟机(也就是我们平常使用的Oracle的虚拟机)在JDK1.8版本移除了永久代,使用元空间替代了它,元空间占用的是系统内存,换个说法,只要系统的内存空间还充足,方法区就会存在足够的空间,但是,并不意味着我们不需要对元空间的大小做限制,因为它是占系统内存,如果无限大,不仅会影响系统其它应用的使用,严重的可能会导致系统崩溃。
2、JDK1.7时字符串常量池和静态变量存储在哪里
JDK1.7时永久代的部分数据已经从Java的永久代中转移到了堆中,如:符号引用、字符串常量池
3、为什么Hotspot在JDK1.8要用元空间替换永久代
1、避免OOM:
因为方法区主要是存储类的相关信息(包括类的字节码文件),虽然永久代可以使用PerSize和MaxPerSize等参数设置永久代的空间大小,但随着ASM、Cglib等动态生成字节码技术的出现可以修改对象字节码信息后,无法控制类信息的大小,很容易导致永久代内存溢出的问题。
JDK1.8使用了元空间替换永久代,因为元空间是使用系统内存,由系统的实际可用空间来控制,在一定程度上可以避免OOM的出现,但是,也需要通过指定MaxMetaspaceSize等参数来控制大小。
2、提高GC性能:
永久代的垃圾收集是和老年代捆绑在一起的,所以无论两者谁满了,都会触发永久代和老年代的垃圾收集。
使用元空间替换后,简化了Full GC,减少了GC的时间(因为GC时不需要再扫描永久代中的数据),提高了GC的性能。在元空间中,只有少量指针指向堆如类的元数据中指向class对象的指针。
3、Hotspot和JRockit合并:
这个是官方给出的原因,永久代只是Hotspot虚拟机中存在的概念,JRockit中并没有这个说法,JDK8需要整合Hotspot和JRockit,所以废弃了永久代,引入了元空间。
4、认识元空间
(一): 特点
1、保证了类和相关元数据的生命周期和类加载器保持一致。
2、加载器都有自己专门的存储空间
3、节省了GC扫描和压缩的时间
4、对象在元空间的位置是固定的
5、不会单独回收某个类,如果GC发现某个类加载器不再存活了,则会将相关的空间整个都回收掉
(二): 存在的问题
1、元空间采用组块分配的方式,具体的区块大小是由类加载器决定的,但是因为类的信息并不是固定大小的,可能会导致非配的空间区块和类需要的区块大小不一致,这种情况会导致碎片的出现。
2、元空间虚拟机不支持压缩操作.
六:程序计数器(Program Counter Register)
线程私有的,是占据着一块小的内存空间,作用是读取下一条需要执行的字节码指令。它也是JVM规范中规定没有OutOfMemoryError出现的区域。
它被设计出来的目的,是为了让多线程情况下的JAVA程序每个线程都能够正常的工作,每个线程都有自己的程序计数器,用于保存线程的执行情况,这样在进行线程切换的时候就可以在上次执行的基础上继续执行了。
简单的举例: 当你正在用手机看着不可告人的小视频,就在紧张刺激的时候,突然老王给你来个电话,此时的你被打断了,臭骂老王一顿后,你肯定想接着看刚刚没看完的进度而不是从头看起,此刻,它是怎么知道你刚刚看到哪里了,程序计数器就起了作用,它负责管理进度,可以让你在线程切换后还能知道之前的执行位置(刺激吧)。
七:栈(Stack)
线程私有的,生命周期和线程一样,在JAVA语言中,它是用来描述方法的执行内存模型。
在一个方法开始执行前会创建一个栈帧(Stack Frame)用于存储方法中的局部变量表、动态链接、方法出口等信息。一个方法的开始执行和执行结束对应着一个栈帧在虚拟机的入栈和出栈的过程。
八:本地方法栈(Native Method Stack)
PS: 本地方法(Native Mehtod)是外部提供给JAVA语言调用的一个接口,一般是使用C或者C 实现。
线程私有,和栈相类似,区别是栈执行的是JAVA语言编写的方法,而本地方法栈执行的是调用Native接口提供的方法既JAVA调用非JAVA实现的接口。
九: JVM和JMM的关系
在平常代码开发或者面试中,经常会听到JVM(JAVA虚拟机)和JMM(JAVA内存模型),我们总是容易混淆它们之前的关系,下面就来详细解释下他们的区别。
JMM(JAVA Memory Model)即JAVA内存模型,在JSR133中指出JMM是用来定义一个 一致的、跨平台 的内存模型,它并不像JVM内存结构一样是真实存在的,是一个缓存一致性协议,屏蔽了各种硬件和操作系统的访问差异的,用于定义数据读写的规则。
一:内存可见性
在JMM中,我们讲多个线程通信的共享内存称为主内存,在并发编程中的应用每个线程都维护着一个自己的工作内存,工作内存中保存的数据是主内存数据的副本,JMM则用于控制本地内存和主内存之间的数据数据交互。
通过上面的图片和分析可知,JMM内存模型和JVM本质上是没有关联的,但是,如果要强行关联的话,主内存实际上对应的是JVM中堆中的对象实例数据部分,工作内存则对应的是JVM中的栈部分。
什么是JRE
JAVA运行时环境(Java Runtime Enviroment)可以将它看做是一个容器, JVM 是它的内容。
JRE = JVM Java Packages Classes(like util, math, lang, awt,swing etc) runtime libraries。
什么是JDK
Java Development Kit 用作开发, 包含了JRE, 编译器和其他的工具(比如: JavaDoc,Java调试器), 可以让开发者开发、编译、执行Java应用程序。
小结
不积跬步,无以至千里;不积小流,无以成江海。今天播种努力的种子,总会有一天发芽!