一、JVM内存区域划分

根据Java虚拟机规范,JVM内存结构如下:

1. 方法区

属于内存共享区域,存储被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据。

2. 堆

这是JVM管理的内存最大的部分,线程共享,主要存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(线程工作区)。会抛出OutOfMemoryError。

3. 虚拟机栈

线程私有,生命和线程一致。描述的Java方法执行的内存模型:每个方法调用时,都会创建一个栈帧,用于存储局部变量表,操作数栈、动态链接、方法出口等信息。每个方法从调用到结束,对应着一个栈帧的入栈和出栈。

4. 本地方法栈

区别于Java虚拟机栈,Java虚拟机栈为虚拟机执行Java方法服务;本地方法栈则为虚拟机使用到的Native方法服务。

5. 程序计数器

线程私有,指向下一条需要执行的字节码指令。

6. 运行时常量池

属于方法区的一部分,用于存放编译器生成的各种字面量和符号引用。intern()方法可以将常量放入池中。

二、垃圾回收

1. 如何判断对象是垃圾

在进行内存回收之前,首先的事情是要判断那些对象是可以回收的,那些是不可以回收的。

(1) 引用计数算法

方法:通过判断对象的引用数量来决定对象是否可回收。

具体实现:每一个对象都有一个引用计数器,被引用则+1,引用失效后-1。如果一个对象的引用为0,那么代表该对象可以被回收。

优点:执行效率高

缺点:无法解决循环引用的问题,导致内存泄漏

(2) 可达性分析方法

方法:通过判断对象的引用链是否可达来决定对象是否可以被回收。

具体实现:通过一系列的GC Root对象作为起点,从这些节点出发所走过的路径称为引用链,如果一个对象没有在引用链中,说明该对象可以被回收。

(3) 可以作为GC Root的对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的的对象
  • 方法区中的常量引用对象
  • 方法区静态属性引用对象
  • 本地方法栈JNI(Native方法)中引用的对象

(4) Java中的引用类型

  • 强引用:任何时候都不会被回收,最常见的引用
  • **软引用(Soft Reference)**:只有当内存不足时才被回收
  • **弱引用(Weak Reference)**:只要发生垃圾回收(无论内存是否足够),弱引用指向的对象就被回收
  • 虚引用:任何时候都可能被垃圾收集器回收

2.垃圾回收算法

(1) 标记-清理算法

标记:通过可达性分析,对存活的对象进行标记

清理:对堆内存进行线性遍历,回收不可达对象的内存

问题:出现内存碎片,效率不高

(2) 复制算法

方法:将内存空间分为两块S1, S2,每次只使用其中的一块 S1,当S1内存不足时,就将该块空间中存货的对象复制到另一块S2。然后令S2作为当前的工作空间,S1进行全面清理。

问题:总有一半的空间闲置,空间利用率低下。

优点:不会产生空间碎片,直接复制,简单高效,适用于对象存活率低的场景(新生代)

改进:由于大多数对象都是朝生熄灭,没有必要按1:1划分空间,可以分为一块较大的Eden区和两个较小的Survivor ,每次使用Eden和其中的一块Survivor。回收时,将存活的对象复制到闲置的Survivor区,然后清理Eden和Survivor空间。

(3) 标记-整理算法

标记:通过可达性分析,对存活的对象进行标记

整理:将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

优点:不会产生空间碎片,不用浪费空间,适用于对象存活率高的场景(老年代)

问题:效率比较低

(4) 分代回收算法

​ 根据对象的生命周期不同划分区域,各区域采用不同的垃圾回收算法,以提高JVM回收效率。JDK8及其以后版本将堆内存分为新生代和老年代:

新生代:对象的生命周期短(1/3堆空间),大部分是朝生熄灭,采用复制算法回收。分为一块较大的Eden区和两个较小的Survivor区(一个from区,一个to区)(1:1:8),每次使用Eden和其中的一块Survivor(成为from区)。回收时,将存活的对象复制到闲置的Survivor区(即to区),然后清理Eden和Survivor空间(from区)。这里注意,对象总是首先在Eden区创建,如果Eden区不足,会在Survivor区创建。

老年带:对象的生命周期长(2/3堆空间),采用标记-清除算法或标记-整理算法回收。

对象如和晋升到老年代:经历一定Minor-GC次数依然存活的对象,Survivor区放不下的对象,新生成的较大对象。

(5)Gc的分类

Minor-GC:只回收新生代

Full-GC:新生代和老年带都回收(整个堆空间)

(6)触发Full-GC的条件

  • 老年代空间不足

  • 永久代空间不足(JDK7之前,永久代在堆上,JDK8以后,使用元空间替代了永久代,放到了内存中,降低Full-GC的频率)

  • CMS GC时出现promotion failed, concurrent mode failure

  • Minor-GC晋升到老年代的平均大小大于老年代的剩余空间

(7)stop-the-world

​ JVM由于执行垃圾回收而终止了应用程序的运行,在所有垃圾回收算法中都会发生,多数GC算法通过减少stop-the-world发生的时间来提高程序性能。

3.垃圾回收器

(1) Serial收集器(-XX:+UseSerialGC,复制算法)

  • 单线程收集,进行垃圾收集时,需要暂停所有工作线程(stop-the-world)

  • 简单高效,Client模式下默认的年轻代收集器。

(2) ParNew收集器(-XX:+UseParNewGC,复制算法)

  • 多线程进行垃圾回收,其余与Serial收集器一样。

  • 单核效率不如Serial,在多核下执行才有优势

(3) Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)

  • 比起更关注用户线程的停顿时间,更关注系统的吞吐量

  • 多核下才有优势,Server模式下的默认新生代垃圾收集器

(4) Serial Old收集器(-XX:+UseSerialOldGC,标记-整理)

  • 单线程收集,进行垃圾收集时,必须暂停其他工作线程。

  • 简单高效,Client模式下默认的老年代收集器。

  • 示意图见Serial老年代部分

(5)Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理)

  • 吞吐量优先收集器,多线程,

  • 示意图见Parallel

(6) CMS收集器(-XX:+UseConcMarkSweepGC,标记-清理)–重点

  • 初始标记:标记GC Roots能直接关联到的对象,需要stop-the-world

  • 并发标记:并发地进行GC Roots Tracing,不会暂停应用程序

  • 重新标记:修正并发标记中变动地部分,需要stop-the-world

  • 并发清理:清理垃圾对象,不会暂停

(7) G1收集器(-XX:+UseG1GC,复制+标记-整理)–重点

  • 优点:并行并发、分代收集、空间整合、可预测地停顿

  • 将整个Java堆内存划分为多个大小相等地Region

  • 年轻代和老年代不再物理隔离

三、JVM调优

常用调优参数:

  • -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
  • -XX:NewRatio:老年代和新生代内存大小比值
  • -XX:MaxTenuingThresold:对象从新生代晋升到老年代经过的GC次数最大阈值

四、相关面试题

1. Object的finalize()方法是否和C++中地析构函数相同

不同,C++中的析构函数的调用是确定的,而finalize方法不一定会执行。

在java中,一个对象被回收要经历两次标记。如果对象通过GC Roots进行可达性分析后发现没有与引用链相连,就会被第一次标记,并且判断是否执行finalize方法,如果对象覆盖了finalize方法,且未被引用过,这个对象就会被放到F-Queue队列。虚拟机会有一个低优先级的线程区执行队列中对象的finalize方法。但是线程的优先级很低,并不保证finalize方法一定会执行。

finalize事实上是给与了对象一次最后重生的机会。

2. java中的强引用、软引用、弱引用、虚引用

  • 强引用: 被强引用指向的对象,一直不会被回收
  • 软引用:被强引用指向的对象,当虚拟机内存不足时,才会被回收
  • 弱引用:被弱引用指向的对象,只要发生垃圾回收,就会被回收
  • 虚引用:和不存在一样,随时可能被回收

3. 对象的创建过程

类加载阶段:

加载:加载class文件

验证:验证calss文件的正确性

准备:为类变量分配内存并设置类变量初始值

解析:将符号引用转为直接引用

初始化:执行类中定义的Java程序代码,静态代码块