上海携程 java 高级面试题(一)
一、JVM 加载 Class 文件的原理机制?
在面试 java 工程师的时候,这道题经常被问到,故需特别注意。
Java 中的所有类,都需要由类加载器装载到 JVM 中才能运行。类加载器本身也是一个类,而它的工作就是把 class 文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
Java 类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到 jvm 中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
Java 的类加载器有三个,对应 Java 的三种类:
类加载器是一个用来加载类文件的类。Java 源代码通过 javac 编译器编译成类 文件。然后 JVM 来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。
有三种默认使用的类加载器:Bootstrap 类加载器、Extension 类加载器和 Application 类加载器。每种类加载器都有设定好从哪里加载类。
Bootstrap 类加载器负责加载 rt.jar 中的 JDK 类文件,它是所有类加载器的父加载 器 。Bootstrap 类 加 载 器 没 有 任 何 父 类 加 载 器 , 如 果 你 调 用 String.class.getClassLoader() , 会 返 回 null , 任 何 基 于 此 的 代 码 会 抛 出 NullPointerException 异常。Bootstrap 加载器被称为初始类加载器。
而 Extension 将加载类的请求先委托给它的父加载器,也就是 Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性定义的目录下加载类。Extension 加载器由 sun.misc.Launcher$ExtClassLoader 实现。
第三种默认的加载器就是 Application 类加载器了。它负责从 classpath 环境变量中加载某些应用相关的类,classpath 环境变量通常由-classpath 或-cp 命令行选项来定义,或者是 JAR 中的 Manifest 的 classpath 属性。Application 类加 载 器 是 Extension 类 加 载 器 的 子 加 载 器 。通 过 sun.misc.Launcher$AppClassLoader 实现。
三个加载器各自完成自己的工作,但它们是如何协调工作呢?哪一个类该由哪个类加载器完成呢?为了解决这个问题,Java 采用了委托模型机制。
委托模型机制的工作原理很简单:当类加载器需要加载类的时候,先请示其 Parent(即上一层加载器)在其搜索路径载入,如果找不到,才在自己的搜索路径搜索该类。这样的顺序其实就是加载器层次上自顶而下的搜索,因为加载器必须保证基础类的加载。之所以是这种机制,还有一个安全上的考虑:如果某人将一个恶意的基础类加载到 jvm,委托模型机制会搜索其父类加载器,显然是不可能找到的,自然就不会将该类加载进来。
我们可以通过这样的代码来获取类加载器:
注意一个很重要的问题,就是 Java 在逻辑上并不存在 BootstrapKLoader 的实
体!因为它是用 C++编写的,所以打印其内容将会得到 null。
前面是对类加载器的简单介绍,它的原理机制非常简单,就是下面几个步骤:
1.装载:查找和导入 class 文件;
2.连接:
3. 初始化:初始化静态变量,静态代码块
二、什么是 Java 垃圾回收机制?
什么是垃圾回收机制:在系统运行过程中,会产生一些无用的对象, 这些对象占据着一定的内存, 如果不对这些对象清理回收无用对象的内存, 可能会导致内存的耗尽, 所以垃圾回收机制回收的是内存。同时 GC 回收的是堆区和方法区的内存。
JVM 回收特点:(stop-the-world)当要进行垃圾回收时候, 不管何种 GC 算法, 除了垃圾回收的线程之外其他任何线程都将停止运行。被中断的任务将会在垃圾回收完成后恢复进行。GC 不同算法或是 GC 调优就是减少 stop-the-world 的时间。à(为何非要 stop-the-world),就像是一个同学的聚会,地上有很多垃圾, 你去打扫, 边打扫边丢垃圾怎么都不可能打扫干净的哈。当在垃圾回收时候不暂停所有的程序, 在垃圾回收时候有 new 一个新的对象 B,此时对象 A 是可达 B 的,但是没有来及标记就把 B 当成无用的对象给清理掉了,这就会导致程序的运行会出现错误。
如何判断哪些对象需要回收呢:
引用计数算法(java 中不是使用此方法):每个对象中添加一个引用计数器, 当有别人引用它的时候, 计数器就会加 1, 当别人不引用它的时候, 计数器就会减 1, 当计数器为 0 的时候对象就可以当成垃圾。算法简单, 但是最大问题就是在循环引用的时候不能够正确把对象当成垃圾。
根搜索方法( 这是后面垃圾搜集算法的基础) :这是 JVM 一般使用的算法, 设立若干了根对象, 当上述若干个跟对象对某一个对象都不可达的时候, 这个对象就是无用的对象。对象所占的内存可以回收。
根搜索算法的基础上, 现代虚拟机的实现当中, 垃圾搜集的算法主要有三种, 分别是标记-清除算法、 复制算法、 标记-整理算法。
标记-消除算法:当堆中的有效内存被耗尽的时候, 就会停止整个系统, 就会调用标记-消除算法, 主要做两件事, 1 就是标记, 2 就是清除。然后让程序恢复。
标记:遍历所有 GCroots 把可达的对象标记为存活的对象。
清除:把未标记为存活的对象清楚掉。
缺点:
就是效率相对比较低。会导致 stop-the-world 时间过长。
因为无用的对象内存不是连续的因此清理后的内存也不是连续的, (会产生内存碎片)因此 JVM 还要维持一个空闲列表, 增加一笔开销, 同时在以后内存使用时候, 去查找可用的内存这个效率也是很低的。
复制算法:(这个算法一般适合在新生代 GC), 将原有的内存分为两块, 每次只适用其中的一块, 在垃圾回收的时候, 将一块正在使用的内存中存活(上述根搜索的算法)的对象复制到另一块没有使用的内存中, 原来的那一块全部清除。 与上述的标记-清除算法相比效率更高,但是不太适合使用在对象存活较多的情况下(如老年代)。
缺点:
每次对整个半区内存回收, 因此效率比上面的要高点, 同时在分配内存的时候不需要考虑内存的碎片。
按照顺序分配内存。
简单高效。
但是最大的问题在于此算法在对象存活率非常低的时候使用, 将可用内存分为两份, 每次只使用一份这样极大浪费了内存。
注意(重要) :
现在的虚拟机使用复制算法来进行新生代的内存回收。
因为在新生代中绝大多数的对象都是“朝生夕亡”, 所以不需要将整个内存分为两个部分, 而是分为三个部分, 一块为 Eden 和两块较小的 Survivor 空间(比例->8:1:1)。
每次使用 Eden 和其中的一块 Survivor,垃圾回收时候将上述两块中存活的对象复制到另外一块 Survivor 上, 同时清理上述 Eden 和 Survivor。
所以每次新生代就可以使用 90%的内存。
只有 10%的内存是浪费的。
(不能保证每次新生代都少于 10%的对象存活, 当在垃圾回收复制时候如果一块 Survivor 不够时候, 需要老年代来分担, 大对象直接进入老年代)
标记-整理算法:
(老年代 GC)在存活率较高的情况下, 复制的算法效率相对比较低, 同时还要考虑存活率可能为 100%的极端情况, 因此又不能把内存分为两部分的复制算法。
在上面标记-复制算法的基础之上, 演变出了一个新的算法就是标记-整理算法。
首先从 GCroots 开始标记所有可达的对象, 标记为存活的对象。
然后将存活的对象压缩到内存一端按照内存地址的次序依次排列, 然后末端内存地址之后的所有内存都清除。
总结:
将标记存活的对象按照内存地址顺序排列到内存另一端, 末端内存地址之后的内存都会被清除。
比较:
相比较于标记-清楚算法 (传统的), 该算法可以解决内存碎片问题同时还可以解决复制算法部分内存不能利用的问题。
但是标记-整理算法的效率也不是很高。
上述算法都是根据根节点搜索算法来判断一个对象是不是需要回收, 而支撑根节点搜索算法能够正常工作理论依据就是语法中变量作用域的相关内容。
三种算法比较:
效率:
复制算法>标记-整理算法>标记-清除算法;
内存整齐度:
复制算法=标记-整理算法>标记-清除算法
内存利用率:
标记-整理算法=标记-清除算法>复制算法
分代收集算法:
现在使用的 Java 虚拟机并不是只是使用一种内存回收机制, 而是分代收集的算法。就是将内存根据对象存活的周期划分为几块。一般是把堆分为新生代、 和老年代。短命对象存放在新生代中, 长命对象放在老年代中。
对于不同的代, 采用不同的收集算法:
新生代:
由于存活的对象相对比较少, 因此可以采用复制算法该算法效率比较快。
老年代:
由于存活的对象比较多哈, 可以采用标记-清除算法或是标记-整理算法
( 注意) 新生态由于根据统计可能有 98%对象存活时间很短因此将内存分为一块比较大的 Eden 空间和两块较小的 Survivor 空间, 每次使用 Eden 和其中一块 Survivor。
当回收时, 将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上, 最后清理掉 Eden 和刚才用过的 Survivor 空间。
上述是垃圾回收机制的算法, 但是垃圾回收器才是垃圾回收的具体实现:
常见有五个垃圾回收器:
一:串行收集器:(Serial 收集器)
该收集器最古老、 稳定简单是一个单线程的收集器, (stop-the-world) 可能会产生长时间的停顿. serial 收集器一定不能用于服务器端。
这个收集器类型仅应用于单核 CPU 桌面电脑。
新生代和老年代都会使用 serial 收集器。
新生代使用复制算法(内存分三块的那个复制算法)。
老年代使用标记-整理算法。
二:并行收集器:(Parallel 收集器)
parallel 收集器使用多线程并行处理 GC, 因此更快。
当有足够大的内存和大量芯数时, parallel 收集器是有用的。
它也被称为“吞吐量优先垃圾收集器。”
三:并行收集器:(Parallel Old 垃圾收集器)
相比于 parallel 收集器, 他们的唯一区别就是在老年代所执行的 GC 算法的不同。
它执行三个步骤:
标记-汇总-压缩( mark – summary – compaction) 。
汇总步骤与清理的不同之处在于, 其将依然幸存的对象分发到 GC 预先处理好的不同区域, 算法相对清理来说略微复杂一点。
四:并行收集器:(CMS 收集器)
(ConcurrentMark Sweep:并发标记清除) 是一种以获取最短回收停顿时间为目标的收集器。
适合应用在互联网站或者 B/S 系统的服务器上, 这类应用尤其重视服务器的响应速度, 希望系统停顿时间最短。
五:G1 收集器
这个类型的垃圾收集算法是为了替代 CMS 收集器而被创建的, 因为 CMS 收集器在长时间持续运行时会产生很多问题。
评论