面试准备系列——Java基础技术篇(8)/java平台与JVM

1.(重点)为什么说java是平台无关(独立)性的语言

平台独立性是指可以在一个平台上编写和编译程序,而在其他平台上运行。保证Java具有平台独立性的机制为“中间码”和“Java虚拟机( Java Virtual Machine, JVM)”。Java程序被编译后不是生成能在硬件平台.上可执行的代码,而是生成了一个“中间码”。不同的硬件平台,上会安装有不同的JVM,由JVM来负责把“中间码”t翻译成硬件平台能执行的代码。由此可以看出JVM不具有平台独立性,而是与硬件平台相关的。

解释执行过程分三步进行:代码的装入、代码的校验和代码的执行。装入代码的工作由“类装载器”完成。被装入的代码由字节码校验器进行检查。

Java字节码的执行也分为两种方式:即时编译方式与解释执行方式,即时编译方式指的是解释器先将字节码编译成机器码,然后再执行该机器码。解释执行方式指的是解释器通过每次解释并执行一小段代码来完成Java字节码程序的所有操作。通常采用的是解释执行方式。而在C/C ++语言中,编译后的代码只能在特定的硬件上执行,换个硬件平台这些代码就无法执行了,从而也导致了C/C ++没有跨平台的特性。但C/C ++有更高的执行效率。

常见题型:
在这里插入图片描述
在这里插入图片描述

2.Java平台与其他语言平台有哪些区别

Java平台是一个纯软件的平台,这个平台可以运行在一- 些基于硬件的平台( 例如Linux、Windows等)之上。Java平台主要包含两个模块: JVM与Java API ( Application Program Interface,应用程序接口)。

JVM是一个虚构出来的计算机,用来把Java编译生成的中间代码转换为机器可以识别的编码并运行。它有自己完善的硬件架构,例如处理器、堆栈、寄存器等,还具有相应的指令系统,它屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在JVM上运行的目标代码(即字节码),就可以在多种平台上不加修改地顺利运行。每当一个Java程序运行时,都会有一个对应的JVM实例,只有当程序运行结束后,这个JVM才会退出。JVM实例通过调用类的main( )方法来启动一个Java程序,而这个main()方法必须是公有的、静态的且返回值为void的方法,该方法接受-一个字符串数组的参数,只有同时满足这些条件才可以作为程序的入口方法。

Java API是Java为了方便开发人员进行开发而设计的,它提供了许多非常有用的接口,这些接口也是用Java语言编写的,并且运行在JVM上。

3.JVM加载class文件的原理机制是什么

Java语言是一种具有动态性的解释型语言,类(class) 只有被加载到JVM中后才能运行。当运行指定程序时,JVM会将编译生成的. class 文件按照需求和一定的规则加载到内存中,并组织成为一个完整的Java应用程序。这个加载过程是由类加载器来完成的,具体来说,就是由ClassLoader和它的子类来实现的。类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。

类的加载方式分为隐式加载与显式加载两种隐式加载指的是程序在使用new等方式创建对象时,会隐式地调用类的加载器把对应的类加载到JVM中。显式加载指的是通过直接调用class. forName( )方法来把所需的类加载到JVM中。

任何一个工程项目都是由许多个类组成的,当程序启动时,只把需要的类加载到JVM中,其他类只有被使用到的时候才会被加载,采用这种方法,一方面可以加快加载速度,另外方面可以节约程序运行过程中对内存的开销。此外,在Java 语言中,每个类或接口都对应一个class文件这些文件可以被看成一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。

在Java语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类( 例如基类)完全加载到JVM中,至于其他类,则在需要时才加载。在Java语言中,可以把类分为3类:系统类、扩展类和自定义类。Java 针对这3种不同的类提供了3种类型的加载器,这3种加载器的关系如下:

在这里插入图片描述
以上这3个类是如何协调工作来完成类的加载呢?其实,它们是通过委托的方式实现的。具体而言,就是当有类需要被加载时,类加载器会请求父类来完成这个载入工作,父类会使用其自己的搜索路径来搜索需要被载入的类,如果搜索不到,才会由子类按照其搜索路径来搜索待加载的类。下例可以充分说明类加载器的工作原理:
在这里插入图片描述
从上例可以看出,TestLoader 类是由AppClassLoader 来加载的。另外需要说明的一点是,由于Bootstrap Loader 是用C++语言来实现的,因此,在Java语言中是看不到它的,所以此时程序会输出null

4.什么是GC

在Java语言中,垃圾回收( Garbage Collction, GC)是 一个非常重要的概念,它的主要作用是回收程序中不再使用的内存。在使用C/C ++语言进行程序开发时,开发人员必须非常仔细地管理好内存的分配与释放,如果忘记或者错误地释放内存往往会导致程序运行不正常甚至是程序崩溃。为了减轻开发人员的工作,同时增加系统的安全性与稳定性,Java 语言提供了垃圾回收器来自动检测对象的作用域,可自动地把不再被使用的存储空间释放掉。具体而言,垃圾回收器要负责完成3项任务:分配内存、确保被引用对象的内存不被错误地回收以及回收不再被引用的对象的内存空间。

垃圾回收器的存在一方面把开发人员从释放内存的复杂工作中解脱出来,提高了开发人员的生产效率;另一方面,对开发人员屏蔽了释放内存的方法,可以避免因开发人员错误地操作内存而导致应用程序的崩溃,保证了程序的稳定性。但是,垃圾回收也带来了问题,为了实现垃圾回收,垃圾回收器必须跟踪内存的使用情况,释放没用的对象,在完成内存的释放后还需要处理堆中的碎片,这些操作必定会增加JVM的负担,从而降低程序的执行效率。

对对象而言,如果没有任何变量去引用它,那么该对象将不可能被程序访问,因此可以认为它是垃圾信息,可以被回收。只要有一个以上的变量引用该对象,该对象就不会被垃圾回收。

对于垃圾回收器来说,它使用有向图来记录和管理堆内存中的所有对象,通过这个有向图就可以识别哪些对象是“可达的” (有引用变量引用它就是“可达的”),哪些对象是“不可达的”(没有引用变量引用它就是不可达的),所有“不可达”对象都是可被垃圾回收的,示例如下:
在这里插入图片描述
上述代码在执行到 i2=il 后,内存的引用关系如下图所示:
在这里插入图片描述

此时,如果垃圾回收器正在进行垃圾回收操作,在遍历上述有向图时,资源2所占的内存是不可达的,垃圾回收器就会认为这块内存已经不会再被使用了,因此就会回收该块内存空间。

垃圾回收都是依据一定的算法进行的,下面介绍其中几种常用的垃圾回收算法。

(1)引用计数算法( Reference Counting Collector)
引用计数作为一种简单但是效率较低的方法,其主要原理如下:在堆中对每个对象都有一个引用计数器;当对象被引用时,引用计数器加1;当引用被置为空或离开作用域的时,引用计数减1,由于这种方法无法解决相互引用的问题,因此JVM没有采用这个算法。

(2)追踪回收算法(Tracing Collector)
追踪回收算法利用JVM维护的对象引用图,从根结点开始遍历对象的应用图,同时标记遍历到的对象。当遍历结束后,未被标记的对象就是目前已不被使用的对象,可以被回收了。

(3)压缩回收算法( Compacting Collector)
压缩回收算法的主要思路如下:把堆中活动的对象移动到堆中一端,这样就会在堆中另外一端留出很大的一块空闲区域,相当于对堆中的碎片进行了处理。虽然这种方法可以大大简化消除堆碎片的工作,但是每次处理都会带来性能的损失。

(4)复制回收算法( Coping Collector)
复制回收算法的主要思路如下:把堆分成两个大小相同的区域,在任何时刻,只有其中的一个区域被使用,直到这个区域的被消耗完为止,此时垃圾回收器会中断程序的执行,通过遍历的方式把所有活动的对象复制到另外-个区域中,在复制的过程中它们是紧挨着布置的,从而可以消除内存碎片。当复制过程结束后程序会接着运行,直到这块区域被使用完,然后再采用上面的方法继续进行垃圾回收。

这个算法的优点是在进行垃圾回收的同时对对象的布置也进行了安排,从而消除了内存碎片。但是这也付出了很高的代价:对于指定大小的堆来说,需要两倍大小的内存空间;同时由于在内存调整的过程中要中断当前执行的程序,从而降低了程序的执行效率。

(5)按代回收算法( Generational Collector)
复制回收算法主要的缺点如下:每次算法执行时,所有处于活动状态的对象都要被复制,这样效率很低。由于程序有“程序创建的大部分对象的生命周期都很短,只有一部分对象有较长的生命周期”的特点,因此可以根据这个特点对算法进行优化。按代回收算法的主要思路如下:把堆分成两个或者多个子堆,每一个子堆被视为一代。算法在运行的过程中优先收集那些“年幼”的对象,如果一个对象经过多次收集仍然“存活”,那么就可以把这个对象转移到高一级的堆里,减少对其的扫描次数。

常见题型:
在这里插入图片描述

5.Java中是否存在内存泄漏问题?

内存泄露是指一个不再被程序使用的对象或变量还在内存中占有存储空间。在C/C++语言中,内存的分配与释放是由开发人员来负责的,如果开发人员忘记释放已分配的内存就会造成内存泄露。而在Java语言中引进了垃圾回收机制,由垃圾回收器负责回收不再使用的对象,既然有垃圾回收器来负责回收垃圾,那么是否还会存在内存泄露的问题呢?

其实,在Java语言中,判断一个内存空间是否符合垃圾回收的标准有两个:第一,给对象赋予了空值null,以后再没有被使用过;第二,给对象赋予了新值,重新分配了内存空间。一般来讲,内存泄露主要有两种情况:一是在堆中申请的空间没有被释放;二是对象已不再被使用,但还仍然在内存中保留着。垃圾回收机制的引人可以有效地解决第一种情况; 而对于第二种情况,垃圾回收机制则无法保证不再使用的对象会被释放。因此,Java 语言中的内存泄露主要指的是第二种情况。

下面通过一个示例来介绍Java语言中的内存泄露:

在这里插入图片描述
在上述例子的循环中,不断创建新的对象加到Vector 对象中,当退出循环后,o的作用域将会结束,但是由于v在使用这些对象,因此垃圾回收器无法将其回收,此时就造成了内存泄露。只有将这些对象从Vector中删除才能释放创建的这些对象。

在Java语言中,容易引起内存泄露的原因很多,主要有以下几个方面的内容:

1)静态集合类,例如HashMap和Vector。如果这些容器为静态的,由于它们的生命周期与程序一致,那么容器中的对象在程序结束之前将不能被释放,从而造成内存泄露,如上例所示。

2)各种连接,例如数据库连接、网络联接以及I0连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement 或ResultSet 不显式地关闭,将会造成大量的对象无法被回收,从而引起内存泄露。

3)监听器。在Java语言中,往往会使用到监听器。通常一个应用中会用到多个监听器,但在释放对象的同时往往没有相应地删除监听器,这也可能导致内存泄露。

4)变量不合理的作用域。一般而言,如果一个变量定义的作用范围大于其使用范围,很有可能会造成内存泄露,另一方面如果没有及时地把对象设置为null,很有可能会导致内存泄露的发生,示例如下:
在这里插入图片描述
在上述伪代码中,通过**readFromNet()方法接收的消息保存在变量msg中,然后调用saveDB( )方法把msg的内容保存到数据库中,此时msg已经没用了,但是由于msg的生命周期与对象的生命周期相同,此时msg还不能被回收,因此造成了内存泄露。**对于这个问题,有如下两种解决方法:第一种方法,由于msg的作用范围只在recieveMsg( )方法内,因此可以把msg定义为这个方法的局部变量,当方法结束后,msg 的生命周期就会结束,此时垃圾回收器就可以回收msg的内容了;第二种方法,在使用完msg后就把msg设置为null,这样垃圾回收器也会自动回收msg内容所占的内存空间。

5)单例模式可能会造成内存泄露。单例模式的实现方法有很多种,下例中所使用的单例模式就可能会造成内存泄露:
在这里插入图片描述
在上述实现的单例模式中,Singleton 存在一个对对象BigClass 的引用,由于单例对象以静态变量的方式存储,因此它在JVM的整个生命周期中都存在,同时由于它有一个对对象BigClass的引用,这样会导致BigClass类的对象不能够被回收。

6.Java中的堆和栈有什么区别

在Java语言中,堆与栈都是内存中存放数据的地方。变量分为基本数据类型和引用类型,基本数据类型的变量(例如int、short、 long、 byte、 float、 double、 boolean 以及char等)以及对象的引用变量(等号左边的部分),其内存都分配在栈上,变量出了作用域就会自动释放,而引用类型的变量(等号右边的部分), 其内存分配在堆上或者常量池(例如字符串常量和基本数据类型常量)中,需要通过new等方式进行创建。

需要特别注意的是,常量池既不属于栈,也不属于堆,他是内存中独立的一块空间。

具体而言,栈内存主要用来存放基本数据类型与引用变量。栈内存的管理是通过压栈和弹栈操作来完成的,以栈帧为基本单位来管理程序的调用关系,每当有函数调用时,都会通过压栈方式创建新的栈帧,每当函数调用结束后都会通过弹栈的方式释放栈帧。

堆内存用来存放运行时创建的对象。一般来讲,通过new关键字创建出来的对象都存放在堆内存中。由于JVM是基于堆栈的虚拟机,而每个Java程序都运行在一个单独的JVM实例上,每一个实例唯一对应一个堆,一个Java程序内的多个线程也就运行在同一个JVM实例上,因此这些线程之间会共享堆内存,鉴于此,多线程在访问堆中的数据时需要对数据进行同步。

在C ++中,堆内存的管理都是由开发人员来负责的,也就是说,开发人员在堆中申请的内存,当不再使用时,必须由开发人员来完成堆内存释放的工作。而在Java语言中,这个内存释放的工作由垃圾回收器来负责执行,开发人员只需要申请所需的堆空间而不需要考虑释放的问题。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。这就是Java中引用的用法。

从堆和栈的功能以及作用来比较,堆主要用来存放对象的,栈主要是用来执行程序的。相较于堆,栈的存取速度更快,但栈的大小和生存期必须是确定的,因此缺乏一定的灵活性。 而堆却可以在运行时动态地分配内存,生存期不用提前告诉编译器,但这也导致了其存取速度的缓慢。

堆和栈的存储如下例所示:
在这里插入图片描述
在上述程序进入main()方法之后,数据的存储关系如下图所示:
在这里插入图片描述
由于i为基本数据类型的局部变量,因此它存储在栈空间中,而r为对象的引用变量,因此也被存储在栈空间中;实际的对象存储在堆空间中,当main( )方法退出后,存储在栈中的i和r通过压栈和弹栈操作将会在栈中被回收,而存储在堆中的对象将会由垃圾回收器来自动回收。

7.关于JVM,在面试中常问的问题收集

1.JVM中的内存是怎么划分的?

JVM中的内存主要划分为5个区域,即方法区,堆内存,程序计数器,虚拟机栈以及本地方法栈。下边是Java虚拟机运行时数据区示意图:
在这里插入图片描述
方法区:方法区是一个线程之间共享的区域。常量,静态变量以及JIT编译后的代码都在方法区主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过-XX:MaxPermSize控制上限。

堆内存堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例(等号右边),通过-Xmx 和-Xms 可以控制大小。

虚拟机栈(栈内存):栈内存中主要保存局部变量、基本数据类型变量以及堆内存中某个对象的引用变量(等号左边)。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊地执行着出栈和入栈的操作。

本地方法栈: 主要是为JVM提供使用native 方法的服务。

程序计数器: 程序计数器是当前线程执行的字节码的位置指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域

提示:
JVM的内存划分主要由以上五个区域组成,需要重点掌握堆内存,栈内存以及方法区域的定义和作用,做到准确理解与阐述。

2.内存分配和垃圾回收

JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。如下图所示:
在这里插入图片描述

  • (1)JVM堆内存的分配:

JVM 初始分配的堆内存由-Xms指定,默认是物理内存的1/64。JVM 最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。

默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小

通过参数Xmn2G 可以设置年轻代大小为2G。通过XX:SurvivorRatio可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域

常见题型:
在这里插入图片描述

  • (2)JVM非堆内存的分配:

JVM使用 XX:PermSize 设置非堆内存初始值,默认是物理内存的1/64。由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。

接下来,我们具体阐述堆内存上的对象分配与回收过程吧。

  • (3)堆内存上对象的分配与回收:
    我们创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个
    -XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝。

另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过
-XX:MaxTenuringThreshold设置晋升年龄。

堆内存上的对象回收也叫做垃圾回收,那么垃圾回收什么时候开始呢?

垃圾回收主要是完成清理对象,整理内存的工作。上面说到GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC,分别介绍如下。

  • Minor GC(年轻代GC):
    对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。

  • Full GC(老年代GC):
    Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。

接下来,我们来看关于内存分配与回收的两个重要概念吧。

动态对象年龄判定:

如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold。

空间分配担保:

发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC

解析:

关于JVM堆内存上对象的分配与回收是面试中考察的重点,希望大家可以对相关知识点熟练掌握,并且清晰阐述各个关键概念。

关于JVM和GC重点去看看这三个文章:
深入理解JVM-内存模型(jmm)和GC

Java进阶(3)——JVM优化之概念学习

Java进阶(3)——JVM优化之垃圾回收

本文的面试题主要借鉴:
Java开发岗高频面试题

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值