《Java虚拟机》必知必会的 14 个问题总结(内存模型+GC)

Posted Hot Autumn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java虚拟机》必知必会的 14 个问题总结(内存模型+GC)相关的知识,希望对你有一定的参考价值。

一、Java 概述

1、Java 相较于 PHP、C#、Ruby 等一样很优秀的编程语言的优势是什么?

(1)体系结构中立,跨平台性能优越。Java 程序依赖于 JVM 运行,javac 编译器编译Java程序为平台通用的字节码文件(.class),再由 JVM 与不同操作系统匹配,装载字节码并解释(也有可能是编译,会在第三个问题中说到)为机器指令执行。

(2)安全性优越。通过 JVM 与宿主环境隔离,且 Java 的语法也一定程度上保障了安全,如废弃指针操作、自动内存管理、异常处理机制等。

(3)多线程。防止单线程阻塞导致程序崩溃,分发任务,提高执行效率。

(4)分布式。支持分布式,提高应用系统性能。

(5)丰富的第三方开源组件。SpringStrutsHibernate、Mybatis、Quartz 等等等等。

2、字节码是什么? .class 字节码文件是什么?

(1)字节码是包含 Java 内部指令集、符号集以及一些辅助信息的能够被 JVM 识别并解释运行的符号序列。字节码内部不包含任何分隔符区分段落,且不同长度数据都会构造成 n 个 8 位字节单位表示。

(2).class 里存放的就是 Java 程序编译后的字节码,包含了类版本信息、字段、方法、接口等描述信息以及常量池表,一组8位字节单位的字节流组成了一个字节码文件。

3、JVM 是什么?HotSpot 虚拟机有什么特点?

JVM 全称 Java Visual Machine,Java 虚拟机。是 Java 程序的运行环境,主要负责装载字节码文件,并解释或编译成对应平台的机器指令执行。

我们使用最多的是 JDK 缺省自带的 HotSpot 虚拟机,使用解释器加编译期并存架构方案。一开始的时候使用解释器,使编译未结束时就可以解释字节码为本地机器指令执行,提高效率。编译器用在 HotSpot 的热点探索功能上,在存在频繁调用的方法或循环次数较多的代码时,就会把这类代码块标记为 “热点代码”,通过内嵌的双重 JIT(Just in time compiler)将字节码直接编译成对应机器指令,以提高效率。

二、Java 内存模型

1、PC 计数器

线程私有,用于记录当前线程正在执行字节码的地址,如果执行的是 native 本地方法,PC 计数器为空。

2、Java 栈

线程私有,也叫作 Java 虚拟机栈,用于存储栈帧,栈帧的入栈出栈过程即方法调用到执行结束的过程。栈帧中主要存放方法执行所需的局部变量表(包括局部变量的声明数据类型、对象引用等)、操作数栈、方法出口等信息

3、本地方法栈

与 Java 栈功能类似,只是用于存储 native 本地方法的相关信息。

4、Java 堆

线程公用,用于存放对象实例,包括数组,也叫 GC 区,是 GC 主要工作的区域。也正是如此,由于 GC 频率过快与效率不高,堆区的可能成为 JVM 性能瓶颈,于是考虑到性能,堆区不再是对象内存分配的唯一选择。这里就涉及到了对象的逃逸分析与栈上分配。

逃逸分析就是用来分析对象的作用域是否在方法内部,当方法返回了当前类实例对象、方法中为当前类成员变量赋值、方法中引用当前类成员变量的值时就会发生逃逸,依然在堆上分配内存。但当对象的作用域就在方法内时,比如在方法内创建了该类的实例,没有返回、没有引用,则这种情况就直接在 Java 栈上分配内存,随着栈帧的出栈释放空间,减轻了堆区GC 的压力。

5、方法区

线程公用,存储了每一个 Java 类的结构信息,比如:字段、各种方法的字节码内容数据、运行时常量池等。方法区也被称为永久带。一般没有显示要求,GC 只对方法区中的常量池回收以及类型卸载。

6、运行时常量池

属于方法区的一部分,类加载器将类的字节码文件加载如 JVM 中后,会把字节码文件中的常量池表转化为运行时常量池。

三、Java 垃圾回收机制

(1)引用计数法:每个对象都创建一个私有的引用计数器,当该对象被其他对象引用时(出现在等号右边),引用计数器加 1;当不再引用时,引用计数器减 1;当引用计数器为 0 时,对象即可被回收。这种方式存在着当两个对象互相引用时,二者引用计数器值都不为 0 无法被回收的问题;

(2)根搜索算法:JVM 一般使用的标记算法,把对象的引用看作图结构,由根节点集合出发,不可达的节点即可回收,其中根节点集合包含的如下 5 种元素:

1、Java 栈中的对象引用;

2、本地方法栈中的对象引用;

3、运行时常量池的对象引用;

4、方法区中静态属性的对象引用;

5、所有 Class 对象;

2、常见的垃圾回收算法有哪些?JVM 使用哪种?

(1)标记-清除算法:分两个阶段执行,第一个阶段标记可用对象,第二个阶段清除垃圾对象;这个方法很基础简单,但效率低下,而且会产生内存碎片(不连续的内存空间),无法再次分配给较大对象。

(2)复制算法:被广泛用于新生代对象的回收。将内存分为两个区域,新对象都分配在一个区域中,回收时将可用对象连续复制到另一个区域,回收完成后,新对象分配在有对象的区域,循环往复。这种算法不会产生内存碎片,且效率较高,但因为同时只有一个区域有效,会导致内存利用率不高。

(3)标记-整理算法:被应用于老年代对象的回收。这种算法与标记清除算法类似,第一个阶段标记可用对象,第二个阶段将可用对象移动到一段连续的内存上,解决了标记-清除算法会产生内存碎片的缺点。

(4)分代回收算法:在 HotSpot 虚拟机中,基于分代的特点(堆内存可进一步分为年轻代、老年代,老年代存放存活时间较长的对象),JVM GC 使用分代回收算法。

年轻代使用复制算法:分为一个较大的 Eden 区与两个较小的、等大小的 Survivor 区(From Space 与 To Space),比例一般是 8:1:1。新对象都分配在 Eden 区,当 GC 发生时(新生代的 GC 一般叫做 Minor GC),将 Eden 区与 From 区中的可用对象复制到 To 区中,From Space 与 To Space 互换名称,循环方法。直到发生如下两种情况,对象进入老年代:

1' From 区内的对象已经达到存活代数阀值(经过 GC 的次数达到设定值),GC 时不会进入 To 区中,直接移动至老年代;

2' 在回收 Eden 区与 From 区后,超出 To 区可容纳范围,则直接将存活对象移动至老年代。

老年代使用标记-整理算法:当老年代满的时候,会触发 Full GC(新生代与老年代一起进行 GC)。

3、常见的垃圾回收器有哪些?有什么特点?适合应用与什么场景?

(1)Serial 收集器

年轻代采用复制算法、串行回收、与 “Stop the world” 机制(GC 时停止其他一切工作),适用于单核 CPU 环境,绝对不推荐应用于服务器端。

Serial 提供了老年代的回收器 Serial Old,采用标记-整理算法,其他特性与新生代一致。

Serial+Serial Old 适合客户端场景。

(2)ParNew 收集器

相当于 Serial 的多线程版本,并行回收,年轻代同样采用复制算法与 “Stop the world” 机制,适用于多核 CPU、低延迟环境,推荐应用于服务器场景。

(3)Parallel 收集器

与 ParNew 类似,复制算法、并行回收、“Stop the world” 机制,但是与 ParNew 不同,Parallel 可以控制程序吞吐量大小,也被称为吞吐量优先的垃圾收集器。

与 Serial 类似,Parallel 也有老年代版本,Parallel Old,同样采用标记整理-算法。

Parallel+Parallel Old 非常适用于服务器场景。

(4)CMS 收集器

与 Parallel 的高吞吐对应,CMS 就是为高并发、低延时而生的。采用标记-清除算法、并行回收、“Stop the world”。因为采用了标记-清除算法,会产生大量内存碎片,要慎重使用。

(5)G1 收集器

是一款基于并行、并发、低延时、暂停时间可控的区域化分代式垃圾回收器。

具有革命意义的设计,放弃了堆区年轻代、老年代的划分方案,而是将堆区或分成约 2048 个大小相同的独立 Region 块。

4、GC 的优化方案?

基本的原则就是尽可能地减少垃圾和减少 GC 过程中的开销。其中需要注意,JVM 进行次 GC 的频率很高,但因为 Minor GC 占用时间极短,所以对系统产生的影响不大。更值得关注的是 Full GC 的触发条,具体措施包括以下几个方面:

(1)不要显式调用 System.gc

调用 System.gc 也仅仅是一个请求(建议)。JVM 接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。但即便这样,很多情况下它会触发 Full GC,也即增加了间歇性停顿的次数。

(2)尽量减少临时对象的使用

临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,也就减少了 Full GC 的概率。

(3)对象不用时最好显式置为 NULL

一般而言,为 NULL 的对象都会被作为垃圾处理,所以将不用的对象显式地设为 NULL,有利于 GC 收集器判定垃圾,从而提高了 GC 的效率。

(4)尽量使用 StringBuffer,而不用 String 来累加字符串

由于 String 是常量,累加 String 对象时,并非在一个 String 对象中扩增,而是重新创建新的 String 对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作 “+” 操作时都必须创建新的 String 对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用 StringBuffer 来累加字符串,因 StringBuffer 是可变长的,它在原有基础上进行扩增,不会产生中间对象。

(5)能用基本类型如 int、long,就不用 Integer、Long 对象

基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

(6)尽量少用静态对象变量

静态变量属于全局变量,不会被 GC 回收,它们会一直占用内存。

(7)分散对象创建或删除的时间

集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM 在面临这种情况时,只能进行 Full GC,以回收内存或整合内存碎片,从而增加主 GC 的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。

5、Java 即使有了 GC 也会出现的内存泄漏情况?举例说明。

1. 静态集合类像 HashMap、Vector 等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象 Object 也不能被释放,因为他们也将一直被 Vector 等应用着。

Static Vector v = new Vector;
for(int i = 1; i<100; i++) 
    Object o = new Object;
    v.add(o);
    o = null;

在这个例子中,代码栈中存在 Vector 对象的引用 v 和 Object 对象的引用 o。在 for 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为,GC 在跟踪代码栈中的引用时,会发现v引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管 o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后,Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。

2.各种连接,数据库连接,网络连接,IO 连接等没有显示调用 close 关闭,不被 GC 回收导致内存泄露。

3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。


转自这个地方

以上是关于《Java虚拟机》必知必会的 14 个问题总结(内存模型+GC)的主要内容,如果未能解决你的问题,请参考以下文章

面试前必知必会的二分查找及其变种

优秀Java程序员必知必会的网络基础,看完这一篇就够了!

面试必备-Java虚拟机内存管理必知必会

sql必知必会的简单总结

关于TCP/IP,必知必会的十个问题

VSCode 必知必会的 20 个快捷键!