Java面经(JVM)

本文详细探讨了Java的JVM内存管理,特别是垃圾收集机制,包括GC的类型如串行、并行、并发收集器,以及CMS和G1的特性。文章还介绍了新生代和老年代的垃圾回收时机,强调了Minor GC和Full GC的区别。此外,详细阐述了分代收集算法和经典的标记-清除、复制、标记-整理算法,以及如何判断对象可回收。同时,线程池和线程局部变量ThreadLocal的使用和潜在的内存泄漏问题也得到了分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM

GC

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

  • 串行:单个线程执行垃圾回收,并且此时用户线程仍然处于等待状态。
  • 并行:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • :指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

三种新生代回收器:SerialGC ParNewGc ParallelScavengeGC

SerialGC:单线程、使用复制算法,垃圾回收时必须暂停所有的工作线程,适用于单CPU

ParNewGc:serial的多线程版本,能与CMS收集器配合工作

ParallelScavengeGC:使用复制算法,并行的多线程收集器
cms收集器尽可能缩短工作线程的停顿时间,而parallelGC则是达到一个可控制的吞吐量

三种老年年代回收器

SerialOldGC:使用标记=整理算法

parNewOldGC:使用标记整理算法,并行,多CPU,注重吞吐量以及CPU敏感的场合,考虑和ParallelGC一起使用

CMS收集器:以获取最短回收停顿时间为目标的收集器,适用于B/S系统。使用标记清除算法
收集过程的四个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

初始标记和重新标记需要停顿其他的工作线程
并发标记和并发清除耗时最长
优点:并发收集,低停顿
缺点:CPU敏感、空间碎片、浮动垃圾
**CPU敏感:**并发阶段占用CPU资源导致引用程序变慢,总吞吐量下降,无法处理浮动垃圾,导致再次引发FULL GC
**浮动垃圾:**由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”
**空间碎片:**用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,

G1收集器:
特点如下:

  • 并行与并发:充分利用多CPU、多核环境的硬件优势。使用多个CPU(CPU或CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:能够采用不同的方式处理新创建的对象和已经存活一段时间的对象从而获得更好的收集效果
  • 空间整合:整体上看是标记-整理、局部上看是基于复制、这保证了运行时间不会产生空间碎片,收集后能提供完整的可用内存
  • 可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

两代回收器的配合关系:

java的内存泄漏指的是什么?

不能再被使用的对象的内存不能被回收,长生命周期的对象引用短生命周期的对象

minor GC运行频繁可能是什么原因引起的?

堆内存太小了

阐述GC算法

GC的对象是堆内存和永久区

GC算法包括:引用计数法。标记清除法、标记压缩法,复制算法

回收的机制是什么?凭什么判断一个对象会被回收?

GC即垃圾收集机制是指JVM用于释放那些不再使用的对象所占用的内存。


GC主要考虑三件要完成的事情:

哪些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数算法和可达性分析算法)

引用计数算法

算法思路:
1)给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;

2)当引用失效时,计数器值就减1;

3)任何时刻计数器为0的对象就是不可能再被使用的;

算法优点:
实现简单,判定效率高;

算法缺点:
很难解决对象之间相互循环引用的问题。所以主流的虚拟机里没有选用引用计数算法来管理内存的。

可达性分析算法

算法思路:
通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始往下搜索,搜索所有走过的路径

称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

如图:

可达性分析算法判定对象是否可以回收的图例中,绿色部分为仍然存活的对象,黄色部分判定可回收的对象。

黄色部分object5、object6、object7虽然相互关联,但是它们到GC Roots是不可达的,也就是没有与GC Roots

建立引用链,所以它们将被判定为是可以回收的对象。

在Java语言中,可以作为GC Roots的对象包括下面几种:

1)虚拟机栈(栈帧中的本地变量表)中引用的对象。

2)方法区中静态属性引用的对象。

3)方法区中常量引用的对象,在jdk7后被移到堆中。

4)本地方法栈中JNI(即一般说的Native方法)引用的对象。

算法优点:
更加精确和严谨,可以分析出循环数据结构相互引用的情况;

算法缺点:
1)实现比较复杂;

2)需要分析大量数据,消耗大量时间;

3)分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",

判断对象是生存还是死亡
即使在可达性分析算法中不可达的对象,也并非是"非死不可的",只是暂时处于"缓刑"阶段,
要真正宣告一个对象死亡,至少要经历两次标记过程。

第一次标记:
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记

并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,

或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行"。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的

队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去触发这个方法。

第二次标记:

GC将对F-Queue队列中的对象进行第二次小规模标记;finalize()方法是对象逃脱死亡的最后一次机会:

如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;

如果对象没有,也可以认为对象已死,可以回收了;

一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;

什么时候回收?(堆的新生代、老年代、永久代的垃圾回收时机,MinorGC 和 FullGC)

程序计数器,虚拟机栈,本地方法栈这3个区域是线程私有的,随线程而生,随线程而灭,这几个区域就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存就自然跟着回收了。主要讨论Java堆和方法区的回收。

Java堆通常分为新生代,老年代。永久代,实际上就是方法区。

【方法区】是JVM的一种规范,存放类信息,常量,静态变量,即时编译后的代码等。

【永久代】是HotSpot的一种具体实现,实际上指的就是方法区,或者说用永久代来实现方法区。对于其他虚拟机来说是不存在永久代的概念的。

**新生代:**都是使用复制收集算法,又可以细分为:den、From Survivor、To Survivor三个区(8:1:1),

Eden:新对象的出生地。
From Survivor**:上一次GC的幸存者,作为这一次GC的被扫描者。
To Survivor
:**保留MinorGC过程中的幸存者。

**【MinorGC的触发条件】****:**当Eden区内存不足的时候,虚拟机将进行一次MinorGC。Survivor区内存不足不会触发MinorGC。MinorGC之后,可能会与一些新生代的对象年龄满足进入老年代,老年代的占用会有所升高。
MinorGC的过程:MinorGC采用复制算法。首先,把Eden和From Survivor区域中存活的对象复制到To Survivor区域,同时把这些对象的年龄+1(默认情况下15岁就直接送到老年代了,晋升老年代的阈值可以通过-XX:MaxTenuringThreshold设置);然后,清空Eden和From Survivor中的对象;最后,To Survivor和From Survivor互换,原To Survivor成为下一次GC时的From Survivor区。

**老年代:**主要存放程序中年龄较大和需要占用大量连续内存空间的对象。老年代的对象比较稳定,所以MajorGC/Full GC执行的频率较低。一般都是在空间不足的时候才会执行MajorGC/Full GC。
老年代的垃圾收集器Serial Old,Parallel Old采用的是标记—整理算法,CMS采用的==标记—清除算法。==标记—清除算法会产生大量的内存碎片。

永久代:HotSpot中对于JVM规范中方法区的实现,指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

**Full GC:****主要是发生在老年代GC,**出现Major GC/Full GC,通常情况下伴随着至少一次的Minor GC(并不是绝对的),Major GC/Full GC的速度一般比Minor GC慢10倍以上。

Full GC的触发条件:
年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代,

永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

  1. 当准备要触发一次Minor GC时,如果发现统计数据说之前Minor GC的平均晋升大小比目前老年代剩余的空间大,则不会触发Minor GC而是转为触发Full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新时代,所以不需要事先触发一次单独的Minor GC);
  2. 如果有永久代的话,要在永久代分配空间但已经没有足够空间时,也要触发一次Full GC;(Java虚拟规范中并不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价比”较低。在大量使用频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。)
  3. System.gc()、heap dump带GC,默认也是触发Full GC。

如何回收?(经典垃圾回收算法(标记清除算法、复制算法、标记整理算法)及分代收集算法和七种垃圾收集器)

标记清除:

1)标记

首先标记出所有需要回收的对象,要宣告一个对象死亡,至少要经历两次标记过程。

第一次标记

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记

并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,

或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为"没有必要执行"。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的

队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去触发这个方法。

第二次标记

GC将对F-Queue队列中的对象进行第二次小规模标记;finalize()方法是对象逃脱死亡的最后一次机会:

如果对象在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;

如果对象没有,也可以认为对象已死,可以回收了;

2)清除

两次标记后,还在"即将回收"集合的对象将被统一回收;

回收执行示意图


2、算法优点
实现简单。

3、算法缺点
该算法主要有两个缺陷,一个是效率问题,另外一个空间问题。

1)效率问题

标记和清除两个过程的效率都不高。

2)空间问题

  标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要分配

较大对象时,无法找到足够的连续内存而不得不提前触发另外一次垃圾收集动作。

4、算法应用场景
针对CMS收集器使用。

复制算法
"复制"(Copying)算法的出现,是为了解决"标记-清除"算法的效率问题。

1、算法思路
1)将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。

2)当一块内存用完后,就将还存活着的对象复制到另外一块上面,然后再把已使用多的内存空间一次清掉。

    这样使得每次都对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶

    指针,按顺序分配内存即可,实现简单,运行高效。

 复制算法示意图


2、算法优点
不会产生内存碎片,内存分配实现简单,高效。

3、算法缺点
1)空间浪费

  可用内存缩减为原来的一半,太过浪费(解决:可以改良,不按1:1比例划分);

2)效率随对象存活率升高而变低

当对象存活率较高时,需要进行较多复制操作,效率将会变低;

4、算法应用场景
现在商业虚拟机都采用这种收集算法来回收新生代,用该算法的垃圾收集器比较多,
如Serial收集器、ParNew收集器、Parallel Scavenge收集器、G1(从局部看)。

标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率会降低。更关键的是,如果不想浪费50%

的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在

老年代一般不直接选用这种算法。

1、算法思路
“标记-整理”(Mark-Compact)算法的标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对回收对象进
行清理,而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存,"标记-整理"算法的执行示意图如下:

2、算法优点
1)不会像复制算法,效率随对象存活率升高而变低。

老年代特点:

对象存活率高,没有额外的空间可以分配担保;所以老年代一般不能直接选用复制算法算法;而选用标记-整理算法;

2)不会像标记-清除算法,产生内存碎片因为清除前,进行了整理,存活对象都集中到空间一侧;

3、算法缺点
主要是效率问题:除像标记-清除算法的标记过程外,还多了需要整理的过程,效率更低;

4、算法应用场景
很多垃圾收集器采用这种算法来回收老年代;如Serial Old收集器、G1(从整体看);

分代收集算法

** “分代收集”(Generational Collection)算法结合不同的收集算法处理不同区域。**

1、算法思路
当前虚拟机的垃圾收集都采用"分代收集"算法,这种算法并没有什么新的思想,只是根据对象的
存活申请周期的不同将内存划分为几块,这样就可以根据各个年代的特点采用最适当的收集算法。

java堆分为新生代和老年代。

1)新生代

 在新生代中,每次垃圾收集都发现有大批量对象死去,只有少量存活,就选用复制算法,

只需要付出少量的存活对象的复制成本就可以完成收集。

2)老年代

在老年代中对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记-清除"或"标记-整理"算法

来进行回收。

HotSpot虚拟机对新生代和老年代一般的内存划分示意图:

2、算法优点
可以根据各个年代的特点采用最适当的收集算法;

3、算法缺点
仍然不能控制每次垃圾收集的时间;

4、算法应用场景
目前几乎所有商业虚拟机的垃圾收集器都采用分代收集算法;如HotSpot虚拟机中全部垃圾收集器:

Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(也保留);

线程池

线程池了解吗?说一下为什么要有线程池?

为什么要用线程池:

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

什么是线程池:
java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池
线程池包括以下四个基本组成部分:
** 1、线程池管理器(ThreadPool):**用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
**2、工作线程(PoolWorker):**线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
**4、任务队列(taskQueue):**用于存放没有处理的任务。提供一种缓冲机制。
假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会为了创建50000而在处理请求时浪费时间,从而提高效率。
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

常见线程池

①newSingleThreadExecutor

单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务

②newFixedThreadExecutor(n)

固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行

③newCacheThreadExecutor(推荐使用)

可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。

④newScheduleThreadExecutor

大小无限制的线程池,支持定时和周期性的执行线程

** java提供的线程池更加强大,相信理解线程池的工作原理,看类库中的线程池就不会感到陌生了。**

说说线程池的主要参数?

ThreadPoolExecutor详解
corePoolSize - 池中所保存的线程数,包括空闲线程。

maximumPoolSize-池中允许的最大线程数。

keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

unit - keepAliveTime 参数的时间单位。

workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务。

threadFactory - 执行程序创建新线程时使用的工厂。

handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。

ThreadPoolExecutor是Executors类的底层实现。

什么情况会发生栈内存溢出

如果线程的请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

jvm 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的jvm 参数。

对象诞生即新生代->eden,在进行minor gc过程中,如果依旧存活,移动到from,变成Survivor,进行标记代数,如此检查一定次数后,晋升为老年代,

你知道哪几种垃圾收集器,各自的优缺点,重点讲下 cms,包括原理,流程,优缺点

Serial、parNew、ParallelScavenge、SerialOld、ParallelOld、CMS、G1

Serial收集器

Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)也就是传说中的Stop The World。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

ParNew收集器

ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。 使用方式可以使用-XX:+UseConcMarkSweepGC,或者是使用-XX:+UseParNewGC来强制开启,可以通过-XX:ParallelGCThreads 来调整或者限制垃圾收集的线程数量。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
Parallel Scavenge提供了两个参数用来精确控制,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用**“标记-整理”算法**。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的内容中详细讲解。

Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择(还记得上面说过Parallel Scavenge收集器无法与CMS收集器配合工作吗?)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。

直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器

CMS 收集器

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

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

  1. 初始标记(CMS initial mark)

  2. 并发标记(CMS concurrent mark)

  3. 重新标记(CMS remark)

  4. 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。

CMS是一款**基于“标记—清除”**算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

G1收集器

G1是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点。

**并行与并发:**G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

**分代收集:**与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。

**空间整合:**与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

总结

各个收集器都有自己适应的业务场景,一般来说在生产中都会使用ParNew+CMS的方式分代收集回收。针对于新生代来说,根据其特性,总的来说还是使用复制算法。而针对于老年代来说算法一般会使用两种,标记-清除和标记-整理,每一种都有自己适用的场景。但是在目前我遇到的生产中还是使用CMS 标记清除的这种比较多,也是比较常用的,虽然标记-清除这种算法会导致大量的内存碎片,但是也是可以通过一种暴力的方式解决,就是FULL GC,G1收集器是比较完美的一种收集器,但是在目前的生产中依旧用得很少。

垃圾回收算法的实现原理。

如何计算对象已死

  1. 引用计数器算法

引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。

引用计数器实现简单,效率高;但是不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。

  1. 可达性分析算法

可达性分析算法是**通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),**当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的,如下图所示。

GC Roots对象包括:

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象。

  • 方法区域中的类静态属性引用的对象。

  • 方法区域中常量引用的对象。

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

回收的区域

  1. 新生代
  2. 永久代

新生代,这个很容易理解,一般来说永久带是最不好回收的。永久代主要回收以下部分内容:

  1. 废弃常量
  2. 无用的类

垃圾回收算法

  1. 标记—清除算法

标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。

标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。如下图所示:

  1. 复制算法

复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。这种方式听上去确实是非常不错的方案,但是总的来说对内存的消耗十分高。

复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1),也就是常提到的一块Eden(80%)和两块Survivor(20%)。当然也会存在10%不够用的情况,这个后面在进行梳理,会有一个补偿机制,也就是分配担保

  1. 标记—整理算法

复制收集算法会存在一种极端情况,就是对象都没死。这种情况会在老年代有几率的出现,所以根据老年代的特点提出了标记—整理算法。 标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存,如下图所示:

  1. 分代收集**

**分代收集是根据对象的存活时间把内存分为新生代和老年代,**根据个代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用标记—复制算法老年代采用标记—整理算法。

垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。

jvm调优

内存泄漏

内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小,目前来说,常遇到的泄漏问题如下:
年老代堆空间被占满

 年老代堆空间被占满
 异常: java.lang.OutOfMemoryError: Java heap space

这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。这种情况一般来说是因为内存泄漏或者内存不足造成的。某些情况因为长期的无法释放对象,运行时间长了以后导致对象数量增多,从而导致的内存泄漏。另外一种就是因为系统的原因,大并发加上大对象,Survivor Space区域内存不够,大量的对象进入到了老年代,然而老年代的内存也不足时,从而产生了Full GC,但是这个时候Full GC也无发回收。这个时候就会产生java.lang.OutOfMemoryError: Java heap space

解决方案如下:

  1. 代码内的内存泄漏可以通过一些分析工具进行分析,然后找出泄漏点进行改善。
  2. 第二种原因导致的OutOfMemoryError可以通过,优化代码和增加Survivor Space等方式去优化。

持久代被占满

持久代被占满
异常:java.lang.OutOfMemoryError: PermGen space

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。 解决方案:

  1. 增加持久代的空间 -XX:MaxPermSize=100M。
  2. 如果有自定义类加载的需要排查下自己的代码问题。

堆栈溢出

堆栈溢出
异常:java.lang.StackOverflowError

一般就是递归没返回,或者循环调用造成

线程堆栈满

线程堆栈满
异常:Fatal: Stack size too small

java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。 解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。

系统内存被占满

系统内存被占满
异常:java.lang.OutOfMemoryError: unable to create new native thread

这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。 分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。 解决:

1\. 重新设计系统减少线程数量。
2\. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。

JVM 内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。

**内存屏障:**为了保障执行顺序和可见性的一条cpu指令
**重排序:**为了提高性能,编译器和处理器会对执行进行重拍
**happen-before:**操作间执行的顺序关系。有些操作先发生。

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

  • **主内存:**共享变量存储的区域即是主内存
    **工作内存:**每个线程copy的本地内存,存储了该线程以读/写共享变量的副本
    http://ifeve.com/java-memory-model-1/
    http://www.jianshu.com/p/d3fda02d4cae
    http://blog.youkuaiyun.com/kenzyq/article/details/50918457

简单说说你了解的类加载器。

类加载器的分类(bootstrap,ext,app,curstom),类加载的流程(load-link-init)
http://blog.youkuaiyun.com/gjanyanlig/article/details/6818655/
JVM的类加载是通过ClassLoader及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

1)Bootstrap ClassLoader

负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader

负责记载classpath中指定的jar包及目录中class

4)Custom ClassLoader

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

类什么时候才被初始化:

1)创建类的实例,也就是new一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName(“com.lyj.load”))

5)初始化一个类的子类(会首先初始化子类的父类)

6)JVM启动时标明的启动类,即文件名和类名相同的那个类

     只有这6中情况才会导致类的类的初始化。

 类的初始化步骤:

    1)如果这个类还没有被加载和链接,那先进行加载和链接

    2)假如这个类存在直接父类,并且这个类还没有被初始化(注意:在一个类加载器中,类只能初始化一次),那就初始化直接的父类(不适用于接口)

     3)加入类中存在初始化语句(如static变量和static块),那就依次执行这些初始化语

讲讲 JAVA 的反射机制。

Java程序在运行状态可以动态的获取类的所有属性和方法,并实例化该类,调用方法的功能

g1 和 cms 区别,吞吐量优先和响应优先的垃圾收集器选择。

Cms是以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现比较占用cpu资源,切易造成碎片
G1是面向服务端的垃圾收集器,是jdk9默认的收集器,基于标记-整理算法实现。可利用多核、多cpu,保留分代,**实现可预测停顿,可控。 **

请解释如下 jvm 参数的含义:
-server -Xms512m -Xmx512m -Xss1024K
-XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxTenuringThreshold=20
XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly。

Server模式启动
最小堆内存512m
最大512m
每个线程栈空间1m
永久代256
最大永久代256
最大转为老年代检查次数20
Cms回收开启时机:内存占用80%
命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期

为什么新生代内存需要有两个Survivor区?

1). 为什么要有Survivor区

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,从而触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。由于老年代的内存空间一般是新生代的2倍,因此进行一次Full GC消耗的时间比Minor GC长得多,这样,频繁的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度。Survivor的存在意义就在于,减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。

2). 为什么要有两个Survivor区

设置两个Survivor区最大的好处就是解决了碎片化。在第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。

同时,我们也知道,现在大多数JVM虚拟机将新生代与老年代按照如下比例来分配:

Eden :Survivor(to,from) = 8 :2(1:1)

在这里,Eden区理所当然大一些,否则新建对象很快就导致Eden区满,进而触发Minor GC,有悖于初衷。

JDK 1.8相对于之前版本中HashMap中的实现的变化?

  • JDK 1.8 以后哈希表的添加、删除、查找、扩容方法都增加了一种节点为TreeNode的情况:

  • 添加时,当桶中链表个数超过 8 时会转换成红黑树;

  • 删除、扩容时,如果桶中结构为红黑树,并且树中元素个数太少的话,会进行修剪或者直接还原成链表结构;

  • 查找时即使哈希函数不优,大量元素集中在一个桶中,由于有红黑树结构,性能也不会差(O(lgn))。

ThreadLocal 的内存泄漏问题

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说,ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
image_1c00c52qv9k2qau17saa516o444.png-59.8kB

那么,ThreadLocal为什么会内存泄漏呢?原因是,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

综合上面的分析,ThreadLocal 的最佳实践为:每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

Java的平台无关性即一次编译处处执行如何实现

  • Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。
    在这里插入图片描述

为什么JVM不直接将源码解析成机器码去执行

  • 需要准备工作:每次执行都需要各种检查
  • 兼容性:也可以将别的语言解析成字节码

JVM如何加载.class文件/类加载机制

加载:类加载器获二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。
准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。
解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
初始化:执行类中定义的Java程序代码(字节码)。
在这里插å¥å›¾ç‰‡æè¿°

Java类加载器及如何加载类

类加载器是实现通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系
双亲委派模型的工作过程是:如果一个类加载器接收到类加载的请求,它会先把这个请求委派给父加载器去完成,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。可以得知,所有的加载请求最终都会传送到启动类加载器中
在这里插å¥å›¾ç‰‡æè¿°

  1. 谈谈ClassLoader
    ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。

  2. ClassLoader的种类
    BootStrapClassLoader:C++编写,加载核心库java.*
    ExtClassLoader:Java编写,加载扩展库javax.*
    AppClassLoader:Java编写,加载程序所在目录
    自定义ClassLoader:Java编写,定制化加载
    自底向上检查类是否已经加载,自顶向下尝试加载类。


自底向上检查类是否已经加载,自顶向下尝试加载类。
在这里插å¥å›¾ç‰‡æè¿°

双亲委派模型工作工程:

  1. 当Application ClassLoader 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。
  2. 当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。
  3. 如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让Extension ClassLoader尝试加载。
  4. 如果Extension ClassLoader也加载失败,就会使用Application ClassLoader加载。
  5. 如果Application ClassLoader也加载失败,就会使用自定义加载器去尝试加载。
  6. 如果均加载失败,就会抛出ClassNotFoundException异常。

双亲委派模型的实现过程:

实现双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法中:
首先会检查请求加载的类是否已经被加载过;
若没有被加载过:
递归调用父类加载器的loadClass();
父类加载器为空后就使用启动类加载器加载;
如果父类加载器和启动类加载器均无法加载请求,则调用自身的加载功能。

双亲委派模型的优点:

Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。
保证了运行的安全性,防止不可信类扮演可信任的类。

类的加载方式

  1. 隐式加载:new
  2. 显式加载:loadClass,forName等
  3. loadClass和forName的区别
    Class.forName得到的class是已经初始化完成的
    Classloader.loadClass得到的class是还没有链接的(只完成了加载阶段,没有到链接阶段)

内存分配

JVM内存分配

Java堆:Java虚拟机所管理的内存中最大的一块,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。

方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。

程序计数器:当前线程所执行字节码的行号指示器。每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。

Java虚拟机栈:用于描述Java方法执行的模型。每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。

本地方法栈:与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,比如在Java中调用C/C++。

JVM内存分代机制,各代特点,分代回收优点

新生代(Young):HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。

老年代(Old):在新生代中经历了多次GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。

永久代(Permanent):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,一般而言不会进行垃圾回收。

在这里插å¥å›¾ç‰‡æè¿°

新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的Header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。

常用的内存调试工具

JConsole(可视化工具,可排查死锁、生成Heap Dump文件等)
Jmap
jstack

垃圾回收

垃圾回收需要完成的三件事:①哪些内存需要回收;②什么时候回收;③如何回收。

哪些内存需要回收


方法区

什么时候回收

引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,每当一个引用失效时,计数器减1,任何时刻计数器为0则代表对象不被引用。
注意:引用计数算法有一个比较大的问题,那就是它不能处理环形数据。即如果有两个对象相互引用,那么这两个对象计数器始终不为0,也就不能被回收。

可达性分析算法:设立若干根对象(GC Roots),每个对象都是一个子节点。从根向下搜索所走过的路径叫引用链,当一个对象到根无任何引用链相连,证明此对象不可用。
Java语言中可以作为GC Roots的对象包括以下几种:

虚拟机栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象

如何回收

标记-清除算法:先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。
复制算法:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将存活的对象复制到另一块,最后将此块内存一次性清理掉。
标记-整理算法:先标记所有需要回收的对象,然后让所有存活的对象向一端移动,最后直接清理掉边界以外的另一端内存。
分代收集算法:把Java堆分为新生代和老年代。新生代中只有少量对象会存活,就选用复制算法;老年代中对象存活率较高,选用标记-XX算法。
垃圾收集器有哪些
Serial收集器:单线程收集器。收集垃圾时必须暂停其他所有工作线程,直到它收集结束。
Parnew收集器:Serial收集器多线程版本。
Parallel Scavenge收集器:使用复制算法的新生代收集器。
Serial Old收集器:使用标记-整理算法的老年代单线程收集器。
Parallel Old收集器:使用标记-整理算法的老年代多线程收集器。
CMS收集器:基于标记-清除算法的低停顿并发收集器。运作步骤为①初始标记②并发标记③重新标记④并发清除。
G1收集器:最前沿的面向服务端应用的垃圾收集器。运作步骤为①初始标记②并发标记③最终标记④筛选回收。G1收集器有以下特点
并行与并发:无需停顿Java线程来执行GC动作。
分代收集:可独立管理整个GC堆。
空间整合:运行期间不会产生内存空间碎片。
可预测的停顿:除了低停顿,还能建立可预测的停顿时间模型。

Minor GC与Full GC

新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次 Minor GC(非绝对),MajorGC 的速度一般会比 Minor GC 慢10倍以上。
Minor GC与Full GC触发条件:

Minor GC触发条件:
当Eden区没有足够的空间进行分配时
老年代最大可用连续空间大于Minor GC历次晋升到老年代对象的平均大小?
Full GC触发条件:
调用System.gc()时(系统建议执行Full GC,但是不必然执行)
老年代空间不足时
方法区空间不足时
老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
-CMS GC在垃圾回收的时候,当对象从Eden区进入Survivor区,Survivor区空间不足需要放入老年代,而老年代空间也不足时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值