1、Java学习手册:Java基础知识点
2、Java学习手册:Java面向对象面试问题
3、Java学习手册:Java集合、泛型面试问题
4、Java学习手册:Java并发与多线程面试问题
5、Java学习手册:Java虚拟机面试问题
6、Java学习手册:Java IO面试问题
7、Java学习手册:Java反射机制面试问题
8、Java学习手册:Java网络编程面试问题
9、Java学习手册:Java异常面试问题
10、Java学习手册:Java设计模式面试问题
11、Java学习手册:Java数据库面试问题
置顶、Java内存区域划分模型
内容参考:《深入理解Java虚拟机》
Java虚拟机规范将方法区描述为堆的一个逻辑部分,与我们粗略的内存模型的对应关系如下:
堆 = 堆 + 方法区
栈 = 虚拟机栈 + 本地方法栈
(1)程序计数器
线程隔离,即每个线程都有自己的程序计数器,并且互不影响。分为两种情况,当线程正在执行的是一个java方法,它的作用是作为字节码的行号指示器,指向下一条需要执行的指令。当线程正在执行的是一个Native方法,那么它的值为空(Undefined)。java虚拟机规范中唯一没有定义OOM异常的内存区域。
(2)Java虚拟机栈
线程隔离,生命周期与线程相同。它描述的是java方法执行的内存模型,每一个java方法执行的时候都会产生一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。当进入一个方法时,栈帧的大小是编译器确定的,运行时不会改变其大小。当虚拟机栈不可扩展的时候,可能抛出StackOverflowError异常,反之,可能抛出OOM异常。
(3)本地方法栈
与Java虚拟机栈功能一致,只不过本地方法栈是针对Native方法的。同样在虚拟机规范中定义了StackOverflowError和OOM两种异常。
(4)Java堆
这个应该是我们最熟悉的区域了,只要是用到new关键字创建的对象都会进入到这个区域,包括对象,数组。堆还能进一步划分,比如按照内存回收的角度来看,堆可以进一步划分为新生代(Eden+Survivor(from+to))和老年代。按照内存分配的角度来看,堆可以进一步划分为多个线程私有的分配缓存区,即TLAB(Thread Local Allocation Buffer)。这种进一步的划分是为了更高效地回收和分配内存。java虚拟机规范中定义了OOM异常。
(5)方法区
这个区域用于存储被加载的类的信息,常量,静态变量以及即时编译器编译后的代码等数据。虚拟机规范中定义了OOM异常。还需要注意的一点是,HotSpot虚拟机中的方法区被很多人称之为“永生代”,这是因为HotSpot的开发团队将分代收集算法运用到了方法区,但是这并不是必须的。
一、JVM加载class文件的原理机制
二、垃圾回收(GC)
注:哪些情况下的对象会被垃圾回收机制处理掉?
1、所有实例都没有活动线程访问。
2、没有被其他任何实例访问的循环引用实例。
3、Java 中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。
要判断怎样的对象是没用的对象。这里有2种方法:
1、采用引用-计数的方法:
给内存中的对象给打上标记,对象被引用一次,计数就加1,引用被释放了,计数就减一,当这个计数为0的时候,这个对象就可以被回收了。当然,这也就引发了一个问题:循环引用的对象是无法被识别出来并且被回收的。所以就有了第二种方法:
2、采用可达性算法:
从一个根(GC Roots)出发,搜索所有的可达对象,这样剩下的那些对象就是需要被回收的对象。
在Java中,可以作为GC Roots的对象有以下几种:
- 1、虚拟机栈中引用的对象
- 2、方法区类静态属性引用的对象
- 3、方法区常量池引用的对象
- 4、本地方法栈JNI引用的对象
注:
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象并不一定会被回收。当一个对象不可达GC Root时,这个对象并不会立马被回收,而是处于一个死缓的阶段,若要被真正的回收需要经历两次标记 ,如果对象在可达性分析中没有与GC Root的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为F-Queue的对队列中,虚拟机会触发一个Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成F-Queue队列一直等待,造成了内存回收系统的崩溃。GC对处于F-Queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。
三、内存泄漏
四、堆内存、栈内存、方法区
- 堆/Heap
JVM管理的内存叫堆;在32Bit操作系统上有4G的限制,⼀般来说Windows下为2G,⽽Linux 下为3G;64Bit的就没有这个限制。
JVM初始分配的内存由-Xms指定,默认是物理内存的1/64但⼩于1G。
JVM最⼤分配的内存由-Xmx指定,默认是物理内存的1/4但⼩于1G。
默认空余堆内存⼩于40%时,JVM就会增⼤堆直到-Xmx的最⼤限制,可以由 -XX:MinHeapFreeRatio=指定。
默认空余堆内存⼤于70%时,JVM会减少堆直到-Xms的最⼩限制,可以由-XX:MaxHeapFreeRatio=指定。
服务器⼀般设置-Xms、-Xmx相等以避免在每次GC后调整堆的⼤⼩,所以上⾯的两个参数没啥⽤。
注1:Java堆的结构是什么样子的?
JVM的堆是运⾏时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由⾃动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的。存活的对象是应⽤可以访问的,不会被垃圾回收。死亡的对象是应⽤不可访问尚且还没有被垃圾收集器回收掉的对象。⼀直到垃圾收集器把这些对象回收掉之前,他们会⼀直占据堆内存空间。
注2:Java中的数组是存储在堆上还是栈上的?
在Java中,数组同样是一个对象,所以对象在内存中如何存放同样适用于数组。
所以,数组的实例是保存在堆中,而数组的引用是保留在栈上的。
五、值传递、引用传递
六、基本回收算法
1、 引⽤计数(Reference Counting)——已弃用
⽐较古⽼的回收算法。原理是此对象有⼀个引⽤,即增加⼀个计数,删除⼀个引⽤则减少⼀个计数。垃圾回收时,只⽤收集计数为0的对象。此算法最致命的是⽆法处理循环引⽤的问题。
2、标记-清除(Mark-Sweep)
此算法执⾏分两阶段。第⼀阶段从引⽤根节点开始标记所有被引⽤的对象,第⼆阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应⽤,同时,会产⽣内存碎⽚。(具体内容见十一节)
3、复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使⽤其中⼀个区域。垃圾回收时,遍历当前使⽤区域,把正在使⽤中的对象复制到另外⼀个区域中。此算法每次只处理正在使⽤中的对象,因此复制成本⽐较⼩,同时复制过去以后还能进⾏相应的内存整理,不过出现“碎⽚”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
4、标记-整理(Mark-Compact)
此算法结合了 “标记-清除”和“复制”两个算法的优点。也是分两阶段,第⼀阶段从根节点开始标记所有被引⽤对象;第⼆阶段,遍历整个堆,清除未标记对象并且把存活对象 “压缩”到堆的其中⼀块,按顺序排放。此算法避免了“标记-清除”的碎⽚问题,同时也避免了“复制”算法的空间问题。
5、增量收集(Incremental Collecting)——已弃用
实施垃圾回收算法,即:在应⽤进⾏的同时进⾏垃圾回收。不知道什么原因JDK5.0中的收集器没有使⽤这种算法的。
6、分代(Generational Collecting)
基于对对象⽣命周期分析后得出的垃圾回收算法。把对象分为年⻘代、年⽼代、持久代,对不同⽣命周期的对象使⽤不同的算法(上述⽅式中的⼀个)进⾏回收。现在的垃圾回收器(从J2SE1.2开始)都是使⽤此算法的。
(1) Young(年轻代)- 复制收集算法
年轻代分三个区。⼀个Eden区,两个 Survivor区。⼤部分对象在Eden区中⽣成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的⼀个),当这个 Survivor区满时,此区的存活对象将被复制到另外⼀个Survivor区,当这个Survivor去也满了的时候,从第⼀个Survivor区复制过来的并且此时还存活的对象,将被复制“年⽼区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同⼀个区中可能同时存在从Eden复制过来对象,和从前⼀个Survivor复制过来的对象,⽽复制到年⽼区的只有从第⼀个Survivor去过来的对象。⽽且,Survivor区总有⼀个是空的。
(2) Tenured(年⽼代)- 标记整理算法
年⽼代存放从年轻代存活的对象。⼀般来说年⽼代存放的都是⽣命期较⻓的对象。年轻代的对象如果能够挺过数次收集,就会进⼊⽼⼈区。⽼⼈区使⽤标记整理算法。因为⽼⼈区的对象都没那么容易死的,采⽤复制算法就要反复的复制对象,很不合算,只好采⽤标记清理算法,但标记清理算法其实也不轻松,每次都要遍历区域内所有对象,所以还是没有免费的午餐啊。-XX:MaxTenuringThreshold= 设置熬过年轻代多少次收集后移⼊⽼⼈区,CMS中默认为0,熬过第⼀次GC就转⼊,可以⽤-XX:+PrintTenuringDistribution 查看。
(3) Perm(持久代/永生代)——已弃用,Java8采用了Mataspace
⽤于存放静态⽂件,如今Java类、⽅法等。持久代对垃圾回收没有显著影响,但是有些应⽤可能动态⽣成或者调⽤⼀些class,例如Hibernate等,在这种时候需要设置⼀个⽐较⼤的持久代空间来存放这些运⾏过程中新增的类。持久代⼤⼩通过-XX:MaxPermSize=进⾏设置。注意Spring,Hibernate这类喜欢AOP动态⽣成类的框架需要更多的持久代内存。⼀般情况下,持久代是不会进⾏GC的,除⾮通过 -XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled进⾏强制设置。
注:
(1)Java 8取消了永生代,采用了Metaspace。
(2)新生代内存不够时发生Micro GC也叫Young GC,JVM内存不够时,会发生Full GC或者Major GC。
当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC。Minor Gc通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生Gc的频率较高,回收速度比较快;Full Gc发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。
七、GC类型
GC有两种类型:Scavenge GC和Full GC。
1、 Scavenge GC
⼀般情况下,当新对象⽣成,并且在Eden申请空间失败时,就好触发Scavenge GC,堆Eden区域进⾏GC,清除⾮存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。
2、 Full GC
对整个堆进⾏整理,包括Young、Tenured和Perm。Full GC⽐Scavenge GC要慢,因此应该尽可能减少FullGC。有如下原因可能导致Full GC:
- Tenured被写满
- Perm域被写满
- System.gc()被显⽰调⽤
- 上⼀次GC之后Heap的各域分配策略动态变化
八、垃圾回收器
⽬前的收集器主要有三种:串⾏收集器、并⾏收集器、并发收集器。
1、 串⾏收集器
使⽤单线程处理所有垃圾回收⼯作,因为⽆需多线程交互,所以效率⽐较⾼。但是,也⽆法使⽤多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以⽤在⼩数据量(100M左右)情况下的多处理器机器上。可以使⽤-XX:+UseSerialGC打开。
2、并⾏收集器
- 对年轻代进⾏并⾏垃圾回收,因此可以减少垃圾回收时间。⼀般在多线程多处理器机器上使⽤。使⽤-XX:+UseParallelGC.打开。并⾏收集器在J2SE5.0第六6更新上引⼊,在Java SE6.0中进⾏了增强–可以堆年⽼代进⾏并⾏收集。如果年⽼代不使⽤并发收集的话,是使⽤单线程进⾏垃圾回收,因此会制约扩展能⼒。使⽤-XX:+UseParallelOldGC打开。
- 使⽤-XX:ParallelGCThreads=设置并⾏垃圾回收的线程数。此值可以设置与机器处理器数量相等。
- 此收集器可以进⾏如下配置:
- 最⼤垃圾回收暂停:指定垃圾回收时的最⻓暂停时间,通过-XX:MaxGCPauseMillis=指定。为毫秒.如果指定了此值的话,堆⼤⼩和垃圾回收相关参数会进⾏调整以达到指定值。设定此值可能会减少应⽤的吞吐量。
- 吞吐量:吞吐量为垃圾回收时间与⾮垃圾回收时间的⽐值,通过-XX:GCTimeRatio=来设定,公式为1/
1+N)。例如,-XX:GCTimeRatio=19时,表⽰5%的时间⽤于垃圾回收。默认情况为99,即1%的时间⽤于垃圾回收。
3、并发收集器
可以保证⼤部分⼯作都并发进⾏(应⽤不停⽌),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求⽐较⾼的中、⼤规模应⽤。使⽤-XX:+UseConcMarkSweepGC打开。
- 并发收集器主要减少年⽼代的暂停时间,他在应⽤不停⽌的情况下使⽤独⽴的垃圾回收线程,跟踪可达对象。在每个年⽼代垃圾回收周期中,在收集初期并发收集器会对整个应⽤进⾏简短的暂停,在收集中还会再暂停⼀次。第⼆次暂停会⽐第⼀次稍⻓,在此过程中多个线程同时进⾏垃圾回收⼯作。
- 并发收集器使⽤处理器换来短暂的停顿时间。在⼀个N个处理器的系统上,并发收集部分使⽤K/N个可⽤处理器进⾏回收,⼀般情况下1<=K<=N/4。
- 在只有⼀个处理器的主机上使⽤并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
- 浮动垃圾:由于在应⽤运⾏的同时进⾏垃圾回收,所以有些垃圾可能在垃圾回收进⾏完成时产⽣,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器⼀般需要20%的预留空间⽤于这些浮动垃圾。
- Concurrent Mode Failure:并发收集器在应⽤运⾏时进⾏收集,所以需要保证堆在垃圾回收的这段时间有⾜够的空间供程序使⽤,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发⽣“并发模式失败”,此时整个应⽤将会暂停,进⾏垃圾回收。
- 启动并发收集器:因为并发收集在应⽤运⾏时进⾏收集,所以必须保证收集完成之前有⾜够的内存空间供程序使⽤,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆时开始执⾏并发收集
4、 ⼩结
- 串⾏处理器:
–适⽤情况:数据量⽐较⼩(100M左右);单处理器下并且对响应时间⽆要求的应⽤。
–缺点:只能⽤于⼩型应⽤ - 并⾏处理器:
–适⽤情况:“对吞吐量有⾼要求”,多CPU、对应⽤响应时间⽆要求的中、⼤型应⽤。举例:后台处理、科学计算。
–缺点:应⽤响应时间可能较⻓ - 并发处理器:
–适⽤情况:“对响应时间有⾼要求”,多CPU、对应⽤响应时间有较⾼要求的中、⼤型应⽤。举例:Web服务器/应⽤服务器、电信交换、集成开发环境。
5、并发与并行的区别
并⾏(Parallel)与并发(Concurrent)仅⼀字之差,但体现的意思却完全不同。
并⾏:指多条垃圾收集线程并⾏,此时⽤⼾线程是没有运⾏的;
并发:指⽤⼾线程与垃圾收集线程并发执⾏,程序在继续运⾏,⽽垃圾收集程序运⾏于另⼀个CPU上。
并发收集⼀开始会很短暂的停⽌⼀次所有线程来开始初始标记根对象,然后标记线程与应⽤线程⼀起并发运⾏,最后⼜很短的暂停⼀次,多线程并⾏的重新标记之前可能因为并发⽽漏掉的对象,然后就开始与应⽤程序并发的清除过程。可⻅,最⻓的两个遍历过程都是与应⽤程序并发执⾏的。
串⾏标记清除是等年⽼代满了再开始收集的,⽽并发收集因为要与应⽤程序⼀起运⾏,如果满了才收集,应⽤程序就⽆内存可⽤,所以系统默认68%满的时候就开始收集。内存已设得较⼤,吃内存⼜没有这么快的时候,可以⽤ -XX:CMSInitiatingOccupancyFraction=适当增⼤该⽐率。
九、常⻅配置汇总
1、堆设置
- -Xms:初始堆⼤⼩
- -Xmx:最⼤堆⼤⼩
- -XX:NewSize=n:设置年轻代⼤⼩
- -XX:NewRatio=n:设置年轻代和年⽼代的⽐值。如:为3,表⽰年轻代与年⽼代⽐值为1:3,年轻代占整个年轻代年
⽼代和的1/4 - -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的⽐值。注意Survivor区有两个。如:3,表⽰Eden:
Survivor=3:2,⼀个Survivor区占整个年轻代的1/5 - -XX:MaxPermSize=n:设置持久代⼤⼩
2、收集器设置
- -XX:+UseSerialGC:设置串⾏收集器
- -XX:+UseParallelGC:设置并⾏收集器
- -XX:+UseParalledlOldGC:设置并⾏年⽼代收集器
- -XX:+UseConcMarkSweepGC:设置并发收集器
3、垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
4、并⾏收集器设置
- -XX:ParallelGCThreads=n:设置并⾏收集器收集时使⽤的CPU数。并⾏收集线程数。
- -XX:MaxGCPauseMillis=n:设置并⾏收集最⼤暂停时间
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运⾏时间的百分⽐。公式为1/(1+n)
5、并发收集器设置
- -XX:+CMSIncrementalMode:设置为增量模式。适⽤于单CPU情况。
- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集⽅式为并⾏收集时,使⽤的CPU数。并⾏收集线程数。
十、调优总结
1、年轻代的痛
由于对年轻代的复制收集,依然必须停⽌所有应⽤程序线程,原理如此,只能靠多CPU,多收集线程并发来提⾼收集速度,但除⾮你的 Server独占整台服务器,否则如果服务器上本⾝还有很多其他线程时,切换起来速度就很慢。 所以,搞到最后,暂停时间的瓶颈就落在了年轻代的复制算法上。
因此Young的⼤⼩设置挺重要的,⼤点就不⽤频繁GC,⽽且增⼤GC的间隔后,可以让多点对象⾃⼰死掉⽽不⽤复制了。但Young增⼤时,GC 造成的停顿时间攀升得⾮常恐怖,据某⼈的测试结果显⽰:默认8M的Young,只需要⼏毫秒的时间,64M就升到90毫秒,⽽升到256M时,就要到 300毫秒了,峰值还会攀到恐怖的800ms。主要原因在于复制算法要等Young满了才开始收集,开始收集就要停⽌所有线程。
2、年轻代⼤⼩选择
- 响应时间优先的应⽤:尽可能设⼤,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发⽣的频率也是最⼩的。同时,减少到达年⽼代的对象。
- 吞吐量优先的应⽤:尽可能的设置⼤,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并⾏进⾏,⼀般适合8CPU以上的应⽤。
3、年⽼代⼤⼩选择
- 响应时间优先的应⽤:年⽼代使⽤并发收集器,所以其⼤⼩需要⼩⼼设置,⼀般要考虑并发会话率和会话持续时间等⼀些参数。如果堆设置⼩了,可以会造成内存碎⽚、⾼回收频率以及应⽤暂停⽽使⽤传统的标记清除⽅式;如果堆⼤了,则需要较⻓的收集时间。最优化的⽅案,⼀般需要参考以下数据获得:
o 并发垃圾收集信息
o 持久代并发收集次数
o 传统GC信息
o 花在年轻代和年⽼代回收上的时间⽐例
减少年轻代和年⽼代花费的时间,⼀般会提⾼应⽤的效率 - 吞吐量优先的应⽤:⼀般吞吐量优先的应⽤都有⼀个很⼤的年轻代和⼀个较⼩的年⽼代。原因是,这样可以尽可能回收掉⼤部分短期对象,减少中期的对象,⽽年⽼代尽存放⻓期存活对象。
4、 较⼩堆引起的碎⽚问题
因为年⽼代的并发收集器使⽤标记、清除算法,所以不会对堆进⾏压缩。当收集器回收时,他会把相邻的空间进⾏合并,这样可以分配给较⼤的对象。但是,当堆空间较⼩时,运⾏⼀段时间以后,就会出现“碎⽚”,如果并发收集器找不到⾜够的空间,那么并发收集器将会停⽌,然后使⽤传统的标记、清除⽅式进⾏回收。如果出现“碎⽚”,可能需要进⾏如下配置:
- -XX:+UseCMSCompactAtFullCollection:使⽤并发收集器时,开启对年⽼代的压缩。
- -XX:CMSFullGCsBeforeCompaction=0:上⾯配置开启的情况下,这⾥设置多少次Full GC后,对年⽼代进⾏压缩
十一、标记-清除(基本回收算法)
在上图中,绿⾊的云代表的是程序中仍在使⽤的对象。从技术层⾯上来说,这有点像是正在执⾏的某个⽅法⾥⾯的局部变量,亦或是静态变量之类的。不同编程语⾔的情况可能会不⼀样,因此这并不是我们关注的重点。蓝⾊的圆圈代表的是内存中的对象,可以看到有多少对象引⽤了它们。灰⾊圆圈的对象是已经没有任何⼈引⽤的了。因此,它们属于垃圾对象,可以被垃圾回收器清理掉。
看起来没有什么问题,不过其中存在一个重大的缺陷。很容易出现一些孤立的环,它们中的对象都不在任何域内,但彼此却互相引用导致引用数不为0。如下图:
在上图中,红色部分其实就是应用程序不再使用的垃圾对象。由于引用计数方法的缺陷,因此会存在内存泄漏。有几种方法可以解决这一问题,比如使用特殊的“弱”引用,但在JVM中,常采用标记-清除算法,具体内容如下:
JVM对于对象可达性的定义要明确⼀些,有着⾮常明确及具体的垃圾回收根对象(Garbage Collection Roots)的定义:
- 1、虚拟机栈中引用的对象
- 2、方法区类静态属性引用的对象
- 3、方法区常量池引用的对象
- 4、本地方法栈JNI引用的对象
JVM通过标记-清除的算法来记录所有可达(存活)对象,同时确保不可达对象的那些内存能够被重⽤。这包含两个步骤:
- 标记是指遍历所有可达对象,然后在本地内存中记录这些对象的信息。
- 清除会确保不可达对象的内存地址可以在下⼀次内存分配中使⽤。
JVM中的不同GC算法,⽐如说Parallel Scavenge,Parallel Mark+Copy, CMS都是这⼀算法的不同实现,只是各阶段略有不同⽽已,从概念上来讲仍然是对应着上⾯所说的那两个步骤。
这种实现最重要的就是不会再出现泄露的对象环了:
缺点就是应⽤程序的线程需要被暂停才能完成回收,如果引⽤⼀直在变的话你是⽆法进⾏计数的。这个应⽤程序被暂停以便JVM可以收拾家务的情况⼜被称为Stop The World pause(STW)。这种暂停被触发的可能性有很多,不过垃圾回收应该是最常⻅的⼀种。
十二、什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
Java虚拟机是⼀个可以执⾏Java字节码的虚拟机进程。Java源⽂件被编译成能被Java虚拟机执⾏的字节码⽂件。
Java被设计成允许应⽤程序可以运⾏在任意的平台,⽽不需要程序员为每⼀个平台单独重写或者是重新编译。Java虚拟机让这个变为可能,因为它知道底层硬件平台的指令⻓度和其他特性。
十三、串⾏(serial)收集器和吞吐量(throughput)收集器的区别是什么?
吞吐量收集器使⽤并⾏版本的新⽣代垃圾收集器,它⽤于中等规模和⼤规模数据的应⽤程序。⽽串⾏收集器对⼤多数的⼩应⽤(在现代处理器上需要⼤概100M左右的内存)就⾜够了。
十四、JVM的永久代中会发⽣垃圾回收么?
垃圾回收不会发⽣在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代⼤⼩对避免Full GC是⾮常重要的原因。
十五、Java内存模型
Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
堆内存是被所有线程共享的运行时内存区域,存在可见性的问题。线程之间共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存存储了该线程共享变量的副本(本地内存是一个抽象概念,并不真实存在),两个线程要通信的话,首先A线程把本地内存更新过的共享变量更新到主存中,然后B线程去主存中读取A线程更新过的共享变量,也就是说假设线程A执行了i = 1这行代码来更新主线程变量i的值,会首先在自己的工作线程中堆变量i进行赋值,然后再写入主存当中,而不是直接写入主存。
十六、原子性、可见性、有序性
(1)原子性:对基本数据类型的读取和赋值操作是原子性操作,这些操作不可被中断,是一步到位的,例如x=3是原子性操作,而y = x就不是,它包含两步:第一读取x,第二将x写入工作内存;x++也不是原子性操作,它包含三部,第一,读取x,第二,对x加1,第三,写入内存。原子性操作的类如:AtomicInteger AtomicBoolean AtomicLong AtomicReference。
(2)可见性:指线程之间的可见性,既一个线程修改的状态对另一个线程是可见的。volatile修饰可以保证可见性,它会保证修改的值会立即被更新到主存,所以对其他线程是可见的,普通的共享变量不能保证可见性,因为被修改后不会立即写入主存,何时被写入主存是不确定的,所以其他线程去读取的时候可能读到的还是旧值
(3)有序性:Java中的指令重排序(包括编译器重排序和运行期重排序)可以起到优化代码的作用,但是在多线程中会影响到并发执行的正确性,使用volatile可以保证有序性,禁止指令重排。
volatile可以保证可见性、有序性,但是无法保证原子性,在某些情况下可以提供优于锁的性能和伸缩性,替代sychronized关键字简化代码,但是要严格遵循使用条件。
十七、类加载器双亲委托模式机制
类加载器查找class所采用的是双亲委托模式,所谓双亲委托模式就是判断该类是否已经加载,如果没有则不是自身去查找而是委托给父加载器进行查找,这样依次进行递归,直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就会直接返回,如果没找到,则继续依次向下查找,如果还没找到则最后交给自身去查找。
(1)优点
- 1、避免重复加载,如果已经加载过一次Class,则不需要再次加载,而是直接读取已经加载的Class。
- 2、更加安全,确保java核心api中定义类型不会被随意替换,比如,采用双亲委托模式可以使得系统在Java虚拟机启动时旧加载了String类,也就无法用自定义的String类来替换系统的String类,这样便可以防止核心API库被随意篡改。
十八、什么是类加载器?类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有以下四种类加载器:
- 1、启动类加载器(Bootstrap ClassLoader):用来加载java核心类库,无法被java程序直接引用。
- 2、扩展类加载器(Extensions ClassLoader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 3、系统类加载器(System ClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 4、用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
十九、对象不可达,一定会被垃圾收集器回收么?
即使不可达,对象也不一定会被垃圾收集器回收。
(1)先判断对象是否有必要执行finalize()方法,对象必须重写finalize()方法且没有被运行过。
(2)若有必要执行,会把对象放到一个队列中,JVM会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。
二十、JVM 年轻代到年老代的晋升过程的判断条件是什么呢?
(1)部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
(2)如果对象的大小大于Eden的二分之一会直接分配在old,如果old也分配不下,会做一次majorGC,如果小于eden的一半但
是没有足够的空间,就进行minorgc也就是新生代GC。minor gc后,survivor仍然放不下,则放到老年代。
(3)动态年龄判断,大于等于某个年龄的对象超过了survivor空间一半,大于等于某个年龄的对象直接进入老年代。
(4)大对象,比如长字符串、数组等,由于需要大量连续的内存空间,所以直接进入老年代。
二十一、JVM出现full GC很频繁,怎么去排查问题?
(1)如果有perm gen的话(jdk1.8就没了),要给perm gen分配空间,但没有足够的空间时,会触发full gc。
(2)System.gc()方法的调用。
(3)当统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间,则会触发full gc。(这就可以从多个角度上看了)
- 是不是频繁创建了大对象(也有可能eden区设置过小)
大对象直接分配在老年代中,导致老年代空间不足,从而频繁gc。 - 是不是老年代的空间设置过小了(Minor GC几个对象就大于老年代的剩余空间了)
如果一次fullgc后,剩余对象不多,那么说明eden区设置太小,导致短生命old区;如果一次fullgc后,old区回收率不大,
那么说明old区太小。
二十二、堆区中的Eden Space 和 Survivor Space区
Eden Space字面意思是伊甸园,对象被创建的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的
survivor区域。
Survivor Space幸存者区,用于保存在eden space内存区域中经过垃圾回收后没有被回收的对象。Survivor有两个,分别为
To Survivor、 From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候Eden区域不能被回收的对象被放入
到空的survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放),另一个survivor(即From
Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),然后To Survivor 和 From Survivor的标记会互
换,始终保证一个survivor是空的。
Eden Space和Survivor Space都属于新生代,新生代中执行的垃圾回收被称之为Minor GC(因为是对新生代进行垃圾回
收,所以又被称为Young GC),每一次Young GC后留下来的对象age加1。
Old Gen老年代,用于存放新生代中经过多次垃圾回收仍然存活的对象,也有可能是新生代分配不了内存的大对象会直接
进入老年代。经过多次垃圾回收都没有被回收的对象,这些对象的年代已经足够old了,就会放入到老年代。
当老年代被放满的之后,虚拟机会进行垃圾回收,称之为Major GC。由于Major GC除并发GC外均需对整个堆进行扫描和
回收,因此又称为Full GC。
heap区即堆内存,整个堆大小=年轻代大小 + 老年代大小。堆内存默认为物理内存的1/64(<1GB);默认空余堆内存小于
40%时,JVM就会增大堆直到-Xmx的最大限制,可以通过MinHeapFreeRatio参数进行调整;默认空余堆内存大于70%时,
JVM会减少堆直到-Xms的最小限制,可以通过MaxHeapFreeRatio参数进行调整。
二十三、JVM调优
(1)内存调优检查
(2)检查堆大小设置是否合理
(3)检查新生代老年代大小设置
(4)新生代中eden与survivor比例
(5)垃圾回收器选择
(6)检查堆中大对象、数量最多的对象、是否发生内存泄漏、堆是否够用、线程堆栈是否够用
二十四、什么是双亲委派模型,有什么好处?如何打破双亲委派模型?
类加载器之间满足双亲委派模型,即:除了顶层的启动类加载器外,其他所有类加载器都必须要自己的父类加载器。当一个类加载器收到类加载请求时,自己首先不会去加载这个类,而是不断把这个请求委派给父类加载器完成,因此所有的加载请求最终都传递给了顶层的启动类加载器。只有当父类无法完成这个加载请求时,子类加载器才会尝试自己去加载。
双亲委派模型的好处?使得Java的类随着它的类加载器一起具备了一种带有优先级的层次关系。Java的Object类是所有类的父类,因此无论哪个类加载器都会加载这个类,因为双亲委派模型,所有的加载请求都委派给了顶层的启动类加载器进行加载。所以Object类在任何类加载器环境中都是同一个类。
如何打破双亲委派模型?使用OSGi可以打破。OSGI(Open Services Gateway Initiative),或者通俗点说JAVA动态模块系统。可以实现代码热替换、模块热部署。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。
二十五、GC一定会导致停顿吗,为什么一定要停顿?任意时候都可以GC吗还是在特定的时候?
GC进行时必须暂停所有Java执行线程,这被称为Stop The World。为什么要停顿呢?因为可达性分析过程中不允许对象的引用关系还在变化,否则可达性分析的准确性就无法得到保证。所以需要STW以保证可达性分析的正确性。
程序执行时并非在所有地方都能停顿下来开始GC,只有在“安全点”才能暂停。安全点指的是:HotSpot没有为每一条指令都生成OopMap(Ordinary Object Pointer),而是在一些特定的位置记录了这些信息。这些位置就叫安全点。
二十六、CMS和G1垃圾收集器
(1)CMS
CMS(Concurrent Mark Sweep) 从名字可以看出是可以进行并发标记-清除的垃圾收集器。针对老年代的垃圾收集器,目的是尽可能地减少用户线程的停顿时间。
收集过程有如下几个步骤:
- 初始标记:标记从GC Roots能直接关联到的对象,会暂停用户线程
- 并发标记:即在堆中堆对象进行可达性分析,从GC Roots开始找出存活的对象,可以和用户线程一起进行
- 重新标记:修正并发标记期间因用户程序继续运作导致标记产生变动的对象的标记记录
- 并发清除:并发清除标记阶段中确定为不可达的对象
CMS的缺点:
- 由于是基于标记-清除算法,所以会产生空间碎片
- 无法处理浮动垃圾,即在清理期间由于用户线程还在运行,还会持续产生垃圾,而这部分垃圾还没有被标记,在本次无法进行回收。
- 对CPU资源敏感
CMS比较类似适合用户交互的场景,可以获得较小的响应时间。
(2)G1
G1(Garbage First),有如下特点:
- 并行与并发
- 分代收集
- 空间整合 :整体上看是“标记-整理”算法,局部(两个Region之间 )看是复制算法。确保其不会产生空间碎片。(这是和CMS的区别之一)
- 可预测的停顿:G1除了追求低停顿外,还能建立可预测的时间模型,主要原因是它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
在使用G1收集器时,Java堆的内存划分为多个大小相等的独立区域,新生代和老年代不再是物理隔离。G1跟踪各个区域的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区域。
G1的收集过程和CMS有些类似:
- 初始标记:标记与GC Roots直接关联的对象,会暂停用户线程(Stop the World)
- 并发标记:并发从GC Roots开始找出存活的对象,可以和用户线程一起进行
- 最终标记:修正并发标记期间因用户程序继续运作导致标记产生变动的对象的标记记录
- 筛选回收:清除标记阶段中确定为不可达的对象,具体来说对各个区域的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
G1的优势:可预测的停顿;实时性较强,大幅减少了长时间的gc;一定程度的高吞吐量。
(3)CMS和G1的区别?
- G1堆的内存布局和其他垃圾收集器不同,它将整个Java堆划分成多个大小相等的独立区域(Region)。G1依然保留了分代收集,但是新生代和老年代不再是物理隔离的,它们都属于一部分Region的集合,因此仅使用G1就可以管理整个堆。
- CMS基于标记-清除,会产生空间碎片;G1从整体看是标记-整理,从局部(两个Region之间)看是复制算法,不会产生空间碎片。
- G1能实现可预测的停顿。