总结
无论是哪家公司,都很重视高并发高可用的技术,重视基础,重视JVM。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。
最后我整理了一些面试真题资料,技术知识点剖析教程,还有和广大同仁一起交流学习共同进步,还有一些职业经验的分享。
1 我们new出来的对象一般是先到伊甸区(Eden)里申请空间,如果伊甸区满了(当前从伊甸区里无法申请到空间),那么会把伊甸区里还存活的对象复制到其中的一个Survivor区里。这里其实已经有个隐含的回收流程,当我们把伊甸区存活的对象复制到Survivor区时,已经把其中无用的对象回收了。
2 接下来当伊甸区和其中的一个Survivor区都满了时,那么会把伊甸区和其中一个Survivor里的存活对象再复制到另外个Survivor区里,这里同样隐含着一次回收流程。
3 如果年轻代的空间都满了(即无法从伊甸区和两个Survivor区里申请到对象),那么虚拟机会把年轻代里还存活的对象复制到年老代里。
4 当年老代再满时(不会再复制到持久代了),会启动Full GC,对年轻代,年老代和持久代进行全面地回收了,这个就需要耗费较长的时间了。
在上述的回收流程中,其实包含着两类回收机制。
第一类是轻量级回收(Minor GC)。在年轻代里的回收流程都是属于这种,比如我们new出来的一个对象在Eden区申请空间失败,就会触发这类GC。
在这类回收流程里,一般会用到一种效率相对较高的标记复制算法(Mark Copy),这种算法不涉及对无用对象的删除,只是把标记存活的对象从一个内存区拷贝到另一个内存区。
第二类是刚才已经提到的重量级的Full GC流程,如下的四种情况会触发这种GC。
1 年老代(Tenured)被写满。
2 持久代被写满。
3 程序员显式地调用了System.gc()方法。
4我们可以通过java命令分配堆空间的运行策略,比如可以设置年轻代和年老代的比例,如果虚拟机监控到上次GC后,这种运行策略发生变化,也会触发Full GC。
这里讲一个可能会导致误解的知识点,程序员还是可以通过System.gc()来建议虚拟机启动垃圾回收,但调用这个方法后,虚拟机一般并不会直接启动,而是会过会找个合适的时间点。这和我们之间给出的“程序员无法通过代码回收内存”说辞并不矛盾。
不重视内存性能可能会导致的后果
===============
一般来讲,轻量级回收的代价大家可以忽略不计,但大家一定得重视Full GC,这里来举个例子来说明这类GC对系统的影响,从中大家可以看下不重视内存性能可能会导致的后果。
在某项目里有个批处理程序,每天下午2点跑,要做的业务是,从每天都会更新的xml文件里读取数据,并把它们插入到数据库里。常规情况下是2点半结束,但在某天,它在下午5点时还在跑,从日志上看,卡住了,不继续运行,而且也没报异常(这个是最令人担忧的,因为我们无法获知异常的原因)。
结果从内存监控上一看,这个程序申请了1G内存,但由于代码没写好,那天正好引发一段平时走不到的流程,从而导致内存使用量持续上升,最后停留在1G的水平。
这时由于年轻代和年老代都满了,就触发了Full GC,在执行Full GC时,会导致”Stop the World”情况发生,也就是说虚拟机终止了所有Java程序,专门做Full GC,这就是卡住的原因。
这个例子倒不是让大家尽量避免Full GC,因为如果代码没写好或者内存分配策略不对,Full GC导致的Stop The World现象迟早会发生。这个例子的作用是让大家一定要重视后继我们讲述的内存性能优化内容,否则有很大概率会发生类似的“卡住”的问题,或者即使不“卡“,也会报出OOM内存溢出异常,这也会造成“程序运行终止“这样的严重问题。
判断对象可回收的依据
==========
不论是轻量级回收还是Full GC,我们都无法回避这样一个问题:Java虚拟机如何判断一个对象可以被回收?
标准非常简单,当某个对象上没有强引用时,该对象就可以被回收。不过,在绝大多数的场景里,当某对象上的最后一个强引用被撤去后,该对象不会被立即回收,而是会在下次启动垃圾回收机制时被回收。
在JDK的早期版本里,是用“引用计数法”来判断对象上是否有强引用,具体来讲,当一个对象上有一个强引用时,把该对象的引用计数值加1,反之则减1。
-
String a = new String(“123”); //包含123内容的对象上的引用数加1
-
a = null; //引用数减1
这里请大家区分“引用”和“值”的差别。比如通过上述代码的第1行,我们会在堆空间里分配一块空间,假设内存首地址是1000,在其中存放了123这个内容,而且通过一个引用a指向这块空间,这时,1000号内存的引用数是1。
在第2行里,我们并不是把1000号内存里的值设置成null(初学者往往会有这样错误的理解),而是把a这个引用指向null。这时,虽然1000号内存的值没变,但没有引用指向它了,它的引用计数值就变0了。 在这种情况下,下次垃圾回收机制启动时,1000号内存就会被回收。
引用计数法的好处是简单,但缺点是无法回收循环引用的对象,比如a引用指向b,b指向c,c再指向a,在这种情况下,哪怕它们游离于主程序之外了(程序不再用到它们了),abc三个引用的计数值都是1,这样它们就始终无法被回收。
正是因为有这样的原因,在后继的JDK版本里,引入了“根搜索算法”(Tracing Collector)。这个算法的效果如下图所示。
在这个算法里,将从一个根节点(GC ROOT)开始,寻找它所对应的引用节点,找到这个节点后,继续寻找该节点的引用节点,以此类推。这样当所有的引用节点都搜索完毕后,剩下的就是没有被引用的节点,也就是可以回收的节点。比如在上图9.4里,从根节点里能找到a,b,c和d这四个节点,而u1和u2这两个节点属于不可达,也就是可以被回收。
具体来讲,可作为GC Root的对象有如下四个。第一,虚拟机栈中引用的对象。第二,方法区中静态属性引用的对象。第三, 方法区中常量引用的对象。第四,本地方法栈中引用的对象。
深入了解finalize方法
==============
finalize()是Object类里的protected类型的方法,子类(所有类都是Object的子类)可以通过覆盖这个方法来实现回收前的资源清理工作。和这个方法相关的流程如下所述。
1 Java虚拟机一旦通过刚才提到的“根搜索算法”判断出某对象处于可回收状态时,会判断该对象是否重写了Object类的finalize方法,如果没,则直接回收。
2如重写过finalize方法,而且未执行过该方法,则把该对象其放入F-Queue队列,另个线程会定时遍历F-Queue队列,并执行该队列中各对象的finalize方法。
3 finalize方法执行完毕后,GC会再次判断该对象是否可被回收,如果可以,则进行回收,如果此时该对象上有强引用,则该对象“复活”,即处于“不可回收状态”。
通过下面的FinalizeDemo.java,我们来演示下通过finalize方法复活对象的做法。
1 public class FinalizeDemo {
最后
金三银四马上就到了,希望大家能好好学习一下这些技术点
学习视频:
大厂面试真题:
Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)收录**