欢迎光临散文网 会员登陆 & 注册

JVM面试总结(三)

2022-04-27 22:34 作者:吾之利剑  | 我要投稿

    JVM是面试必问的模块,整个JVM我个人感觉可以分为内存模型、类加载机制、gc垃圾回收和性能优化四个大块;

今天主要总结一下gc(Garbage Collector)垃圾回收机制;

1、为什么要进行垃圾回收

    在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象 的内存时,该内存便成为垃圾。

    垃圾回收能自动释放内存空间,减轻编程的负担,JVM的一个系统级线程会自动释放该内存块。垃圾回收意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对 象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。

    事实上,除了释放没用的对象,垃圾回收也可以清除内存记录碎片。由于创建对象和垃圾回收器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。

主要总结:

    1、不进行垃圾回收,可能会导致内存不够用。

    2、除了释放无用的对象,gc也可以清除内存中的记录碎片,进行碎片整理, 将堆内存移到堆的另一端,以便JVM将整理出的内存分配给新的对象。

    3、现在应用程序所对应的业务,用户群体日益强大,没有gc无法保证应用程序的正常运行。


2、简述Java垃圾回收机制

    这个考察对垃圾回收机制整体的了解;一般从以下几个方面回答:

1、哪些内存需要回收;

    Java的内存运行区数据中,程序计数器、虚拟机栈、本地方法栈这3个区域随线程而生亡,其栈帧分配多少内存基本上是在类结构确定下来时就已知的,所以这3个区域的内存分配和回收都具备确定性,而不需要关注如何回收:当方法结束或者线程结束时,内存自然就回收了。

    那么需要垃圾回收的只有Java堆和方法区。一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样,只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾收集器所关注的正是这部分内存该如何管理,我们平时所说的内存分配与回收也仅仅特指这一部分内存。

2、怎么定义垃圾

    JVM的gc工作主要针对的对象是堆内存,在做gc工作之前,首先要判定堆内存中的对象实例是否为垃圾,通常使用以下两种算法来定义:

    1、引用计数算法

    Java在运行时,当有一个地方引用该对象实例,会将这个对象实例加1,引用失效时就减1,JVM在扫描内存时,发现引用计数值为0的则是垃圾对象,计数值大于0的则为活跃对象。

    目前垃圾回收算法,没有采用引用计数算法,原因是在对象互相引用的情况下,无法判定两者是否为垃圾对象。

    2、可达性分析算法(根搜索算法)

    当前主流的商用程序语言的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。这个算法的基本思路就是通过一系列称为“gc Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到gc Roots间没有任何引用链相连,或者用图论的话来说就是从gc Roots到这个对象不可达时,则表明该对象不可能再被使用。

在Java里面,固定可作为gc Roots的对象包括以下几种:

    1、在虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等 .

    2、在方法区中类静态属性引用的对象,例如Java类的引用类型静态变量。

    3、在方法区中常量引用的对象,例如字符串常量池里的引用。

    4、在本地方法栈中JNI(Native方法)引用的对象。

    5、Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

    6、所有被同步锁(synchronized关键字)持有的对象。

    7、反映JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等判定一个类型是否属于“不再被使用的判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了,需要同时满足下面三个条件:

    1、该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。

    2、加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如OSGi、JSP的重加载等,否则通常是很难达成的。

    3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。  

     3、怎么回收垃圾

    即就是垃圾回收算法。

3、JVM的引用类型有哪些?

    1、强引用(StrongReference):最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

    2、软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存流出异常。

    3、弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。

    4、虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

4、垃圾回收算法(如何判断对象是否存活)

1、标记-清除(Mark-Sweep)

    标记-清除算法,和字面意思一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

   它的主要缺点有两个:      

 1、效率问题:标记和清除过程的效率都不高     

  2、空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发gc,甚至Stop The World。


2、标记-复制算法

    标记-复制算法可以解决效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。    这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效(From Survivor和To Survivor使用的就是复制算法。老年代不使用这种算法)。

  它的主要缺点有两个:      

 1、效率问题:在对象存活率较高时,复制操作次数多,效率降低       

 2、空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)

 

3、标记-整理算法

    标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 针对老年代对象的存亡特征,出现了有针对性的“标记-整理”(Mark-Compact)算法,其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

4、分代回收

    分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期(对象熬过垃圾收集过程的次数)是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率目前JVM常用回收算法就是分代回收,年轻代以复制算法为主,老年代以标记整理算法为主

1、新生代(Young Generation)

    1、所有新生成的对象首先都是放在新生代的。新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。

    2、新生代内存按照8: 1: 1的比例分为一个Eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将Eden区存活对象复制到一个survivor0区,然后清空Eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空Eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

    3、当survivor1区不足以存放 Eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full gc,也就是新生代、老年代都进行回收

    4、新生代发生的gc也叫做Minor gc,Minorgc发生频率比较高(不一定等Eden区满了才触发)

2、年老代(Old Generation)

    1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

    2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major gc即Full gc,Full gc发生频率比较低,老年代对象存活时间比较长,存活率标记高。

3、持久代(Permanent Generation)

    用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。一般至少会把Java堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域


5、垃圾收集器

各种回收器,各自优缺点,重点CMS、G1

    1、 Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,但可能会产生较长的停顿,只使用一个线程去回收。

参数设置   -XX:+UseSerialgc 新生代和老年代都用串行收集器

    2、 ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。

    3、 Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。

    4、 Parallel Old收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法

    5、 CMS收集器

    收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。

    从名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为 4 个步骤,包括:

    1、初始标记-短暂,仅仅只是标记一下 gc Roots 能直接关联到的对象,速度很快。

    2、并发标记-和用户的应用程序同时进行,进行 gc Roots 追踪的过程,标记从 gcRoots 开始关联的所有对象开3始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作)。'

    3、重新标记-短暂,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

    4、并发清除由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

参数-XX:+UseConcMarkSweepgc ,表示新生代使用 ParNew,老年代的用 CMS

6、 G1收集器

    G1(Garbage First)是一款主要面向服务端应用的垃圾收集器, 在JDK1.7版本正式启用,是JDK 9以后的默认垃圾收集器。G1宣告取代ParallelScavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。

    G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxgcPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

参数设置 开启参数-XX:+UseG1gc 分区大小-XX:+G1HeapRegionSize

6、JVM中一次完整的gc流程是怎样的

    1、大对象直接进入到老年代 。

    2、小对象先在eden区分配内存,当eden满了后,触发一次Minor gc,清理eden区域。         3、存活下来的对象进入到survivor区域,年龄+1 4、当年龄>15(默认)时进入到老年代,当老年代满了后触发一次Full gc。

7、Full gc会导致什么?

    Full gc的时候除 gc线程外的所有用户线程处于暂停状态,也就是不会有响应了。

    一般Full gc速度很快,毫秒级的,用户无感知。除非内存特别大上百G的,或者Full gc也无法收集到足够内存导致一直Full gc,应用的外在表现就是程序卡死了。

8、什么时候JVM触发gc,如何减少Fullgc的次数

    当 Eden 区的空间耗尽时 Java 虚拟机便会触发一次 Minor gc 来收集新生代的垃圾,存活下来的对象,则会被送到 Survivor 区,简单说就是当新生代的Eden区满的时候触发 Minor gc。

    serial gc 中,老年代内存剩余已经小于之前年轻代晋升老年代的平均大小,则进行 Full gc。而在 CMS 等并发收集器中则是每隔一段时间检查一下老年代内存的使用量,超过一定比例时进行 Full gc 回收。

可以采用以下措施来减少Full gc的次数:

  1. 增加方法区的空间;

  2. 增加老年代的空间;

  3. 减少新生代的空间;

  4. 禁止使用System.gc()方法;

  5. 使用标记-整理算法,尽量保持较大的连续内存空间;

  6. 排查代码中无用的大对象。

注意:

gc的回收时间是不确定的,即使你显示的调用的System.gc()。因为和线程优先级有关

使用了finalize()方法之后,gc是在这个方法执行之后的下一次进行垃圾的回收。

9、扩展

1、JVM查看gc命令

2、如果频繁老年代回收怎么分析解决?

3、重点考察点是分代回收算法,CMS收集器和G1收集器

参数:

-Xms 初始堆大小。如:-Xms256m

-Xmx 最大堆大小。如:-Xmx512m

-Xmn 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%

-Xss JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。-XX:NewRatio 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3

-XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10

-XX:PermSize 永久代(方法区)的初始大小

-XX:MaxPermSize 永久代(方法区)的最大值

-XX:+PrintgcDetails 打印 gc 信息

-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用



以上内容仅供参考,请合理利用搜索引擎!




JVM面试总结(三)的评论 (共 条)

分享到微博请遵守国家法律