最近都没怎么写博客了,也在思考写啥,是教程还是原理分析,总之是自己太懒了。然后有朋友面试,我自己也去看了下面试题,发现jvm这块好多不会,就好好整理了下,大部分是手动敲的,还有的是c过来的。不过都是精髓,纯文字还是不好记,但是我也会了一句话,要么忍,要么狠,要么滚。
先看题
1.运行时数据区域有哪些,各自的作用。
2.怎样判断一个对象已经死去。
3.说说强引用,软引用,弱引用,虚引用以及他们之间和gc的关系。
4.对象如何晋升到老年代。
5.什么是类加载器的双亲委派模型。
6.有哪些哪里收集算法,为什么用这些算法。
7.什么是java内存模型,用过那些垃圾回收器。
8.jvm垃圾回收机制,何时触发MinorGC。
9.新生代中Eden区和Survivor区的默认比例是多少,为什么是这个比例。
10.jvm调优经验。
下面是我花了三天时间整理的,题目顺序可能不一样,不过看完就能对的上。
1.程序计数器
线程私有,记录线程执行字节码的行号,各线程之间计数器互不影响,cpu同时处理多个线程时,线程怎样切换回来继续执行就是计数器记录了各线程自己上次执行的内存地址。比如,你周末在公司加班,你女朋友给你打电话,你接了个电话,回来继续加班,接着之前的工作继续工作。当然对于没有女朋友的,这个不成立。
2.虚拟机栈
同样线程私有,和线程的生命周期相同,管理所有的线程,线程执行方法会创建栈帧,用来存储当前方法的局部变量表,操作数帧,动态链接,方法出口等。方法调用对应入栈到出栈,先进后出。如果线程请求深度大于虚拟机允许深度出现嵌套循环,死循环将抛出stackoverflowerror异常。若虚拟机栈无法申请到足够内存,抛出outofmemoryerror异常。
3.本地方法栈
和虚拟机栈类似,虚拟机栈为java方法服务,本地方法栈调用本地方法,方法前面有native修饰,由非java语言实现。
4.java堆
jvm内存管理最大的区域,垃圾回收最活跃的的区域。堆数据属于线程共享,存放对象实例,堆细分又可以分为新生代(eden,from,to),老年代.有点像现在的共享经济,都可以使用,但是又分很多类别。
5.方法区
同堆一样,属于线程共享。存放虚拟机加载的类信息,静态变量,常量等。1.8版本以前叫永久代,1.8以后叫元空间。元空间不再是jvm内存一部分,是直接存在于本机内存,而常量池移到堆中。就像深圳,以前只是小渔村,现在发达了,改名字了,而且地区也有特权。
最近上海搞了个垃圾分类,判断垃圾属于哪种垃圾,jvm在垃圾回收之前需要判断对象是否死亡,怎么判断呢?
1.引用计数算法。
实现简单,高效。给对象添加引用计数器,当本地方法引用它,计数器加一,引用失效减一。计数器为0表示死亡。缺点,无法解决循环引用的问题。比较直观,就像生死簿。
2.可达性算法。
也叫搜索路径算法,从GC ROOTS的对象为起点开始向下搜索,走过的路径叫引用链,当对象没有引用链时这个对象就不可用,也就是死亡了。就好比你对公司还有价值,那么你继续存在,你创造的价值对公司没发展,那也是白搭,但是你创造的价值需要经过审核才知道。
GCROOTS包含下面几种:
①虚拟机栈引用的对象
②方法区中类静态属性信用的对象
③方法区常量引用的对象
④native方法引用的对象
就算可达性算法不可达,不意味对象会被回收。至少需要标记两次,才是真死亡。第一次
筛选有没引用链,第二次是否要执行finalize方法。当对象覆盖了finalize方法,还没被执行,在方法中获得引用,这个对象也不会被回收,反之会被回收。
当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止。如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委托给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种加载器环境中都是同一个类。相反,如果没有使用双亲委托模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
其实很好理解就是,你要加载一个类先看你爸爸加载过同名的没,如果有爷爷,先问你爷爷。
垃圾收集器 1.Serial收集器,单线程收集器,串行回收,收集时会停止所有线程工作(STW),使用复制收集算法。虚拟机运行client模式的默认收集器。特点是cpu利用率高,但用户等待时间长。适合小型应用。
2.ParNew收集器 属于Serial的多线程版本,对应的是server模式的新生代收集器。
3.Parallel Scavenge 收集器 属于并行收集器,使用复制算法,它是以吞吐量最大化为目标但是相对的STW较长。适用于大型应用,科学家计算,大规模数据采集。
4.CMS收集器 它是以短暂停顿时间(STW)为目标,提高响应速度,比如证券交易。使用的是标记清楚算法,运行时会产生空间碎片,适合大型服务器。
5.Serial old收集器 这个也可以归为第一种,不同的是使用标记整理算法,老年代的收集器。
6.Parallel old收集器 可以归为第三类,老版本的吞吐量优先收集器,多线程,使用标记整理算法。
总结,收集器可分成两类,串行和并行。收集器针对的应用场景不同,对象不同,提供了不同的版本,对应算法也不一样。要提高吞吐量就要牺牲停顿时间,反之亦然。没有最好的,只有最适合的。
无论是用计数算法还是可达性算法判断对象是否死亡,都和对象的"引用"有关。
1.强引用
类似于 a a=new a()出来的对象,只要存在,收集器永远不会将它回收。即使是发生outofmemory异常,也不会回收。
2.软引用
描述一些有用倒不是必要的对象,在系统即将发生内存泄漏异常时出现,内存不足时,将它们列为回收范围,进行二次回收,如果回收后依旧没足够内存,则会抛出内存溢出异常。
3弱引用
同样是描述非必需的对象,对象只能存活到在下一次垃圾回收之前,无论内存是否足够,回收时都会被干掉。
4.虚引用
就像它的名字一样,形同虚设。只是起到一个通知作用,不会对生存时间有任何影响。
1.大对象会直接进去老年代,大对象的判断依据是大于虚拟机设置的xx:PretenureSizeThreadhold这个参数。这样的好处是避免在伊甸园区和幸存区发生大量的内存复制。
2.长期存活的对象
我们知道虚拟机给每个对象都定义了一个年龄,对象在经历一次Minor GC时会从伊甸园区到幸存区,年龄会加一。下次Minor GC又从幸存区到伊甸园区,年龄再次加一,这样进行下去年龄到达默认15xx:MaxTenuringThreshold时,就会晋升到老年代。
3.动态对象年龄判断
不一定非要达到15这个年龄才会进入老年代,当幸存区相同年龄对象总和达到幸存区空间一半时,所有年龄大于或者等于年龄的对象会进入老年代。
1.Minor GC触发
伊甸园区内存不够时会触发MinorGC。
2.Full GC触发
①显示调用system.gc时。
②老年代或者方法区空间不够
③在进行Minor GC前判断老年代最大连续空间是否大于新生代所有对象总和,并且判断是否允许担保失败,不允许就触发。允许,再判断最大连续空间是否大于晋升到老年代对象的平均大小,不大于,触发。
1.标记-清除算法
算法
先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法有两个问题:
标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除,有没有联想到可达性分析算法?显然遍历的效率是很低的
会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。
2.复制算法
为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。
优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:算法的代价是将内存缩小为了原来的一半,未免太高了一点。
.现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。
当然,90%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时(例如,存活的对象需要的空间大于剩余一块Survivor的空间),需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
3.标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,二是让所有的存活对象都向一端移动,然后清理掉边界外的内存。
目前商用虚拟机都使用“分代收集算法”,所谓分代就是根据对象的生命周期把内存分为几块,一般把Java堆中分为新生代和老年代,这样就可以根据对象的“年龄”选择合适的垃圾回收算法。
新生代:“朝生夕死”,存活率低,使用复制算法。
老年代:存活率较高,使用“标记-清除”算法或者“标记-整理”算法。
降低Full GC 频次,一天1-2次,尽量控制在晚上,可以选择重启服务器或者定时任务出发Full GC。
确保大多数对象“朝生夕死”
提高大对象的进入门槛(-XX:MaxTenuring=15)
2、针对JVM堆的设置,JVM初始堆内存分配有-Xms指定,默认是物理内存的1/64;
最大分配堆内存有-Xmx指定,默认是物理内存的1/4;当堆内存小于40%时,JVM就会自动增加,直到最大值。当空余堆内存大于70%时,JVM就会自动减少,直到最小值。因此,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值;
3、配置年轻代(Xmn)的值,持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
4、设置个线程的堆栈大小Xss,每个线程默认堆栈大小为1M,可根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数也是有限制的,不能无限生成,经验值在3000~5000左右。
5、回收器的选择,JVM给了三种选择:串行收集器、并行收集器、并发收集器,JVM会根据当前系统配置进行判断,自动选择。但我们可以对一些参数进行设置。串行收集器只适用于小数据量的情况,一般不用处理;可以配置年轻代使用并发收集器,老年代使用并行收集器;如果响应时间有限,就选择并发收集器,尽可能设大年轻代;
如果吞吐量优先就选择并行收集器,也尽可能设大年轻代;
6、禁用Tomcat的DNS查询。(当 Web 应用程序记录客户端的信息时,它也会记录客户端的 IP 地址或者通过域名服务器查找机器名转换为 IP 地址。 DNS 查询需要占用网络,并且可能从很多很远的服务器或者不起作用的服务器上去获取对应的 IP, 这样会消耗一定的时间。为了消除 DNS 查询对性能的影响,可以关闭 DNS 查询。方法是修改 server.xml 文件中的 enableLookups 参数值。)
7、线程数配置:Tomcat连接数过大可能引起的死机。所以,可以根据并发量在Tomcat的server.xml中修改他的最大线程数、初始化线程数等参数。(一般也就估计这配置,小了就配大点。也没有具体评估过)
我们在项目中,一般也就是项目出现问题以后,再去优化毕竟有的问题是不可预见的。常见的oom异常如下:Tomcat在年老代溢出(java.lang.OutOfMemoryError: Java heap space)、持久代溢出(java.lang.OutOfMemoryError: PermGen space)、堆栈溢出(java.lang.StackOverflowError)、线程溢出(Fatal: Stack size too smal)、内存溢出(java.lang.OutOfMemoryError: unable to create new native thread)是会抛出不同溢出。根据溢出去进行修改。
十个问题已经解答,有人问你这不九个答案吗,其实有一个答案把两个问题都答完了。
参考博客:https://blog.csdn.net/langyichen/article/details/89083448
https://blog.csdn.net/wangxiaotongfan/article/details/82389881