X、一些面试题
0、JDK、JRE、JVM
JDK是Java开发工具包,内含JRE,可用于程序开发、也支持程序运行;
JRE是Java运行环境,只能用于程序运行,不能用于程序开发;
JVM是Java虚拟机,负责“翻译”、执行Java代码。
1、说一下 JVM 的主要组成部分及其作用?
JVM包含两个子系统和两个组件,两个子系统为Class loader(类加载器)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
Class loader(类装载):根据给定的全限定名类名,加载字节码文件到运行时数据区中的方法区。
Execution engine(执行引擎):执行classes中的指令。
Native Interface(本地库接口):与本地方法库交互,是与其它编程语言交互的接口。
Runtime data area(运行时数据区):这就是我们常说的JVM的内存。
作用
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
上述作用,也是jvm运行流程。
2、JVM 运行时数据区的组成结构
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范,Java 虚拟机规范规定运行时数据区分为以下 5 个部分:
1)程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
2)Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
3)本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用本地方法服务的;
4)Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
5)方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
3、说一下堆栈的区别?
1)物理地址不同
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。比如,标记-消除,复制,标记-压缩,分代(即新生代使用复制算法,老年代使用标记-压缩)
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
2)内存分配不同
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
栈是连续的,所以分配的内存大小要在编译期就已确认,大小是固定的。
3)存放的内容不同
堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
PS:
1)静态变量放在方法区;
静态的对象放在堆。
2)程序的可见度
堆对于整个应用程序都是共享、可见的;
栈只对于线程是可见的,它的生命周期和线程相同。
4、队列和栈是什么?有什么区别?
队列和栈都是被用来预存储数据的。
区别:
1)操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
2)可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
3)操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。
5、什么是内存泄漏?Java会存在内存泄漏吗?
内存泄漏是指不再被使用的对象或者变量还一直占据在内存中。
理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因是:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。
5.1、内存溢出
内存溢出即常说的OOM,指的是申请内存超过系统所能提供的大小,从而导致系统异常或崩溃。
常见原因:
1)递归调用层数过多;2)长时间运行程序,频繁分配内存、但没有及时回收;3)读取大文件或处理大数据时,内存不足以容纳数据导致溢出;
4)循环引用:两个或多个对象互相引用。
内存泄漏与内存溢出是两个独立的概念,但我认为,大量的内存泄漏就可能导致内存溢出,有一定的因果关系。
6、请简述Java垃圾回收机制 / GC是什么?
垃圾回收机制,也就是常说的GC(Gabage Collection)机制,它是指JVM中一个用来清理垃圾、回收内存的程序,它是低优先级的、在一般情况下不会执行,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被引用的对象,并将它们添加到要回收的集合中,进行回收。
7、为什么要进行垃圾回收?请简述垃圾回收的优点和原理
内存处理是编程人员容易忘记和忽视的地方,但内存不回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。
垃圾回收机制也是java语言一个显著的特点,它使java程序员在编写程序时不用再考虑内存管理的问题。
由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。
垃圾回收机制还有效的防止了内存泄露,使内存得到更有效的应用。
8、在Java中,对象什么时候可以被垃圾回收?JVM怎么判断对象是否可以被回收?JVM中的永久代中会发生垃圾回收吗?
1)当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
2)一般有两种方法来判断:
引用计数器法:为每个对象创建一个引用计数器,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点是不能解决循环引用;
可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
3)虽然垃圾回收不会发生在永久代,但永久代也是会被回收的;当永久代满了或者是超过了临界值,会触另一种机制——发完全垃圾回收(Full GC)。
9、说一下 JVM 有哪些垃圾回收算法?
1)标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
2)复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
3)标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
4)分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
10、JVM垃圾回收算法和垃圾回收器是什么关系?说一下 JVM 有哪些垃圾回收器?
垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
Parallel Old收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
G1(Garbage First)收集器 (标记-整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
11、细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
12、简述java类加载机制 / 描述一下JVM加载Class文件的原理机制
jvm的类加载,就是就是jvm的类加载器根据类的全限定类名,把这个类的数据从字节码文件加载到jvm内存中,并对数据做校验、解析和初始化,最终形成可以被虚拟机直接使用的指令,这个过程就是jvm的类加载
Java类的加载是动态的,并不是一次性将所有类全部加载后再运行,为了节省内存开销,刚开始只是将那些保证程序运行的基础类全部加载到jvm中,至于其他类,则在需要的时候才加载。
ps:类装载方式有两种 :
1)隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中(用的时候再加载就是隐式加载)
2)显式装载, 通过class.forname()等方法,显式加载需要的类(启动的时候就加载就是显式加载)
13、什么是类加载器,类加载器有哪些?
通过类的全限定类名、获取该类的二进制字节流的代码块,就叫做类加载器。
主要有一下四种类加载器:
启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
14、说一下类装载的执行过程?
类装载分为以下 5 个步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
15、什么是双亲委派模型?
双亲委派模型是JVM类加载器加载字节码文件的一种方式,具体为:
当一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
16、说一下 JVM 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
16.1、JVM调优都做些什么?
JVM调优的主要目的:
1)控制GC的行为,GC是很消耗系统性能的,因此经常需要根据系统运行程序的特性来更改GC行为;
2)控制JVM堆栈大小,一般来说,JVM的内存分配不需要修改,但是当你的程序新生代对象在某个时间段产生的比较多的时候,就需要控制新生代的堆大小;同时,还要需要控制总的JVM大小避免内存溢出;
3)控制JVM线程的内存分配,如果是多线程程序,产生线程和线程运行所消耗的内存也是可以控制的,需要通过一定时间的观测后,配置最优结果。
17、常用的 JVM 调优的参数都有哪些?
-Xmx2g:指定java程序的最大堆内存为 2g;
-Xms:指定java程序的最小堆内存,通常设置成跟最大堆内存一样,减少GC;
-Xmn:设置年轻代大小。整个堆大小=年轻代大小+年老代大小。所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8;
-Xss:指定线程的最大栈空间, 此参数决定了java函数调用的深度, 值越大调用深度越深, 若值太小则容易出栈溢出错误(StackOverflowError);
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:PermSize:指定方法区(永久区)的初始值,默认是物理内存的1/64,在Java8永久区移除, 代之的是元数据区,由-XX:MetaspaceSize指定;
-XX:MaxPermSize:指定方法区的最大值, 默认是物理内存的1/4,在java8中由-XX:MaxMetaspaceSize指定元数据区的大小;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。