1、JVM内存模型

JVM内存模型主要有五个部分组成:程序计数器、虚拟机栈、本地方法栈、方法区、堆

程序计数器

程序计数器是线程私有的一块内存区域,主要用来保存虚拟机所要执行字节码的位置,每个线程都有一个独立的程序计数器,但是程序计数器只为执行Java方法服务,执行native方法时,程序计数器为空

虚拟机栈

虚拟机栈,也叫方法栈,是描述Java方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等信息,在方法执行时,执行入栈操作,在方法返回时,执行出栈操作

本地方法栈

本地方法栈与虚拟机栈类似,主要区别是虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行native方法服务的

方法区

方法区是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量信息、静态变量和即时编译器编译优化后的代码等数据。JDK1.7中的永久代和JDK1.8中的metaspace都是方法区的一种实现

Java堆

Java堆是线程共享的一块内存区域,也是垃圾回收所管理的主要区域,也叫GC堆。在虚拟机启动时创建,主要目的是存储对象实例,几乎所有的对象实例都在堆中创建,当堆内存无法完成实例分配,并且堆无法再扩展,将会抛出OOM异常。根据对象实例存活时间的不同,JVM把堆内存进行分代管理,由垃圾收集器管理对象的回收。

2、如何判断对象死亡

引用计数法可达性分析算法

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加1,当引用失效时,计数器值就减1。而主流的Java虚拟机并没有采用引用计数法,最主要原因是它很难解决对象之间相互循环引用的问题。

可达性分析算法

这个算法的思想就是通过一系列称为“GC Roots”对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为一个引用链,当一个对象到“GC Roots”没有任何引用链相连时,证明这个对象是不可用的。

要宣告一个对象真正死亡,至少还需要经历两次标记过程,没有与GCRoots引用相连时会进行一次标记和筛选,如果对象没有覆盖finalize方法或者已经被执行过,那么对象就会被回收;如果对象覆盖了finalize方法并且还没有执行,就会把对象放置到F -Queue队列,对F-Queue队列中进行第二次标记,如果对象再finalize方法拯救了自己,就移除出F-Queue队列,否则就会被回收掉。

哪些可以作为GCRoots对象

虚拟机栈和本地方法栈引用的对象

方法区静态变量和常量引用的对象

3、Java中的引用

强引用、弱引用、软引用和虚引用

强引用

就是类似Object obje = new Object这样的引用,只要强引用存在,垃圾收集器用用不会回收被引用的对象。

软引用

软引用描述的是一些有用但是非必须的对象,对于软引用关联的对象,在系统将要发生内存溢出之前,将这些对象列进回收范围进行二次回收,如果内存还不够,将会抛出OOM异常。

弱引用

弱引用和软引用类似,也是用来描述一些有用但是非必须的对象,区别软引用,弱引用关联的对象只能存活到下次垃圾回收之前。

虚引用

需引用不会决定对象的生命周期,任何时候都可以被垃圾收集器回收掉。

4、方法区垃圾回收

方法区垃圾回收主要包括:废弃常量及无用的类

废弃常量:没有任何对象引用常量池中的常量

无用的类:

该类的所有实例都已经被回收

加载该类的ClassLoader已经被回收

该类对应的java.lang.Class对象没有在任何地方被引用

5、垃圾收集算法

标记-清除

标记-清除算法的思想就是,先标记要回收的对象,标记完成后再同一回收被标记对象。

存在两个问题:效率不高、容易产生大量不连续的内存碎片

标记-整理

标记-整理,就是将所有存活的对象都向一端移动,然后直接清除掉端边界以外的对象。 解决标记-整理容易产生不连续的内存碎片问题

复制算法

复制算法,就是将可用内存分为大小相等的两块,每次只使用其中的一块。当一块内存用完了,将还存活的对象复制到另一块内存上,然后直接把已使用过的内存空间都清除掉。 这种方式,内存代价太大,内存区域不是按照1:1划分,默认按照8:1:1进行划分 分代收集

根据对象的生命周期,一般将Java堆分为新生代和老年代。新生代每次回收时都会由大量对象死亡,只有少量对象存活,因此采用复制算法;而老年代,对象存活率高,没有额外空间对它进行分配担保,因此采用标记-清除或标记整理算法。

6、什么时候触发Minor GC和什么时候出发Full GC

Minor GC触发条件:

当Eden区满时,会触发Minor GC

Full GC触发条件:

当老年代内存空间不够时

持久代(方法区)内存不够时

调用system.gc(),建议系统进行full GC ,但是不会立即执行

Eden区 minor GC 复制到老年代的对象的大小,大于老年代的可用空间大小

年轻代复制对象较大,survivor区空间不足,同时老年代内存空间也不足,则会进行full GC进行回收

7、Java类加载器有哪几种,是什么关系

启动类加载器

负责将JAVA_HOME\lib目录下的类库加载到虚拟机内存中

扩展类加载器

负责加载JAVA_HOME\lib\ext目录下的或者被java.ext.dirs系统变量所指定路径的所有类库

应用程序类加载器

这种类加载器由sun.misc.Launcher$AppClassLoader实现,由于这个类加载器是ClassLoader的getSystemClassLoader()方法的返回值,因此也叫系统类加载器,负责加载用户类路径classpath下的所指定的类库

自定义类加载器

所有的自定义类加载器都是ClassLoader的直接子类或间接子类

8、双亲委派模型的加载流程是怎样的,有什么好处?

如果一个类加载器收到类加载请求时,首先不会自己尝试加载类,而是委托给父类加载器,如果父加载器还存在其父类加载器,则继续向上委托,直到顶层的启动类加载器。如果父类加载器能够加载,则返回;如果父类加载器无法完成此加载任务时,子类加载器才会尝试自己去加载。

好处

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子类加载器再加载一次。

其次是考虑到安全因素,可以防止核心API库被随意篡改。

9、类加载机制

**类加载过程主要分为:加载、连接和初始化,连接又分为:验证、准备、解析

加载

将类的class文件中的二进制数据加载到内存中,并将该字节流所代表的的静态数据结构转换为方法区运行时的数据结构,并在堆中生成该类的java.lang.Class对象,作为访问方法区运行时数据结构的入口。

验证

主要确保被加载的类的正确性

文件格式验证

元数据验证

字节码验证

符号引用验证

准备

为类的静态变量分配内存空间,并将其初始化为默认值

解析

把类的符号引用转换为直接引用

初始化

为类的静态变量赋予正确的初始值,在编译阶段会生成方法,该方法包含了所有类变量的赋值动作和静态代码块的执行代码,在类的主动使用时,会触发了类的初始化,虚拟机就会调用这个方法。

jvm对类的加载是一个延迟的机制,只有当一个类首次主动使用时,才会触发初始化操作。

类的主动触发初始化的场景

通过new关键字

访问类的静态变量,包括访问和修改类的静态变量

访问类的静态方法

对一个类的反射操作

初始化子类时,会导致父类的初始化

启动类,也就是执行了main函数所在的类,会导致该类初始化

10、垃圾收集器

新生代收集器:Serial收集器、ParNew收集器、Parallel Scavenge收集器

老年代收集器:SerialOld收集器、Parallel Old收集器,CMS收集器

新生代和老年代收集器:G1收集器

Serial收集器

Serial收集器是运行在Client模式下默认的新生代收集器,是单线程收集器,在垃圾回收时,会暂停所有其他的工作线程

ParNew收集器

ParNew收集器时Serial收集器的多线程版本

Parallel Scavenge收集器

Parallel Scavenge收集器是新生代并行收集器,它的目标是达到一个可控制的吞吐量。适合后台运算,不需要太多交互的任务。

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

Serial Old收集器

Serial Old收集器,也是单线程收集器,基于标记-整理算法

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法

CMS收集器

CMS收集器是以获取最短回收停顿时间为目标的收集器,是基于标记-清除算法。 整个过程分为:初始标记、并发标记、重新标记和并发清除四个阶段。其中初始标记和重新标记阶段仍然是“stop the world”。

优点

并发收集与低停顿

缺点

对CPU资源敏感,CPU个数少于4个时,CMS对用户程序的影响可能变得很大,为了应付这种情况,虚拟机提供了一种“增量式并发收集器”的CMS收集器变种

无法处理浮动垃圾,在并发清理阶段,伴随着新的垃圾产生,这些垃圾在标记过程之后,当次CMS无法回收这部分垃圾,只能等待下次GC时再清理掉。

CMS是基于标记-清理算法,因此很容产生大量连续的内存碎片

G1收集器

G1收集器是JDK1.7后全新的回收期,用于取代CMS收集器,G1收集器的阶段主要分为:初始标记、并发标记、最终标记、筛选回收四个阶段。

初始标记,标记了GCRoots能直接关联到的对象,这个阶段需要停顿线程

并发标记,是从GC Root开始对堆中对象进行可达性分析,找出存活的对象

最终标记,标记那些在并发标记阶段发生变化的对象,这个阶段需要停顿线程,但是可并行执行

筛选回收,对各个Region的回收价值和成本进行排序,根据用户所期待的GC停顿时间制定回收计划,回收一部分Region

11、常用的GC策略

常见内存回收策略可以从以下几个维度来理解:

串行&并行

串行:单线程执行内存回收工作。十分简单,无需考虑同步等问题,但耗时较长,不适合多cpu。

并行:多线程并发进行回收工作。适合多CPU,效率高。

并发& stop the world

stop the world:jvm里的应用线程会挂起,只有垃圾回收线程在工作进行垃圾清理工作。简单,无需考虑回收不干净等问题。

并发:在垃圾回收的同时,应用也在跑。保证应用的响应时间。会存在回收不干净需要二次回收的情况。

压缩&非压缩©

压缩:在进行垃圾回收后,会通过滑动,把存活对象滑动到连续的空间里,清理碎片,保证剩余的空间是连续的。

非压缩:保留碎片,不进行压缩。

copy:将存活对象移到新空间,老空间全部释放。(需要较大的内存。)

12、JVM内存参数设置

-Xms设置堆的最小空间大小。

-Xmx设置堆的最大空间大小。

-Xmn:设置年轻代大小

-XX:NewSize设置新生代最小空间大小。

-XX:MaxNewSize设置新生代最大空间大小。

-XX:PermSize设置永久代最小空间大小。

-XX:MaxPermSize设置永久代最大空间大小。

-Xss设置每个线程的堆栈大小

13、JDK1.8中的为什么用MetaSpace替换掉了PermGen?MetaSpace存储在哪里?

MetaSpace 元空间是存储在本地内存的,受本地内存空间大小限制

因为:

字符串存在永久代中,容易出现性能问题和内存溢出 类及方法的信息难以确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大容易出现老年代溢出 永久代会为GC带来不必要的复杂度,并且回收效率偏低 Oracle可能会将Hotspot与JRockit合二为一

14、简单描述一下GC的分代回收

目前大多数的垃圾收集器采用的都是分代回收,分代回收的主要思想就是根据对象存活的生命周期将内存划分为若干个不同的区域。

一般将Java堆分为新生代和老年代,新生代的对象都是朝生夕死的,每次垃圾回收都有大量的对象被回收,只有少量对象存活,因此采用复制算法;而老年代中的对象存活率高,没有额外空间进行分配担保,因此就必须使用“标记-清理”或“标记-整理”算法来进行回收

15、垃圾收集算法

标记-清除

首先标记所有需要回收的对象,标记完成后统一回收所有标记的对象

标记-整理

和“标记-整理”算法类似,区别是后续步骤不是直接清除标记回收的对象,而是将所有存活的对象都向一端移动,然后直接清除端边界以外的对象

复制算法

将可用内存空间分为大小相等的两块,每次只使用其中的一块。当一块内存空间用完后,将还存活的对象复制到另一块上,然后直接清除掉这块已使用的内存空间

分代收集

一般将Java堆分为新生代和老年代,新生代的对象朝生夕死,每次垃圾回收都有大部分对象被回收,只有少部分对象存活,因此采用复制算法。而老年代中的对象存活率都比较高,并且没有额外空间进行分配担保,因此就必须使用“标记-清除“或”标记-整理“算法。

16、内存分配及回收策略

对象优先在Eden区分配

大多数情况下,对象在新生代的Eden区出生,当Eden区没有足够空间,将会触发一次Minor GC

大对象直接进入老年代

所谓大对象,就是那些需要连续使用内存空间的Java对象,典型的大对象就是那些很长的字符串或数组等。经常出现大对象容易导致还有很多内存空间时就触发垃圾回收以确保有足够连续的内存空间来安置它们。

长期存活的对象直接进入老年代

虚拟机为每一个对象都定义了一个年龄计数器,对象在Eden区出生并且经历一次minor GC后仍然存活,并且survivor区能容纳的话,对象就被复制到survivor区,并且将对象的年龄设为1,对象在survivor区每经历一次minor GC并且仍然存活的话,就将对象的年龄加1,当对象的年龄到达指定年龄后,会直接进入到老年代

动态对象年龄判断

虚拟机并不总是要求对象必须到达一定年龄后才晋升到老年代,如果survivor空间中相同年龄的对象大于survivor空间所有对象占用内存总和的一半,则大于等于这个年龄的对象直接进入到老年代,无需等到MaxTenuringThreshold所指定的年龄

空间分配担保

在发生Minor GC之前,虚拟机会首先检查老年代最大可用的连续空间是否大于新生代所有对象的空间,如果是,那么进行minor GC就是安全的。否则,就会去判断HandlePromotionFailure设置值是否允许失败,如果允许,则会判断新生代历次晋升到老年代对象的平均大小是否大于老年代的可用空间,如果大于,则进行一次Minor GC,尽管这一次Minor GC可能是危险的,若小于或者不允许冒险,则会进行一次Full GC