0%

JVM

1. 什么是JVM?

JVM是指Java虚拟机,在JVM上可以运行Java编译后的字节码文件,也正是因为JVM使得Java具有跨平台性。

2. 简单介绍下JVM的内存区域?

JVM内存区域有堆、栈、本地方法栈、虚拟机栈和程序计数器。

Java虚拟机运行时数据区

其中方法区和堆是线程共享的。

虚拟机栈:每个线程都有其自己的虚拟机栈,是其私有的,方法执行时,栈帧会存储其局部变量、操作数栈和动态链接。

本地方法栈:本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。

程序计数器:也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。

Java堆:Java堆一般都是最大的区域,里面还细分了很多区域,一般垃圾回收都在堆内进行,并且几乎所有的对象创建也在堆上。

方法区:方法区是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

它特别在 Java 虚拟机规范对它的约束非常宽松,所以方法区的具体实现历经了许多变迁,例如 jdk1.7 之前使用永久代作为方法区的实现。

Java虚拟机栈 Java 堆内存结构

3. 说一下 JDK1.6、1.7、1.8 内存区域的变化?

JDK1.6时使用永久代作为方法区:

JDK 1.6内存区域

JDK1.7时将字符串常量池、静态变量,存放在堆上:

JDK 1.7内存区域

在 JDK1.8 时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间:

JDK 1.8内存区域

4. 运行时常量池、类常量池和字符串常量池分别是什么?

运行时常量池(Runtime Constant Pool)是 Java 虚拟机内存中的一块区域,它存储着在类文件的常量池(Class Constant Pool)中的所有常量,并且在运行时期间可以在程序中被引用到。运行时常量池中存储的内容包括了字符串常量、整型常量、浮点型常量等类型。

类常量池(Class Constant Pool)是在编译期间预先在 class 文件中创建的一个常量池,它存储着类的相关信息,如类名、方法名、字符串常量、整型常量等。

字符串常量池(String Constant Pool)是一个特殊的常量池,它存储着字符串常量,并且使用字符串常量池可以节省内存,因为所有的字符串常量在内存中只有一份拷贝,避免了创建多份字符串常量对象所带来的内存浪费。

5. 为什么用元空间代替永久代?

有主观和客观两方面的原因。

  • 主观上,使用永久代容易产生内存溢出等问题。
  • 客观上,在 Oracle 收购 BEA 获得了 JRockit 的所有权后,为了和JRockit 虚拟机更好兼容,综合考虑替换了。

6. 对象的加载过程?

容易和类的加载过程混淆。

  1. 首先查看类是否加载、解析或初始化过。如果没有,则先执行类的加载过程。
  2. 类检查完成后,JVM将为新的对象分配内存。
  3. 内存分配完成后,JVM将分配到的内存空间初始化为0
  4. 接下来初始化对象头,里面包含了该对象时哪个类的实例、如何找到元数据链接、对象的哈希码以及对象的GC分代年龄等信息。

7. 类的加载过程

  1. 加载。将类通过类加载器从硬盘里的.class文件加载到内存中
  2. 链接。链接包括三个步骤。
    • 验证:验证语法是否正确
    • 准备:为类变量(静态变量)分配内存,并将其初始化为默认值
    • 解析:此时将符号引用转换为直接引用,符号引用是一种指向类或方法的引用,而直接引用是指向实际内存位置的引用。
  3. 初始化:为类变量分配内存,并为其赋值,如果没有赋值则为其默认值。

8. 什么是指针碰撞?什么是空闲列表?

指针碰撞和空闲列表都是分配内存的方式。

  • 指针碰撞:在内存中有一个指针,指针两侧分别是已分配区域和未分配区域,当有新的对象要分配时,指针向未分配区域移动相应距离。
  • 空闲列表:内存中已分配区域和未分配区域是不规整的,由一个表来记录每块区域的相关信息,在需要分配时根据记录表查找合适的区域进行分配,分配完成后再更新记录表。

两种方式的选择由Java堆是否规整决定,而Java堆是否规整由选择的垃圾回收器是否具有压缩整理能力决定。

9. JVM 里 new 对象时,堆会发生抢占吗?JVM 是怎么设计来保证线程安全的?

会发生抢占,假如内存正在给一个对象分配时,指针移动需要修改还没来得及移动,此时另一个线程进来,也需要分配内存,这样就会产生冲突,此时有两个解决办法:

  • 采用CAS算法保证原子性。
  • 事先为每个线程分配一小段缓存区域(本地线程缓冲区),当有新的线程进来时,先分配到缓冲区,如果缓冲区满了或者放不下,再锁定内存区域同步分配内存区域。

10. 对象的内存布局是怎样的?

对象主要有对象头、实例数据、对齐填充三部分。

对象的存储布局
  • 对象头:
    • 类型指针:表示对象代表哪个类。
  • 实例数据:用来存储对象真正的有效信息,也就是我们在程序代码里所定义的各种类型的字段内容,无论是从父类继承的,还是自己定义的。
  • 对齐填充:没有特别含义,相当于占位符。

11. 内存溢出和内存泄漏是什么意思?

内存泄漏指内存没有被正确释放,使内存被白白占用。

内存溢出指内存超出可用内存限制,而溢出。

12. 能手写内存溢出的例子吗?

  • 堆溢出:堆溢出只要不断创建不可被回收的静态变量、静态对象即可
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
  • 虚拟机栈溢出:虚拟机栈存储的是线程,所以不断创建线程早晚会溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* vm参数:-Xss2M
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

*13. 内存泄漏可能由哪些原因造成?

  • 静态集合类:静态集合类生命周期和JVM一样,所以一直不会被释放
1
2
3
4
5
6
7
public class OOM {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
list.add(obj);
}
}
  • 单例模式:和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
  • 数据连接、IO、Socket 等连接:创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收。
1
2
3
4
5
6
7
8
9
10
11
try {
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url", "", "");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("....");
} catch (Exception e) {

}finally {
//不关闭连接
}
  • 变量不合理的作用域:一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。
1
2
3
4
5
6
7
8
9
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代码
//由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放
object = null;
}
}

在《Effective Java》第七条 消除过期的对象引用。在栈弹出操作时,如果是非Java这种没有垃圾回收机制的语言,如果不将其置为空等,就容易发生内存泄漏,因为栈内部还维护着对这些对象的过期引用。所谓过期引用,就是永远不会再被解除的引用。

  • hash值改变:假如在HashMap中的某个值,hash改变后,用相同的key将找不到这个值,从而无法删除。这也是为什么 String 类型被设置成了不可变类型的原因。

String在创建时是会缓存Hash值的,如果改变了就不是其本身了,所以设置成了不可变类型。

  • ThreadLocal使用不当:ThreadLocal 的弱引用导致内存泄漏

14. 解释一下ThreadLocal 的弱引用导致内存泄漏?

ThreadLocal 是 Java 中一种线程级别的数据隔离技术,它可以让每个线程都拥有一份独立的数据副本,从而避免了多个线程之间的数据冲突问题。但是在使用 ThreadLocal 时,如果不注意它的生命周期管理,就可能会导致内存泄漏问题,其中之一就是弱引用导致的内存泄漏。

ThreadLocal 内部维护了一个 Map,用于存储每个线程的数据副本。Map 的 key 是 ThreadLocal 对象的弱引用,value 是对应线程的数据副本。当 ThreadLocal 对象没有被外部强引用时,它就有可能被垃圾回收器回收。但是由于 Map 中的 key 是弱引用,垃圾回收器在回收 ThreadLocal 对象时并不会主动清理对应的 Entry,这就可能导致 Map 中出现 key 为 null 的 Entry,而这些 Entry 对应的 value 就无法被访问,但却一直占用着内存,从而造成内存泄漏。

为了避免这种内存泄漏问题,我们可以通过显式地调用 ThreadLocal 的 remove() 方法来清除当前线程的数据副本,或者在定义 ThreadLocal 变量时使用匿名内部类的方式重写它的 initialValue() 方法,使其返回一个弱引用对象,从而让 ThreadLocal 对象本身成为一个弱引用。这样当 ThreadLocal 对象被垃圾回收时,对应的 Entry 也会被自动清理,避免了内存泄漏问题。

15. 如何判断对象仍然存活?

一般有两种算法:引用计数发和可达性分析法。

引用计数法:简而言之,就是在对象中添加一个引用计数器,当有对象引用时,计数器+1,引用失效时-1,当计数器为0时,则可判断为对象死亡。

可达性分析法:在JVM中主流的垃圾回收器采用的就是本方法,该方法就是将一系列称为 GC Root 的对象作为搜索起始点,由这个起始点出发向下搜索,搜索过的路径称为引用链,当一个对象到 GC Root 没有任何引用链时,则证明此对象是不可用的。

例如object5、object6、object7虽然互有关联,但它们到GC Roots是不可达的,所以它们将会被判定为可回收对象。

GC Root

16. 可作为GC Root的对象有哪些?

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

17. 对象有哪些引用?

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

18. finalize()方法了解吗?有什么作用?

这个方法用的不多。打个比喻,垃圾回收有点像秋后问斩,而 finalize() 有点像刀下留人。

在垃圾回收时,对象在进行可达性分析后发现没有与 GC Root 关联,则会被第一次标记。之后进行第二次筛选,看是否有必要执行 finalize()——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this 关键字)赋值给某个类变量或者对象的成员变量, ,如果没有,则进行回收。

19. Java的堆分区?

堆分为新生代和老年代。新生代又分伊甸园区、幸存者1区、幸存者2区,比例为8:1:1。

Java堆内存划分

20. 介绍下垃圾回收算法?

  • 标记-清除:这种算法较简单,就是标记需要回收的对象,之后执行清理操作。但是也存在着两个缺点

    • 效率会降低:随着对象和需要清理的对象越来越多,执行效率会降低
    • 空间碎片化问题
  • 标记-复制:本算法则是先将内存区域分为两块,每次只使用一块,等到一块使用完后,将还存活的对象复制到另一块上面,然后把使用过的需要清理的对象一次性清理。

    • 本算法缺点就是空间利用率不高,但是由于新生代存活对象不多,每次复制的也只是少量对象,所以采用这个算法。
  • 标记-整理:顾名思义,每次清理完成后,便将还存活的对象向某一端移动。

    • 这种方式在老年代使用较多。

21. 介绍一下新生代内存划分及垃圾回收?

新生代分伊甸园区、幸存者1区、幸存者2区,比例为8:1:1。每次分配内存时,只使用一块伊甸园区和一块幸存者区,当进行垃圾回收时,便将所有的存活区块复制到另一个未使用的幸存者区,之后再进行垃圾回收。

22. Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC 都是什么意思?

部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS 收集器会有单独收集老年代的行为。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。

整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

23. Young GC会在什么时候发生?

新创建的对象优先在新生代 Eden 区进行分配,如果 Eden 区没有足够的空间时,就会触发 Young GC 来清理新生代。

24. 什么时候触发Full FC?

  • 老年代目前连续的可用空间大小 < 新生代以往历次Young GC后升入老年代对象大小总和的平均大小
  • 当Young GC后,没有被回收需要升入老年的对象大小 > 老年代可用空间大小
  • 当老年代空间使用率过高达到一定比例的时
  • 当To区放不下从From区和eden区拷贝的内容时,或新生代达到阈值需要升到老年代的对象,而老年代放不下时
  • 当调用 System.gc()
  • 另外,假如方法区还由永久代实现,如果永久代空间不足也会Full GC

25. 对象在什么情况下会进入老年代?

  • 长期存活的对象,在每次Young GC后,会有一个标记移区年龄,当移区年龄大于15(默认,可设置)时,则会进入老年代。
  • 当新生代相同年龄的对象大小总和大于幸存者区空间大小的一半时,大于等于该年龄的都会进入老年代。
  • 当新生代内对象连续占用的空间过大时(可设置参数),会进入老年代。
  • 在Yuong GC后假如新生代还有大量对象存在,到幸存者区无法容纳时,将会进入老年代

26. 常见的垃圾回收器有哪些?

  • serial收集器:单线程执行,在执行时,需要暂停其他工作线程
  • ParNew收集器:多线程执行,实际上就是serial的多线程版本
  • serial Old收集器:属于serial老年版本,单线程执行,采用标记-整理算法
  • Parallel Old收集器:属于Parallel 老年版本,多线程执行,采用标记-整理算法
  • CMS 收集器:在收集过程中可以与用户线程并发操作,CMS牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。
  • Garbage First 收集器:Garbage First(简称 G1)收集器是垃圾收集器的一个颠覆性的产物,它开创了局部收集的设计思路和基于 Region 的内存布局形式。

27. 什么是Stop The World ? 什么是 OopMap ?什么是安全点?

在垃圾回收的过程中,会涉及到对象的移动。而为了保证对象引用更新的正确性,需要暂停所有的用户线程,像这样的停顿称为 Stop The World

在HotSpot中,有个数据结构叫OopMap,它记录了对象偏移量等计算出来。在即时编译的过程中,会在特定的位置上生成OopMap,这些特定的位置就叫安全点。

特定位置可以是:

  1. 循环的末尾
  2. 方法临返回前
  3. 可能抛出异常的位置

用户程序执行时并不是能够随便停下来的,而是在安全点才能停下来进行垃圾回收。

28. 说一下 CMS 收集器的垃圾收集过程?

  • 初始标记:单线程执行,需要 Stop The World ,标记GC Root能直达的对象
  • 并发标记:无停顿、根据初始标记的对象,遍历整个对象图
  • 重新标记:多线程执行,需要 Stop The World ,根据上一步标记需要清除的对象
  • 并发清除:无停顿,和用户线程同时运行,清除掉所有标记的对象和死亡的对象

29. 说一下 G1收集器的垃圾收集过程?

G1收集器是垃圾收集器的一个颠覆性产物,开创了局部收集的设计思路和基于Region的内存布局形式。

G1虽然也遵循分代回收的设计,但是其划分方式和其他不同。其他收集器是划分新生代、老年代和持久代。但是G1回收器将连续的Java堆分成多个大小相等的区域(Region),每个区域都可以是Eden区、Survivor区和老年代,收集器可以对扮演不同角色的区域采取不同的回收策略。

这样避免回收整个堆,而是根据若干个Region集进行收集,同时维护一个优先级列表,跟踪各个Region的回收价值,优先收集价值高的Region。

G1收集器运行大致可以划分成四个步骤:

  • 初始标记:标记GC Root可直达的对象,Stop The World执行
  • 并发标记:根据GC Root可直达的对象,寻找整个堆内要回收的对象,和用户线程并发执行
  • 最终标记:Stop The World执行,标记上一个阶段产生的垃圾
  • 筛选回收:Stop The World执行,选择多个Region构成回收集,把存活的对象复制到空的Region中,再把旧的Region全部回收

30. 有了CMS,为什么还需要G1?

CMS的优点就是——并发收集、低停顿。同时也有缺点:

  • 会产生较多的内存碎片。
  • CMS并发能力比较依赖CPU的性能,并且并发收集阶段用户程序还在运行,可能会影响用户程序的性能。
  • 并发收集时,用户线程仍在运行,会产生所谓的“浮动垃圾”,如果“浮动垃圾”不在此阶段清除,就要到下一阶段,可能会引发再次 Full GC,影响性能。

而G1主要解决了内存碎片过多的问题。

**31. 对象一定分配在堆中吗?有没有了解逃逸分析技术?

不一定。随着JIT发展不断对代码进行优化,其中有一部分优化的目的是减少内存堆的分配压力,其中一项技术叫做逃逸分析。

逃逸分析,通俗讲,就是对象被new出来以后,它可能被外部调用,如果是作为参数传递到了外部,则称为方法逃逸。

逃逸

除此之外,如果对象还有可能被外部线程访问到,例如赋值给可以在其它线程中访问的实例变量,这种就被称为线程逃逸。

逃逸分析的好处:

  • 栈上分配:如果确定一个对象不会逃逸到线程之外,那么久可以考虑将这个对象在栈上分配,对象占用的内存随着栈帧出栈而销毁,这样一来,垃圾收集的压力就降低很多。
  • 同步消除:由于线程同步是一个较为耗时的工作,假如一个对象不会逃逸出当前线程,无法被其他线程访问,则可以不进行同步。
  • 标量替换:如果一个数据是基本数据类型,不可拆分,它就被称之为标量。把一个 Java 对象拆散,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么可以不创建对象,直接用创建若干个成员变量代替,可以让对象的成员变量在栈上分配和读写。

32. 一个类的加载过程是怎样的?

  • 首先根据全限定类名获取此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转换为方法区运行时数据结构。
  • 在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类各种数据的访问入口。

33. 类加载器有哪些?

  • 启动类(引导类)加载器:用来加载java核心类库。
  • 扩展类加载器:它用来加载 Java 的扩展库。
  • 系统类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。
  • 用户自定义类加载器:用户通过继承 java.lang.ClassLoader 类的方式自行实现的类加载器。

34. 什么是双亲委派机制?

如果一个类加载器收到了类的加载请求,一般不会自己先去尝试加载这个类,而是委派给父类,一层层向上委派,当父类表示无法加载时,子加载器才会尝试自己去完成加载。

35. 为什么要使用双亲委派机制?

为了保证系统的稳定有序。

比如用户自己写了一个名为java.lang.Object类,放在程序的ClassPath中,如果由各个类加载器自行去加载的话,那么系统中就会出现多个不同的Object类。

36. 如何自己实现一个热部署功能?

一个类加载首先通过 Java 编译器,将 Java 文件编译成 class 字节码,类加载器读取 class 字节码,再将类转化为实例,对实例 newInstance 就可以生成对象。

类加载器 ClassLoader 功能,也就是将 class 字节码转换到类的实例。在 Java 应用中,所有的实例都是由类加载器,加载而来。

一般在系统中,类的加载都是由系统自带的类加载器完成,而且对于同一个全限定名的 java 类(如 com.csiar.soc.HelloWorld),只能被加载一次,而且无法被卸载。

既然在类加载器中,Java 类只能被加载一次,并且无法卸载。那么我们可以把类加载器去掉并自定义类加载器,重写 ClassLoader 的 findClass 方法(此处可以看下源码,各个类加载器都继承 ClassLoader 类,并重写了findClass方法)

这样实现步骤大致就是:

  1. 销毁原来的类加载器
  2. 更新class类文件
  3. 自定义类加载器并重写findClass方法去加载更新后的文件

37. Tomcat 的类加载机制了解吗?

Tomcat类加载器

Tomcat破坏了双亲委派机制。这是因为Tomcat中可能会部署多个应用,如果多个应用依赖的某一个Jar版本不同,这样使用双亲委派机制,无法加载多个相同的类,因为双亲委派机制就是保证系统的稳定有序,让其加载相同的类。

所以Tomcat提供了隔离机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。每一个 WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交 CommonClassLoader 加载,这和双亲委派刚好相反。

欢迎关注我的其它发布渠道