目录
说一下几种垃圾收集算法的原理和特点,应用的场景。怎么优化复制算法?
1、进程与线程
1)进程与线程的区别
- 进程是具有一定独立功能的程序、它是系统进行资源分配和调度的一个独立单位。
- 线程是进程的一个实体,是CPU调度和分派的基本单位,线程自己基本上不拥有系统资源。在运行时,只是暂用一些计数器、寄存器和栈。
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
- 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
2、垃圾收集器与内存分配策略
1)说一下Java 的垃圾回收机制?
它使得Java 程序员在编写程序的时候不再需要考虑内存管理。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。程序员可以手动执System.gc(),通知GC 运行,但是Java 语言规范并不保证GC 一定会执行。
Where:运行时的内存分布情况。见下一题。
When:对象何时需要被回收的?也就是何时回收无效对象,已死对象的?这里涉及到两种做法:引用计数法和可达性分析算法。这里还涉及到java中4 种引用方式:强引用,软引用,弱引用和虚引用(区别:https://blog.csdn.net/lovoo/article/details/51615423),其引用强度越来越来低,意味着引用越弱的对象越容易被垃圾回收的。
how:对象如何被回收的?4 种垃圾回收算法。
2)JVM 的内存布局/内存模型?
主要包括:方法区,堆,Java栈,PC寄存器,本地方法栈
方法区和堆由所有线程共享。
堆:存放所有程序在运行时创建的对象。
方法区:当JVM的类装载器加载.class文件,并进行解析,把解析的类型信息放入方法区。
3)说一下引用计数法与可达性分析算法
https://blog.csdn.net/quinnnorris/article/details/75040538
4)堆里面的分区和各自的特点
- 年轻代:年轻代又进一步可以划分为一个伊甸园(Eden)和两个存活区(Survivor space),伊甸园是进行内存分配的地方,是一块连续的空闲内存区域,在里面进行内存分配速度非常快,因为不需要进行可用内存块的查找。新对象是总是在伊甸园中生成,只有经受住了一定的考验后才能后顺利地进入到存活区中,这种考验是什么在后面会讲到。把存活区划分为两块,其实也是为了满足垃圾回收的需要,因为在年轻代中经历了“回收大劫”未必就能够进入到年老代中。系统总是把对象放在伊甸园和一个存活区(任意的一个),在垃圾回收时,根据其存活时间被复制到另一个存活区或者年老代中,则之前的存活区和伊甸园中剩下的都是需要被回收的对象,只对这两个区域进行清除即可,两个存活区是交替使用,循环往复,在下一次垃圾回收时,之前被清除的存活区又用来放置存活下来的对象了。一般来说,年轻代区域较小,而且大部分对象是需要进行清除的,采用“复制算法”进行垃圾回收。
- 年老代:在年轻代中经历了N 次回收后仍然没有被清除的对象,就会被放到年老代中,都是生命周期较长的对象。对于年老代和永久代,采用一种称为“标记-清除-压缩(Mark-Sweep-Compact)”的算法。标记的过程是找出当前还存活的对象,并进行标记;清除则是遍历整个年老区,找到已标记的对象并进行清除;而压缩则是把存活的对象移动到整个内存区的一端,使得另一端是一块连续的空间,方便进行内存分配和复制。
- Minor GC:当新对象生成,但在Eden申请空间失败时就会触发Minor GC,对Enden区进行GC,清除掉非存活的对象,并且把存活的对象移动到Survivor 区中的其中一个区中。前面的提到考验就是Minor GC,也就是说对象经过了Minor GC 才能够进入到存活区中。这种形式的GC 只会在年轻代中进行,因为大部分对象都是从Eden区开始的,同时Eden 区不会分配得太大,所以对Eden 区的GC 会非常地频繁。
- Full GC:对整个对进行整理,包括了年轻代、年老代和持久代。Full GC 要对整个块进行回收,所以要比Minor GC 慢得多,因此应该尽可能减少Full GC 的次数。
5)Minor GC与Full GC分别在什么时候发生?
Minor GC:如果Eden空间占满了,会触发minor GC。Minor GC后仍然存活的对象会被复制到S0中去。这样Eden 就被清空可以分配给新的对象。又触发了一次Minor GC,S0和Eden中存活的对象被复制到S1中,并且S0和Eden 被清空。在同一时刻,只有Eden和一个Survivor Space 同时被操作。当每次对象从Eden 复制到Survivor Space 或者从Survivor Space 中的一个复制到另外一个,有一个计数器会自动增加值。默认情况下如果复制发生超过16次, JVM 会停止复制并把他们移到老年代中去。
Full GC: System.gc()方法的调用;老年代代空间不足;永生区空间不足;CMS GC时出现promotion failed和concurrent mode failure;统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间;堆中分配很大的对象。详细参考:
https://blog.csdn.net/chenleixing/article/details/46706039
6)内存分配规则?
1.对象优先分配在Eden 区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden 区和两个Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1 次Minor GC 那么对象会进入Survivor 区,之后每经过一次Minor GC 那么对象的年龄加1,直到达到阀值,对象进入老年区。
4.动态判断对象的年龄。如果Survivor 区中相同年龄的所有对象大小的总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC 时,JVM 会计算Survivor 区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure 设置,如果true 则只进行Monitor GC,如果false 则进行Full GC。
7)说一下几种垃圾收集算法的原理和特点,应用的场景。怎么优化复制算法?
- 标记-清除算法:最基本的收集算法“标记-清除”(Mark-Sweep)算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,之所以说它是最基本的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的主要不足有两个:一是效率问题,标记和清除效率都不高,二是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序在运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作.
- 复制算法:为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。(不适合存活率较高场景)
- 标记-整理算法:复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费50%的空间就要使用额外的空间进行分配担保(Handle Promotion当空间不够时,需要依赖其他内存),以应对被使用的内存中所有对象都100%存活的极端情况。对于“标记-整理”算法,标记过程仍与“标记-清除”算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存。
- 分代收集算法:当前的商业虚拟机的垃圾收集都是采用“分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
8)GC 收集器有哪些?CMS 收集器与G1 收集器的特点。
- serial收集器:是一个单线程的垃圾收集器,它只会使用一个cpu、一个收集线程工作;它在进行gc的时候,必须暂停其他所有线程到它工作结束(这种暂停往往让人难以接受)。适合单个cpu,采用复制算法。
- parnew收集器:parnew其实就是serial的多线程版本,parnew在单线程的情况下甚至不如serial,parnew是除了serial之外唯一能和CMS配合的。parnew默认开启收集线程数和cpu的数量相同,我们可以利用-XX:ParallelGCThreads参数来控制它开启的收集线程数。
- parallel scavenge收集器:parallel scavenge主要就是关注吞吐量。所谓吞吐量:运行用户代码的时间/(运行用户代码时间+GC花费的时间)。提供了2个参数来控制吞吐量:-XX:GCTimeRatio:gc时间占用的总比例,也就是吞吐量的倒数。-XX:MaxGCPauseMillis:最大的暂停毫秒数(这个数值并非越小越好,如果把他设置小了,系统会根据这个值调整空间的大小,也就会加快GC的频率)parallel scavenge可以设置开启-XX:UseAdaptiveSizePolicy,开启这个参数之后就无需关注新生代大小eden和survivor等比例,晋升老年代对象年龄的这些细节了。
- serial old收集器:serial收集器的老年代版本,使用标记整理算法,主要有两个作用:jdk5之前和parallel scavenge配合使用。作为cms失败的后备收集方案
- parallel old收集器:是parallel收集器的老年代版本,用于和parallel收集器搭配组合的,因为parallel收集器不能和cms组合,但是和serial old收集器效率又太低。对吞吐量和CPU敏感度特别关注的应用可以使用parallel+parallel old的组合。
CMS收集器:
适用特点优势:并发收集,低停顿
CMS的过程:
- 初始标记:标记一下GC ROOT能直接关联的对象,速度很快,这个阶段是会STW(Stop-The-World)。
- 并发标记:GC ROOT的引用链的查找过程,标记能关联到GC ROOT的对象,这一个阶段是不需要STW的。
- 重新标记:在并发标记阶段,应用的线程可能产生新的垃圾,所以需要重新标记,这个阶段也是会STW。
- 并发清除:这个阶段就是真正的回收垃圾的对象的阶段,无需STW。
CMS的缺点:
- 对cpu比较敏感。
- 可能出现浮动垃圾:在并发清除阶段,用户还是继续使用的,这时候就会有新的垃圾出现,CMS只能等下一次GC才能清除掉他们。
- CMS运行期间预留内存不够的话,就会出现concurrent Mode Failure,这时候就会启动serial收集器进行收集。
- CMS基于标记清除算法实现,会产生内存碎片空间。碎片空间过多就会对大对象的分配空间造成麻烦。为了解决碎片问题,CMS提供一个参数来控制是否在GC的时候整合内存中的碎片,这个碎片整合的操作是无法并发的,会延长STW的时间。
G1收集器:
特点:
- 并发并行;利用多CPU来缩短STW(Stop-The-World)的时间。
- 使用分代收集;不需要其他收集器配合也能独立管理整个堆。
- 整体是基于标记-整理,局部使用复制算法,不会产生碎片空间。
- 可以预测停顿:G1把整个堆分成多个Region,然后计算每个Region里面的垃圾大小(根据回收所获得的空间大小和回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
- 面向服务器端应用
G1的运行过程:
- 初始标记:标记一下GC ROOT能直接关联的对象,速度很快,这个阶段是会STW。
- 并发标记:在GC ROOT中运用可达性分析算法,找出存活的对象,耗时较长,但是无需STW。
- 最终标记:修正并发标记期间用户线程对垃圾对象的修改,需要停顿线程,但是可以并行执行。
- 筛选回收:先计算回收各个Region的价值,然后根据用户需求来进行回收
3、虚拟机类加载机制
1)类加载过程
三大步骤--->加载、链接(验证、准备、解析)、初始化
第1条中的二进制字节流并不只是单纯地从Class文件中获取(从JAR包加载class文件,从网络加载class文件)。
2)JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。