Skip to content

Latest commit

 

History

History
597 lines (328 loc) · 38.1 KB

JVM 那点破事.md

File metadata and controls

597 lines (328 loc) · 38.1 KB
title
第四篇:JVM !内存结构、垃圾收集、OOM、双亲委派

JVM 那点破事!内存结构、垃圾收集、OOM、双亲委派

作者:Tom哥
公众号:微观技术
博客:https://offercome.cn
人生理念:知道的越多,不知道的越多,努力去学

JVM 内存结构?

答案:

JAVA 程序在运行的过程中会把他管理的内存划分为若干个不同的数据区域,每一块儿的数据区域所负责的功能都是不同的,他们也有不同的创建时间和销毁时间。紫色区域代表是线程共享的区域。

1、程序计数器

程序计数器是程序控制流的指示器,循环,跳转,异常处理,线程的恢复等工作都需要依赖程序计数器去完成。程序计数器是线程私有的,它的生命周期是和线程保持一致的,我们知道,N 个核数的 CPU 在同一时刻,最多有 N个线程同时运行,在我们真实的使用过程中可能会创建很多线程,JVM 的多线程其实是通过线程轮流切换,分配处理器执行时间来实现的。既然涉及的线程切换,所以每条线程必须有一个独立的程序计数器。

2、虚拟机栈

虚拟机栈,其描述的就是线程内存模型,也可以称作线程栈,也是每个线程私有的,生命周期与线程保持一致。在每个方法执行的时候,jvm 都会同步创建一个栈帧去存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法的生命周期就贯彻了一个栈帧从入栈到出栈的全部过程。

3、本地方法栈

本地方法栈的概念很好理解,我们知道,java底层用了很多c的代码去实现,而其调用c端的方法上都会有native,代表本地方法服务,而本地方法栈就是为其服务的。

4、堆 可以说是 JVM 中最大的一块儿内存区域了,它是所有线程共享的,不管你是初学者还是资深开发,多少都会听说过堆,毕竟几乎所有的对象都会在堆中分配。

5、方法区

方法区也是所有线程共享的区域,它存储了被 jvm 加载的类型信息、常量、静态变量等数据。运行时常量池就是方法区的一部分,编译期生成的各种字面量与符号引用就存储在其中。

6、直接内存

这部分数据并不是 JVM 运行时数据区的一部分,nio 就会使用到直接内存,也可以说堆外内存,通常会配合虚引用一起去使用,就是为了资源释放,会将堆外内存开辟空间的信息存储到一个队列中,然后GC会去清理这部分空间。堆外内存优势在 IO 操作上,对于网络 IO,使用 Socket 发送数据时,能够节省堆内存到堆外内存的数据拷贝,所以性能更高。看过 Netty 源码的同学应该了解,Netty 使用堆外内存池来实现零拷贝技术。对于磁盘 IO 时,也可以使用内存映射,来提升性能。另外,更重要的几乎不用考虑堆内存烦人的 GC 问题。但是既然是内存。也会受到本机总内存的限制,

什么是 JVM 内存模型 ?

答案:

Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。

这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系:

  • 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
  • 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作
  • volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
  • 线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作
  • 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
  • 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C

怎么理解 happens-before 呢?如果按字面意思,比如第二个规则,线程(不管是不是同一个)的解锁动作发生在锁定之前?这明显不对。happens-before 也是为了保证可见性,比如那个解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果。

Java 提供了几种语言结构,包括 volatile、final 和 synchronized,它们旨在帮助程序员向编译器描述程序的并发要求,其中:

  • volatile : 保证可见性和有序性
  • synchronized : 保证可见性和有序性; 通过管程(Monitor)保证一组动作的原子性
  • final : 通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性(如果this 引用逃逸就不好说可见性了)

编译器在遇到这些关键字时,会插入相应的内存屏障,保证语义的正确性。

有一点需要注意的是,synchronized 不保证同步块内的代码禁止重排序,因为它通过锁保证同一时刻只有一个线程访问同步块(或临界区),也就是说同步块的代码只需满足 as-if-serial 语义 , 只要单线程的执行结果不改变,可以进行重排序。

所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性。另外,还确保正确同步的Java 代码可以在不同体系结构的处理器上正确运行。

as-if-serial 是什么?单线程的程序一定是顺序的吗?

答案:

as-if-serial 意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

垃圾对象是怎么找到的?

答案:

1、引用计数算法

就是给对象添加一个计数器,每当有一个地方引用它的时候,计数器就加1。每当有一个引用失效的时候,计数器就减1

**当计数器的值为0的时候,那么该对象就是垃圾了。**这种方案的原理很简单,而且判定的效率也非常高,但是却可能会有其他的额外情况需要考虑。

比如两个对象循环引用,a 对象引用了 b 对象,b 对象也引用了 a 对象,a、b 对象却没有再被其他对象所引用了,其实正常来说这两个对象已经是垃圾了,因为没有其他对象在使用了,但是计数器内的数值却不是 0,所以引用计数算法就无法回收它们。这种算法是比较直接的找到垃圾,然后去回收,也被称为"直接垃圾收集"。

2、可达性分析法

这也是JVM 默认使用的寻找垃圾算法它的原理就是定义了一系列的根,我们把它称为 "GC Roots" ,从 "GC Roots" 开始往下进行搜索,走过的路径我们把它称为 "引用链" ,当一个对象到 "GC Roots" 之间没有任何引用链相连时,那么这个对象就可以被当做垃圾回收了。

如图,根可达算法就可以避免计数器算法不好解决的循环引用问题,Object 6、Object 7、Object 8 彼此之间有引用关系,但是没有与"GC Roots" 相连,那么就会被当做垃圾所回收

GC Roots 有哪些?

答案:

固定的GC Roots 对象不固定的临时GC Roots对象。

固定的GC Roots:

  • 虚拟机栈(栈帧的本地变量表)中所引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如 Java 类的引用静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池中的引用。
  • 在方法区栈中 JNI (譬如 Native 方法)引用的对象
  • Java 虚拟机内部的引用,如基本数据类型对应的 Class 对象,一些常驻的异常对象(空指针异常、OOM等),还有类加载器。
  • 所有被 Synchronized 持有的对象
  • 反应 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调本地代码缓存等

临时GC Roots:

  • 为什么会有临时的 GC Roots ?:目前的垃圾回收大部分都是分代收集和局部回收,如果只针对某一部分区域进行局部回收,那么就必须要考虑的当前区域的对象有可能正被其他区域的对象所引用,这时候就要将这部分关联的对象也添加到 GC Roots 中去来确保根可达算法的准确性。这种算法是利用了逆向思维,找到使用的对象,剩下的就是垃圾,也被称为"间接垃圾收集"。

四种引用类型 ?

答案:

1、强引用

"Object o = new Object()" 就是一种强引用关系,这也是我们在代码中最常用的一种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。

2、软引用

当内存空间不足时,就会回收软引用对象。

// 软引用   
SoftReference<String> softRef = new SoftReference<String>(str);

软引用用来描述那些有用但是没必要的对象。

3、弱引用

弱引用要比软引用更弱一点,它只能够存活到下次垃圾回收之前。也就是说,垃圾回收器开始工作,会回收掉所有只被弱引用关联的对象。

WeakReference<String> weakRef = new WeakReference<String>(str);

ThreadLocal 中就使用了弱引用来防止内存泄漏。

4、虚引用

虚引用是最弱的一种引用关系,它的唯一作用是用来作为一种通知。如零拷贝(Zero Copy),开辟了堆外内存,虚引用在这里使用,会将这部分信息存储到一个队列中,以便于后续对堆外内存的回收管理。

分代收集理论

答案:

大多数的垃圾回收器都遵循了分代收集的理论进行设计,它建立在两个分代假说之上:

  • **弱分代假说:**绝大多数对象都是朝生夕灭的。
  • **强分代假:**熬过越多次数垃圾回收过程的对象就越难消亡。

这两种假说的设计原则都是相同的:垃圾收集器应该将jvm划分出不同的区域,把那些较难回收的对象放在一起(一般指老年代),这个区域的垃圾回收频率就可以降低,减少垃圾回收的开销。剩下的区域(一般指新生代)可以用较高的频率去回收,并且只需要去关心那些存活的对象,也不用标记出需要回收的垃圾,这样就能够以较低的代价去完成垃圾回收。

  • 跨代引用假说:如果某个新生代的对象存在了跨代引用,但是老年代的对象是很难消亡的,那么随着时间的推移,这个新生代对象也会慢慢晋升为老年代对象,那么这种跨代引用也就被消除了。

由于跨代引用是很少的,所以我们不应该为了少量的跨代引用去扫描整个老年代的数据,只需要在新生代对象建立一个记忆集来记录引用信息。记忆集:将老年代分为若干个小块,每块区域中有 N 个对象,在对象引用信息发生变动的时候来维护记忆集数据的准确性,这样每次发生了 "Minor GC" 的时候只需要将记忆集中的对象添加到 "GC Roots" 中就可以了。

垃圾回收算法?

答案:

1、标记清除

这种算法的实现是很简单的,有两种方式

  • 标记出垃圾,然后清理掉
  • 标记出存货的对象,回收其他空间

缺点:

  • 随着对象越来越多,那么所需要消耗的时间就会越来越多
  • 标记清除后会导致碎片化,如果有大对象分配很有可能分配不下而出发另一次的垃圾收集动作

2、标记复制

这种算法解决了第一种算法碎片化的问题。就是开辟两块完全相同的区域,对象只在其中一篇区域内分配,然后标记出那些存活的对象,按顺序整体移到另外一个空间,如下图,可以看到回收后的对象是排列有序的,这种操作只需要移动指针就可以完成,效率很高,之后就回收移除前的空间

缺点:

  • 浪费过多的内存,使现有的可用空间变为原先的一半

3、标记整理法

这种算法可以说是结合了前两种算法,既有标记删除,又有整理功能。

这种算法就是通过标记清除算法找到存活的对象,然后将所有存活的对象,向空间的一端移动,然后回收掉其他的内存。

什么是 STW ?

答案:

Java 中Stop-The-World机制简称 STW ,是在执行垃圾收集算法时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java 中一种全局暂停现象,全局停顿,所有 Java 代码停止,native 代码可以执行,但不能与 JVM 交互。

为什么需要 STW ?

答案:

在 java 应用程序中引用关系是不断发生变化的,那么就会有会有很多种情况来导致垃圾标识出错。想想一下如果 Object a 目前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他对象指向了 Object a,那么此刻 Object a 又不是垃圾了,那么如果没有 STW 就要去无限维护这种关系来去采集正确的信息。再举个例子,到了秋天,道路上洒满了金色的落叶,环卫工人在打扫街道,却永远也无法打扫干净,因为总会有不断的落叶。

垃圾回收器是怎样寻找 GC Roots ?

答案:

我们在前面说明了根可达算法是通过 GC Roots 来找到存活的对象的,也定义了 GC Roots,那么垃圾回收器是怎样寻找GC Roots 的呢?首先,为了保证结果的准确性,GC Roots枚举时是要在STW的情况下进行的,但是由于 JAVA 应用越来越大,所以也不能逐个检查每个对象是否为 GC Root,那将消耗大量的时间。一个很自然的想法是,用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 GC 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 OopMap (ordinary object pointer) 普通对象指针的数据结构来记录这类信息。

OopMap 是做什么的?有什么好处?

答案:

我们知道,一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。使用 OopMap 可以避免全栈扫描,加快枚举根节点的速度。但这并不是它的全部用意。它的另外一个更根本的作用是,可以帮助 HotSpot 实现准确式 GC (即使用准确式内存管理,虚拟机可用知道内存中某个位置的数据具体是什么类型) 。

什么是安全点?

答案:

从线程角度看,安全点可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的。比如:方法调用、循环跳转、异常跳转等这些地方才会产生安全点。如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停所有活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待 GC 结束。那么如何让线程在垃圾回收的时候都跑到最近的安全点呢?这里有两种方式

  • 抢先式中断
    • 抢先式中断:就是在stw的时候,先让所有线程完全中断,如果中断的地方不在安全点上,然后再激活直到运行到安全点的位置再中断。
  • 主动式中断
    • 主动式中断:在安全点的位置打一个标志位,每个线程执行都去轮询这个标志位,如果为真,就在最近的安全点挂起。

安全区域是什么?解决了什么问题

答案:

刚刚说到了主动式中断,但是如果有些线程处于sleep状态怎么办呢?

为了解决这种问题,又引入了安全区域的概念安全区域是指在一段代码片中,引用关系不会发生改变,实际上就是一个安全点的拓展。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里 JVM 要发起 GC 时,就不用管标识自己为“安全区域”状态的线程了,该线程只能乖乖的等待根节点枚举完或者整个GC过程完成之后才能继续执行。

垃圾回收器?

答案:

前面和大家聊了很多垃圾收集算法,所以在真正实践的时候会有多种选择,垃圾回收器就是真正的实践者,接下来就和大家聊聊10种垃圾回收器

1、Serial

Serial是一个单线程的垃圾回收器,采用复制算法负责新生代的垃圾回收工作,可以与 CMS 垃圾回收器一起搭配工作。

在 STW 的时候只会有一条线程去进行垃圾收集的工作,所以可想而知,它的效率会比较慢。但是他确是所有垃圾回收器里面消耗额外内存最小的,没错,就是因为简单。

2、ParNew

ParNew 是一个多线程的垃圾回收器,采用复制算法负责新生代的垃圾回收工作,可以与CMS垃圾回收器一起搭配工作。

它其实就是 Serial 的多线程版本,主要区别就是在 STW 的时候可以用多个线程去清理垃圾。

3、Pararllel Scavenge

Pararllel Scavenge 是一个多线程的垃圾回收器,采用复制算法负责新生代的垃圾回收工作,可以与 Serial Old , Parallel Old 垃圾回收器一起搭配工作。

是与 ParNew 类似,都是用于年轻代回收的使用复制算法的并行收集器,与 ParNew 不同的是,Parallel Scavenge 的目标是达到一个可控的吞吐量。吞吐量=程序运行时间/(程序运行时间+GC时间)。如程序运行了99s,GC耗时1s,吞吐量=99/(99+1)=99%。Parallel Scavenge 提供了两个参数用以精确控制吞吐量,分别是用以控制最大 GC 停顿时间的 -XX:MaxGCPauseMillis 及直接控制吞吐量的参数 -XX:GCTimeRatio.停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效的利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

4、Serial Old

Serial Old 是一个单线程的垃圾回收器,采用标记整理算法负责老年代的垃圾回收工作,有可能还会配合 CMS 一起工作。

其实它就是 Serial 的老年代版本,整体链路和 Serial 大相径庭。

5、Parallel Old

Parallel Old 是一个多线程的垃圾回收器,采用标记整理算法负责老年代的垃圾回收工作,可以与 Parallel Scavenge 垃圾回收器一起搭配工作。

Parallel Old 是 Pararllel Scavenge 的老年代版本,它的设计思路也是以吞吐量优先的,ps+po 是很常用的一种组合。

6、CMS

CMS可以说是一款具有"跨时代"意义的垃圾回收器,支持了和用户线程一起工作,做到了一起并发回收垃圾的"壮举"。

  • 1.初始标记
    • 初始标记只是标记出来和 GC Roots 直接关联的对象,整个速度是非常快的,为了保证标记的准确,这部分会在 STW 的状态下运行。
  • 2.并发标记
    • 并发标记这个阶段会直接根据第一步关联的对象找到所有的引用关系,这一部分时刻用户线程并发运行的,虽然耗时较长,但是不会有很大的影响。
  • 3.重新标记
    • 重新标记是为了解决第二步并发标记所导致的标错情况,这里简单举个例子:并发标记时a没有被任何对象引用,此时垃圾回收器将该对象标位垃圾,在之后的标记过程中,a又被其他对象引用了,这时候如果不进行重新标记就会发生误清除。这部分内容也是在STW的情况下去标记的。
  • 4.并发清除
    • 这一步就是最后的清除阶段了,将之前真正确认为垃圾的对象回收,这部分会和用户线程一起并发执行。

CMS的三个缺点

  • 1.影响用户线程的执行效率

    • CMS默认启动的回收线程数是(处理器核心数 + 3)/ 4 ,由于是和用户线程一起并发清理,那么势必会影响到用户线程的执行速度,并且这个影响随着核心线程数的递减而增加。所以 JVM 提供了一种 "增量式并发收集器"的 CMS 变种,主要是用来减少垃圾回收线程独占资源的时间,所以会感觉到回收时间变长,这样的话单位时间内处理垃圾的效率就会降低,也是一种缓和的方案。
  • 2.会产生"浮动垃圾"

    • 之前说到 CMS 真正清理垃圾是和用户线程一起进行的,在清理这部分垃圾的时候用户线程会产生新的垃圾,这部分垃圾就叫做浮动垃圾,并且只能等着下一次的垃圾回收再清除。
  • 3.会产生碎片化的空间

    • CMS 是使用了标记删除的算法去清理垃圾的,而这种算法的缺点就是会产生碎片化,后续可能会导致大对象无法分配从而触发和 Serial Old 一起配合使用来处理碎片化的问题,当然这也处于 STW的情况下,所以当 java 应用非常庞大时,如果采用了 CMS 垃圾回收器,产生了碎片化,那么在 STW 来处理碎片化的时间会非常之久。

7、G1

G1(Garbage First):顾名思义,垃圾回收第一,官方对它的评价是在垃圾回收器技术上具有里程碑式的成果。G1 回收的目标不再是整个新生代,不再是整个老年代,也不再是整个堆了。G1 可以面向堆内存的任何空间来进行回收,衡量的标准也不再是根据年代来区分,而是哪块空间的垃圾最多就回收哪块儿空间,这也符合 G1 垃圾回收器的名字,垃圾第一,这就是 G1 的 Mixed GC 模式。当然我的意思是垃圾回收不根据年代来区分,但是 G1 还是根据年代来设计的,我们先来看下 G1 对于堆空间的划分:

G1 垃圾回收器把堆划分成一个个大小相同的Region,每个 Region 都会扮演一个角色,H、S、E、O。E代表伊甸区,S代表 Survivor 区,H代表的是 Humongous(G1用来分配大对象的区域,对于 Humongous 也分配不下的超大对象,会分配在连续的 N 个 Humongous 中),剩余的深蓝色代表的是 Old 区,灰色的代表的是空闲的 region。在 HotSpot 的实现中,整个堆被划分成2048左右个 Region。每个 Region 的大小在1-32 MB之间,具体多大取决于堆的大小。在并发标记垃圾时也会产生新的对象,G1 对于这部分对象的处理是这样的:将 Region 新增一块并发回收过程中分配对象的空间,并为此设计了两个 TAMS(Top at Mark Start)指针,这块区域专门用来在并发时分配新对象,有对象新增只需要将 TAMS 指针移动下就可以了,并且这些新对象默认是标记为存活,这样就不会干扰到标记过程

但是这种方法也会有个问题,有可能垃圾回收的速度小于新对象分配的速度,这样会导致 "Full GC" 而产生长时间的 STW。在 G1 的设计理念里,最小回收单元是 Region,每次回收的空间大小都是Region的N倍,那么G1是怎么选择要回收哪块儿区域的呢?G1 会跟踪各个 Region 区域内的垃圾价值,和回收空间大小回收时间有关,然后维护一个优先级列表,来收集那些价值最高的Reigon区域。

G1 回收过程?

答案:

1、初始标记(会STW)

仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

2、并发标记

从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。

  1. 最终标记(会STW)

对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。

  1. 清理阶段(会STW)

更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。

Minor GC 和 Full GC 区别?

答案:

Minor GC:只收集新生代的GC。

Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

1、通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。

2、老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。

3、由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

4、调用System.gc时,系统建议执行Full GC

三色标记?

答案:

这里我们又提到了一个概念叫做 SATB 原始快照,关于SATB会延伸出有一个概念,三色标记算法,也就是垃圾回收器标记垃圾的时候使用的算法,这里我们简单说下:将对象分为三种颜色

  • 白色:没被 GC 访问过的对象(被 GC 标记完后还是白色代表是垃圾)
  • 黑色:存活的对象
  • 灰色:被 GC 访问过的对象,但是对象引用链上至少还有一个引用没被扫描过

我们知道在 并发标记 的时候 可能会 出现 误标 的情况,这里举两个例子:

  • 1.刚开始标记为 垃圾 的对象,但是在并发标记过程中 变为了存活对象
  • 2.刚开始标记为 存活 的对象,但是在并发标记过程中 变为了垃圾对象

第一种情况影响还不算很大,只是相当于垃圾没有清理干净,待下一次清理的时候再清理一下就好了。第二种情况就危险了,正在使 用的对象的突然被清理掉 了,后果会很严重。那么 产生上述第二种情况的原因 是什么呢?

  • 1.新增 一条或多条 黑色到白色 对象的新引用
  • 2.删除 灰色 对象 到该白色对象 的直接 引用或间接引用。

当这两种情况 都满足 的时候就会出现这种问题了。所以为了解决这个问题,引入了 增量更新 (Incremental Update)和 原始快照 (SATB)的方案:

  • 增量更新破坏了第一个条件:增加新引用时记录 该引用信息,在后续 STW 扫描中重新扫描(CMS的使用方案)。
  • 原始快照破坏了第二个条件:删除引用时记录下来,在后续 STW 扫描时将这些记录过的灰色对象为根再扫描一次(G1的使用方案)。

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

答案:

1、栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个 栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;

2、当线程请求的栈深度超过了虚拟机允许的最大深度,会抛出StackOverFlowError异常,方法递归调用容易出现该问题;

3、通过参数-xss调整 jvm 栈的大小

如何排查 OOM 问题?

答案:

1、增加两个参数 -XX:+HeapDumpOnOutOfMemoryError、 -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目 录;

2、同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;

3、使用 MAT 工具载入 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间 长了就会内存溢出,可以把改为弱引用

类加载机制是什么?

答案: 类加载机制:

  • Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被 Jvm 可以直接使用的类型,这个过程就可以成为虚拟机的类加载机制。

加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,是依次有序的。但是解析阶段有可能会在初始化之后才会进行,这是为了支持 Java 动态绑定的特性。

动态绑定:

  • 在运行时根据具体对象的类型进行绑定。提供了一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。

类加载过程?

答案:

1、加载,加载分为三步:

  • 通过类的全限定性类名获取该类的二进制流;
  • 将该二进制流的静态存储结构转为方法区的运行时数据结构;
  • 在堆中为该类生成一个class对象;

2、验证:验证该class文件中的字节流信息符合虚拟机的要求,不会威胁到 jvm 的安全;

3、准备:为class对象的静态变量分配内存,初始化其初始值;

4、解析:该阶段主要完成符号引用转化成直接引用;

5、初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程

常见的类加载器有哪些?

答案:

类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:

1、启动类加载器(BootStrapClassLoader):用来加载java核心类库,无法被java程序直接引用;

2、扩展类加载器(Extension ClassLoader):用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;

3、应用类加载器(AppClassLoader):它根据java的类路径来加载类,一般来说,java应用的类都是 通过它来加载的;

4、自定义类加载器:由java语言实现,继承自ClassLoader

什么是双亲委派 ?

答案:

双亲委派模型:简而言之,就是说一个类加载器收到了类加载的请求,不会自己先加载,而是把它交给自己的父类去加载,层层迭代

如果应用程序类加载器收到了一个类加载的请求,会先给扩展类加载器,然后再给启动类加载器,如果启动类加载器无法完成这个类加载的请求,再返回给扩展类加载器,如果扩展类加载器也无法完成,就返回给应用类加载器。

好处:

  • 说这个问题前我要先和大家说一个概念,JVM中类的唯一性是由类本身和加载这个类的类加载器决定的。简单的说,如果有个a类,如果被两个不同的类加载器加载,那么他们必不相等。你看到这里会不会想到所有类的父类都是 Object 是怎么实现的了吗?是因为无论哪一个类加载器加载 Object 类,都会交给最顶层的启动类加载器去加载,这样就保证了 Object 类在 JVM 中是唯一的

如何打破双亲委派模型?

答案:

自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法

对象的栈上分配?

答案:

如果所有对象都分配在堆中那么会给 GC 带来许多不必要的压力,比如有些对象的生命周期只是在当前线程中,为了减少临时对象在堆内分配的数量,就可以在栈上分配,随着线程的消亡而消亡。当然栈上空间必须充足,否则也无法分配,在判断是否能分配到栈上的另一条件就是要经过逃逸分析。

逃逸分析(Escape Analysis):

  • 简单来讲就是:Java Hotspot 虚拟机判断这个新对象是否只会被当前线程引用,并且决定是否能够在 Java 堆上分配内存。

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

答案:

1、对象头: 对象头又分为 MarkWordClass Pointer 两部分。

  • MarkWord:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位,gc记录信息等等。
  • ClassPointer:用来指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。在 32 位系统占 4 字节,在 64 位系统中占 8 字节。

2、Length: 只在数组对象中存在,用来记录数组的长度,占用 4 字节 3、Instance data: 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的) 4、Padding: Java 对象占用空间是 8 字节对齐的,即所有 Java 对象占用 bytes 数必须是 8 的倍数,是因为当我们从磁盘中取一个数据时,不会说我想取一个字节就是一个字节,都是按照一块儿一块儿来取的,这一块大小是 8 个字节,所以为了完整,padding 的作用就是补充字节,保证对象是 8 字节的整数倍

JVM 调优命令?

答案:

  • 1、jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
  • 2、jstat:jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
  • 3、jmap:jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还可以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候,自动生成dump文件。 jmap不仅能生成dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息,如当前使 用率、当前使用的是哪种收集器等。
  • 4、jhat:jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
  • 5、jstack:jstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈,就可 以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文 件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道 java程序是如何崩溃和在程序何处发生问题。