Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。
文章目录
说说 JVM 的主要组成成分及其作用?
JVM 各组成如图(图片来源网络)
Class Loader 类加载器
:加载类文件到内存,Class loader 只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是由Exectution Engine 执行引擎负责的。Exection Engine 执行引擎
:字节码执行引擎也叫解释器,负责解释命令,交由操作系统执行。Native Interface 本地方法接口
:本地接口的作用是融合不同的语言为 Java 所用。Runtime Data Area 运行时数据区
:运行时数据区是 JVM 的重点,我们所有所写的程序都被加载到这里,之后才开始运行。运行时数据区主要包括:程序计数器、Java 虚拟机栈、本地方法栈、Java 堆、方法区。
什么是 JVM 内存结构?
请参考:【Java 运行时数据区域】
说一下堆栈的区别?
请参考:【Java 运行时数据区域】
谈谈 JVM 中的常量池?
JVM 常量池主要分为 Class 文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
Class 文件常量池
。Class 文件是一组以字节为单位的二进制数据流,在 Java 代码的编译期间,我们编写的 Java 文件就被编译为 .class 文件格式的二进制数据存放在磁盘中,其中就包括 Class 文件常量池。运行时常量池
:当类加载到内存中后,JVM 就会将 Class 文件常量池中的内容存放到运行时的常量池中;在解析阶段,JVM 会把符号引用替换为直接引用,但是运行时常量池的内容并不全部来自 Class 常量池;在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是 String.intern()。全局字符串常量池
:字符串常量池是 JVM 所维护的一个字符串实例的引用表,在 HotSpot VM 中,它是一个叫做 StringTable 的全局表。在字符串常量池中维护的是字符串实例的引用,底层 C++ 实现就是一个 HashTable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。基本类型包装类对象常量池
:Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这 5 种整型的包装类也只是在对应值小于等于 127 时才可使用对象池,也即对象不负责创建和管理大于 127 的这些类的对象。
怎么判断对象是否可以被回收?
- 引用计数法
- 可达性分析算法
请参考:【对象存活、垃圾收集算法】
Java 中都有哪些引用类型?
- 强引用、软引用、弱引用、虚引用
请参考:【对象存活、垃圾收集算法】
被引用的对象就一定能存活吗?
- 不一定,看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候,即 OOM 前会被回收,但如果没有在 Reference 链中的对象就一定会被回收。
说一下 JVM 有哪些垃圾回收算法
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
请参考:【对象存活、垃圾收集算法】
说一下 JVM 有哪些垃圾收集器
- Serial 收集器
- ParNew 收集器
- Parallel Scavenge 收集器
- Serial Old 收集器
- Parallel Old 收集器
- CMS 收集器
- G1 收集器
请参考:【垃圾收集器详解】
详细介绍一下 CMS 垃圾收集器
请参考:【垃圾收集器详解】
详细说一下 G1 的回收过程?
请参考:【垃圾收集器详解】
JVM 中一次完整的 GC 是什么样子的?
先描述一下 Java 堆内存划分。
- 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。 新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
- 新生代的垃圾回收(又称 Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
- 老年代的垃圾回收(又称 Major GC)通常使用“标记-清除”或“标记-整理”算法。
再描述它们之间转化流程:
- 对象优先在 Eden 分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。
- 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能够被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。对象晋升的老年代的年龄阈值,可以通过参数
-XX:MaxTenuringThreshold
设置。 - 动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率) 时,大于或等于该年龄的对象直接进入老年代。其中这个目标使用率通过
-XX:TargetSurvivorRatio
指定,默认为 50%; - Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。
- 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能够被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为 1。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。对象晋升的老年代的年龄阈值,可以通过参数
- 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。虚拟机提供了一个
-XX:PretenureSizeThreshold
参数,令大于这个设置值的对象直接在老年代分配。 - 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行 Full GC,Full GC 清理整个内存堆,包括年轻代和老年代。
请参考:【内存分配与回收策略】
说说空间分配担保机制
- 如果 Minor GC 时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这时发现老年代也放不下这些对象了,那怎么处理呢?其实 JVM 有一个老年代空间分配担保机制来保证对象能够进入老年代。
- 在执行每次 Minor GC 之前,JVM 会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。因为在极端情况下,可能新生代 Minor GC 后,所有对象都存活下来了,而 survivor 区又放不下,那可能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小的,那就可以放心进行 Minor GC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老年代空间不够放入新生代所有存活对象,这个时候 JVM 就会先检查
-XX:HandlePromotionFailure
参数是否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC,尽快这次 Minor GC 是有风险的。如果小于,或者-XX:HandlePromotionFailure
参数不允许担保失败,这时就会进行一次 Full GC。 - 在允许担保失败并尝试进行 Minor GC 后,可能会出现三种情况:
- ① Minor GC 后,存活对象小于 survivor 大小,此时存活对象进入 survivor 区中。
- ② Minor GC 后,存活对象大于 survivor 大小,但是小于老年代可用空间大小,此时直接进入老年代。
- ③ Minor GC 后,存活对象大于 survivor 大小,也大于老年代可用空间大小,老年代也放不下这些对象了,此时就会发生 “Handle Promotion Failure”,就触发了 Full GC。如果 Full GC 后,老年代还是没有足够的空间,此时就会发生 OOM 内存溢出了。
说一下 JVM 调优工具?
- jps:虚拟机进程状况工具
- jstat:虚拟机统计信息监视工具
- jinfo:Java 配置信息工具
- jmap:Java 内存映像工具
- jhat:虚拟机堆转储快照分析工具
- jstack:Java 堆栈跟踪工具
- HSDIS:JIT 生成代码反汇编
请参考:【虚拟机性能监控与故障处理工具】
常用的 JVM 调优的参数有哪些?
请参考:【调优案例分析与实战】