您的当前位置:首页>全部文章>文章详情

上海携程java高级面试题(三)

发表于:2024-01-08 21:37:17浏览:231次TAG: #Java知识 #Java面试题
1
Java 堆的结构是什么样子的?
JVM 的堆是运行时数据区, 所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的, 不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前, 他们会一直占据堆内存空间。
永久代是用于存放静态文件, 如 Java 类、 方法等。持久代对垃圾回收没有显著影响, 但是有些应用可能动态生成或者调用一些 class, 例如 Hibernate 等, 在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类, 永久代中一般包含:
类的方法(字节码…)类名(Sring 对象)
.class 文件读到的常量信息
class 对象相关的对象列表和类型列表 (e.g., 方法对象的 array).
JVM 创建的内部对象JIT 编译器优化用的信息
虚拟机中的共划分为三个代:
年轻代(Young Generation) 、 年老代(Old Generation) 和持久代(Permanent Generation) 。
其中持久代主要存放的是 Java 类的类信息, 与垃圾收集要收集的 Java 对象关系不大。年轻代和年老代的划分是对垃 圾收集影响比较大的。
年轻代:
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个 Eden 区, 两个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成。
当 Eden 区满时, 还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时, 此区的存活对象将被复制到另外一个 Survivor 区, 当这个 Survivor去也满了的时候, 从第一个 Survivor 区复制过来的并且此时还存活的对象, 将被复制“年老区(Tenured)”。
需要注意, Survivor 的两个区是对称的, 没先后关系, 所以同一个区中可能同时存在从 Eden 复制过来对象, 和从前一个 Survivor 复制过来的对象, 而复制到年老区的只有从第一个 Survivor 去过来的对象。而且, Survivor 区总有一个是空的。同时, 根据程序需要, Survivor 区是可以配置为多个的(多于两个) , 这样可以增加对象在年轻代中的存在时间, 减少被放到年老代的可能。
年老代:
在年轻代中经历了 N 次垃圾回收后仍然存活的对象, 就会被放到年老代中。因此, 可以认为年老代中存放的都是一些生命周期较长的对象。
持久代:
用于存放静态文件, 如今 Java 类、 方法等。持久代对垃圾回收没有显著影响, 但是有些应用可能动态生成或者调用一些 class, 例如 Hibernate 等, 在这种时候需要设置一个比较大的持 久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
注意:
JDK1.8中, 永久代已经从java堆中移除, String直接存放在堆中, 类的元数据存储在meta space中, meta space 占用外部内存, 不占用堆内存。
可以说, 在 java8 的新版本中, 持久代已经更名为了元空间(meta space) 。
2
Java 中会存在内存泄漏吗, 简述一下?
理论上 Java 因为有垃圾回收机制(GC) 不会存在内存泄露问题(这也是 Java 被广泛使用于服务器端编程的一个重要原因) ;然而在实际开发中, 可能会存在无用但可达的对象, 这些对象不能被 GC 回收, 因此也会导致内存泄露的发生。 
例如 Hibernate 的 Session(一级缓存)中的对象属于持久态, 垃圾回收器是不会回收这些对象的, 然而这些对象中可能存在无用的垃圾对象, 如果不及时关闭(close) 或清空(flush) 一级缓存就可能导致内存泄露。
3
Java 类加载过程?
在 Java 中, 类装载器把一个类装入 Java 虚拟机中, 要经过三个步骤来完成:装载、 链接和初始化, 其中链接又可以分成校验、 准备、 解析装载:查找和导入类或接口的二进制数据;
链接:执行下面的校验、 准备和解析步骤, 其中解析步骤是可以选择的;
校验:检查导入类或接口的二进制数据的正确性;
准备:给类的静态变量分配并初始化存储空间;解析:将符号引用转成直接引用;
初始化:激活类的静态变量,初始化 Java 代码和静态 Java 代码块
4
什么是 GC? 为什么要有 GC?
GC 是垃圾收集的意思, 内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃, Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的, Java 语言没有提供释放已分配内存的显示操作方法。 
Java 程序员不用担心内存管理, 因为垃圾收集器会自动进行管理。要请求垃圾收集, 可以调用下面的方法之一:System.gc()或 Runtime.getRuntime().gc(), 但 JVM 可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露, 有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行, 不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收, 程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
在 Java 诞生初期, 垃圾回收是 Java 最大的亮点之一, 因为服务器端的编程需要有效的防止内存泄露问题, 然而时过境迁, 如今 Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得 iOS 的系统比 Android 系统有更好的用户体验, 其中一个深层次的原因就在于 Android 系统中垃圾回收的不可预知性。
采用“分代式垃圾收集”。这种方法会跟 Java 对象的生命周期将堆内存划分为不同的区域, 在垃圾收集过程中, 可能会将对象移动到不同区域:
伊甸园(Eden) :这是对象最初诞生的区域, 并且对大多数对象来说, 这里是它们唯一存在过的区域。
幸存者乐园(Survivor) :从伊甸园幸存下来的对象会被挪到这里。
终身颐养园(Tenured) :这是足够老的幸存对象的归宿。年轻代收集(Minor-GC) 过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时, 就会触发一次完全收集(Major-GC) , 这里可能还会牵扯到压缩, 以便为大对象腾出足够的空间。
与垃圾回收相关的 JVM 参数:
-Xms / -Xmx —堆的初始大小 / 堆的最大大小

-Xmn — 堆中年轻代的大小

补充:
Java 是由 C++发展来的。
它摈弃了 C++中一些繁琐容易出错的东西。其中有一条就是这个 GC。
写 C/C++程序, 程序员定义了一个变量, 就是在内存中开辟了一段相应的空间来存值。内存再大也是有限的, 所以当程序不再需要使用某个变量的时候, 就需要释放这个内存空间资源,好让别的变量来用它。在 C/C++中, 释放无用变量内存空间的事情要由程序员自己来解决。就是说当程序员认为变量没用了, 就应当写一条代码, 释放它占用的内存。这样才能最大程度地避免内存泄露和资源浪费。
但是这样显然是非常繁琐的。程序比较大, 变量多的时候往往程序员就忘记释放内存或者在不该释放的时候释放内存了。而且释放内存这种事情, 从开发角度说, 不应当是程序员所应当关注的。程序员所要做的应该是实现所需要的程序功能, 而不是耗费大量精力在内存的分配释放上。
Java 有了 GC, 就不需要程序员去人工释放内存空间。当 Java 虚拟机发觉内存资源紧张的时候, 就会自动地去清理无用变量所占用的内存空间。当然, 如果需要, 程序员可以在 Java程序中显式地使用 System.gc()来强制进行一次立即的内存清理。
因为显式声明是做堆内存全扫描, 也就是 Full GC, 是需要停止所有的活动的(Stop The World Collection) , 你的应用能承受这个吗?而其显示调用 System.gc()只是给虚拟机一个建议, 不一定会执行, 因为 System.gc()在一个优先级很低的线程中执行。
5
如何判断一个对象是否存活?

在堆里面存放着 Java 世界中几乎所有的对象实例, 垃圾收集器对堆内存进行回收前, 都会先判断这些对象之中哪些还“存活”着, 哪些已经“死去”(即不可能在被任何途径使用的对象)。一共有两种算法:

引用计数算法
给对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就是不可能再被使用的。

JVM 里面并没有选用引用计数算法来管理内存, 主要原因是它很难解决对象之间相互循环引用的问题。

可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链(Reference Chain), 当一个对象到 GC Roots 没有任何引用链相连时, 则证明此对象是不可用的。

更多文章,请关注公众号【程序员李木子】,有免费的电子书哦