什么是虚拟机?
Java 虚拟机:是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件( .class
)。
Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。
但是,跨平台的是 Java 程序(包括字节码文件),,而不是 JVM。JVM 是用 C/C++ 开发的,是编译后的机器码,不能跨平台,不同平台下需要安装不同版本的 JVM 。
- 也就是说,JVM 能够跨计算机体系结构来执行 Java 字节码,主要是由于 JVM 屏蔽了与各个计算机平台相关的软件或者硬件之间的差异,使得与平台相关的耦合统一由 JVM 提供者来实现。
JVM 由哪些部分组成?
JVM 的结构基本上由 4 部分组成:
- 类加载器,在 JVM 启动时或者类运行时将需要的 class 加载到 JVM 中。
- 内存区,将内存划分成若干个区以模拟实际机器上的存储、记录和调度功能模块,如实际机器上的各种功能的寄存器或者 PC 指针的记录器等。
- 执行引擎,执行引擎的任务是负责执行 class 文件中包含的字节码指令,相当于实际机器上的 CPU 。
- 本地方法调用,调用 C 或 C++ 实现的本地方法的代码返回结果。
怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?
使用以下语句来确定 JVM 是 32 位还是 64 位 : System.getProperty("sun.arch.data.model")
- 1.32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB ,但实际上会比这个小很多。64 位 JVM 允许指定最大的堆内存,理论上可以达到 2^64。 - 2.64 位 JVM 中,int 的长度是多数?
Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位或者 4 个字节。意思就是说,在 32 位 和 64 位 的Java 虚拟机中,int 类型的长度是相同的。
Java 内存区域与内存溢出异常
1.JVM 运行内存的分类?
- 程序计数器: Java 线程私有,类似于操作系统里的 PC 计数器,它可以看做是当前线程所执行的字节码的行号指示器。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。 - 虚拟机栈(栈内存):Java线程私有,虚拟机栈描述的是 Java 方法执行的内存模型:
每个方法在执行的时候,都会创建一个栈帧用于存储局部变量、操作数、动态链接、方法出口等信息。每个方法调用都意味着一个栈帧在虚拟机栈中入栈到出栈的过程。 - 本地方法栈 :和 Java 虚拟机栈的作用类似,区别是该区域为 JVM 提供使用 Native 方法的服务。
- 堆内存(线程共享):所有线程共享的一块区域,垃圾收集器管理的主要区域。目前主要的垃圾回收算法都是分代收集算法,所以 Java 堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等,默认情况下新生代按照 8:1:1 的比例来分配。
- 方法区(线程共享):各个线程共享的一个区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。运行时常量池也是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。
2.直接内存是不是虚拟机运行时数据区的一部分?
直接内存(Direct Memory),并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中农定义的内存区域。在 JDK1.4 中新加入了 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通脱一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
- 本机直接内存的分配不会受到 Java 堆大小的限制,受到本机总内存大小限制。
- 配置虚拟机参数时,不要忽略直接内存,防止出现 OutOfMemoryError 异常。
3.直接内存(堆外内存)与堆内存比较?
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显。
- 直接内存 IO 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显。
4.后续的版本主要对【方法区】做了什么调整
JDK7 的改变:存储在永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap。但永久代仍存在于 JDK7 中,但是并没完全移除。常量池和静态变量放到 Java 堆里。
JDK8 的改变:废弃 PermGen(永久代),新增 Metaspace(元数据区)。那么方法区还在么?方法区在 Metaspace 中了,方法区都是一个概念的东西。
5.JDK8 之后 Perm Space 有哪些变动? MetaSpace ⼤⼩默认是⽆限的么? 还是你们会通过什么⽅式来指定⼤⼩?
JDK8 后用元空间替代了 Perm Space ;字符串常量存放到堆内存中。
MetaSpace 大小默认没有限制,一般根据系统内存的大小。JVM 会动态改变此值。
可以通过 JVM 参数配置
- -XX:MetaspaceSize : 分配给类元数据空间(以字节计)的初始大小。此值为估计值,MetaspaceSize 的值设置的过大会延长垃圾回收时间。垃圾回收过后,引起下一次垃圾回收的类元数据空间的大小可能会变大。
- -XX:MaxMetaspaceSize :分配给类元数据空间的最大值,超过此值就会触发Full GC 。此值默认没有限制,但应取决于系统内存的大小,JVM 会动态地改变此值。
6.为什么要废弃永久代?
- 现实使用中易出问题。
1.由于永久代内存经常不够用或发生内存泄露,爆出异常 java.lang.OutOfMemoryError
2.字符串存在永久代中,容易出现性能问题和内存溢出。
3.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。 - 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 可能会将HotSpot 与 JRockit 合二为一。
7.Java 内存堆和栈区别?
- 栈内存用来存储基本类型的变量和对象的引用变量;堆内存用来存储Java中的对象,无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中。
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存;堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问。
- 如果栈内存没有可用的空间存储方法调用和局部变量,JVM 会抛出 java.lang.StackOverFlowError 错误;如果是堆内存没有可用的空间存储生成的对象,JVM 会抛出 java.lang.OutOfMemoryError 错误。
- 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满。-Xss 选项设置栈内存的大小,-Xms 选项可以设置堆的开始时的大小。
8.JAVA 对象创建的过程?
Java 中对象的创建就是在堆上分配内存空间的过程,此处说的对象创建仅限于 new 关键字创建的普通 Java 对象,不包括数组对象的创建。
类的整个生命周期分为以上七个阶段,其中验证、准备、解析统称为连接阶段
1)检测类是否被加载
当虚拟机遇到 new 指令时,首先先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就执行类加载过程。
2)为对象分配内存
类加载完成以后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了。只需要在堆上分配所需要的内存即可。
3)为分配的内存空间初始化零值
对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。
4)对对象进行其他设置
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的 hashcode ,GC 分代年龄等信息。
5)执行 init 方法
执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于 Java 程序来说还需要执行 init 方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了 init 方法之后,这个对象才真正能使用。
到此为止一个对象就产生了,这就是 new 关键字创建对象的过程。过程如下:
类加载器是如何加载 Class 文件的?
- 1.加载(Loading),是找到 .class 文件并把这个文件包含的字节码加载到内存中。
- 2.连接(Linking),又可以分为三个步骤:
* 验证:确保被加载的类的正确性
* 准备:Class 类数据结构分析及相应的内存分配,为类的静态变量分配内存,并将其初始化为默认值
* 解析:把类中的符号引用转换为直接引用。 - 3.初始化:声明类变量是指定初始值,使用静态代码块为类变量指定初始值
9.对象的内存布局是怎样的?
对象的内存布局包括三个部分:
- 对象头:对象头包括两部分信息。
1.第一部分,是存储对象自身的运行时数据,如哈希码,GC 分代年龄,锁状态标志,线程持有的锁等等。
2.第二部分,是类型指针,即对象指向类元数据的指针。 - 实例数据:就是数据。
- 对齐填充:不是必然的存在,就是为了对齐。
10.对象是如何定位访问的?
对象的访问定位有两种:
- 句柄定位:Java 堆会画出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 直接指针访问:Java 堆对象的不居中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址。
对比两种方式?
这两种对象访问方式各有优势。
- 使用句柄来访问的最大好处,就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
- 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
我们目前主要虚拟机 Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。
11.有哪些 OutOfMemoryError 异常?
在 Java 虚拟机中规范的描述中,除了程序计数器外,虚拟机内存的其它几个运行时区域都有发生的 OutOfMemoryError(简称为“OOM”) 异常的可能。
- Java 堆溢出
- 虚拟机栈和本地方法栈溢出
- 方法区和运行时常量池溢出,从 JDK8 开始,就变成元数据区的内存溢出。
- 本机直接内存溢出
12.当出现了内存溢出,你怎么排错?
1、首先,控制台查看错误日志。
2、然后,使用 JDK 自带的 jvisualvm 工具查看系统的堆栈日志。
3、定位出内存溢出的空间:堆,栈还是永久代(JDK8 以后不会出现永久代的内存溢出)。
如果是堆内存溢出,看是否创建了超大的对象。
如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环。
13.Java 中会存在内存泄漏吗?
理论上 Java 因为有垃圾回收机制(GC)不会存在内存泄露问题。然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被 GC 回收也会发生内存泄露。例如说:
Hibernate 的 Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象。
使用 Netty 的堆外的 ByteBuf 对象,在使用完后,并未归还,导致使用的一点一点在泄露。
垃圾收集器与内存分配策略
1.什么是垃圾回收机制?
- Java 中对象是采用
new
或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由 Java 虚拟机通过垃圾回收机制完成的。GC 为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控。 - Java 程序员不用担心内存管理,因为垃圾收集器会自动进行管理。
- 可以调用下面的方法之一:
System#gc()
或Runtime#getRuntime()#gc()
,但 JVM 也可以屏蔽掉显示的垃圾回收调用。
2.为什么不建议在程序中显式的声明 System.gc() ?
因为显式声明是做堆内存全扫描,也就是 Full GC ,是需要停止所有的活动的(Stop The World Collection),对应用很大可能存在影响。
另外,调用 System.gc() 方法后,不会立即执行 Full GC ,而是虚拟机自己决定的。
3.如果一个对象的引用被设置为 null , GC 会立即释放该对象的内存么?
不会, 这个对象将会在下一次 GC 循环中被回收。
4.finalize()方法什么时候被调用?它的目的是什么?
finallize()方法是在释放该对象内存前由 GC (垃圾回收器)调用。
- 通常建议在这个方法中释放该对象持有的资源,例如持有的堆外内存、和远程服务的长连接。
- 一般情况下,不建议重写该方法。
- 对于一个对象,该方法有且仅会被调用一次。
若对象在进行可达性分析后发现没有与 GC roots 相连接的引用链,那么他将会被第一次标记并进行一次筛选,筛选的条件是该对象是否有必要执行 finalize()方法,当对象没有重写finalize()方法或者 finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没必要执行。若该对象被判定为有必要执行 finalize 方法,则这个对象会被放在一个 F-Queue 队列,finalize 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-queue 中的对象进行第二次小规模的标记,若对象要在 finalize 中成功拯救自己—只要重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时他们将会被移出“即将回收”集合。
5.如何判断一个对象是否已经死去?
有两种方式:引用计数和可达性分析
1)引用计数
每个对象有一个引用计数属性,新增一个引用时计数加 1 ,引用释放时计数减 1 ,计数为 0 时可以回收。此方法简单,无法解决对象相互循环引用的问题。
2)可达性分析(Reachability Analysis)
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。目前在用的有 Java、C# 等语言。
6.如果 A 和 B 对象循环引用,是否可以被 GC?
可以,因为 Java 采用可达性分析的判断方式。
7.在 Java 语言里,可作为 GC Roots 的对象包括以下几种?
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中的类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中 JNI(即一般说的 Native 方法)中引用的对象。
8.方法区是否能被回收?
方法区可以被回收,但是价值很低,主要回收废弃的常量和无用的类。
9.如何判断无用的类
需要完全满足如下三个条件:
- 该类所有实例都被回收(Java 堆中没有该类的对象)。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方利用反射访问该类。
10.Java 对象有哪些引用类型?
Java 一共有四种引用类型:
- 强引用
- 软引用(SoftReference)
- 弱引用(WeakReference)
-
虚引用(PhantomReference)
1)强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2)软引用(SoftReference)
如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
3)弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4)虚引用(PhantomReference)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
**WeakReference 与 SoftReference的区别?
虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率。
- 但是 WeakReference 一旦失去最后一个强引用,就会被 GC 回收
- 而 SoftReference 虽然不能阻止被回收,但是可以延迟到 JVM 内存不足的时候。
为什么要有不同的引用类型?
不像 C 语言,我们可以控制内存的申请和释放,在 Java 中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对 GC 回收时机不可控的妥协。有以下几个使用场景可以充分的说明:
- 利用软引用和弱引用解决 OOM 问题。用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题.
- 通过软引用实现 Java 对象的高速缓存。比如我们创建了一 Person 的类,如果每次需要查询一个人的信息,哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这将引起大量 Person 对象的消耗,并且由于这些对象的生命周期相对较短,会引起多次 GC 影响性能。此时,通过软引用和 HashMap 的结合可以构建高速缓存,提供性能。
JVM 垃圾回收算法?
-
1).标记-清除算法
是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
-
缺点:
- 1、效率问题,标记和清除两个过程的效率都不高。
- 2、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
-
2).标记-整理算法
标记整理算法,类似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
-
优点:
- 1、相对标记清除算法,解决了内存碎片问题。
- 2、没有内存碎片后,对象创建内存分配也更快速了(可以使用TLAB进行分配)。
-
缺点:
- 1、效率问题,(同标记清除算法)标记和整理两个过程的效率都不高。
-
3.复制算法
复制算法,可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。
优点:
1、效率高,没有内存碎片。
缺点:
1、浪费一半的内存空间。
2、复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。 4).分代收集算法
当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把 Java 堆分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法。
而老年代中,因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。
- 图的左半部分是未回收前的内存区域,右半部分是回收后的内存区域。
- 对象分配策略:
- 对象优先在 Eden 区域分配,如果对象过大直接分配到 Old 区域。
- 长时间存活的对象进入到 Old 区域。
- 改进自复制算法
- 现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 公司的专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照
1:1
的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor 。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。 - HotSpot 虚拟机默认 Eden 和 2 块 Survivor 的大小比例是
8:1:1
,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10% 的内存会被“浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
- 现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 公司的专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照
什么是安全点?
SafePoint 安全点,顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定,比如记录OopMap 的状态,从而确定 GC Root 的信息,使 JVM 可以安全的进行一些操作,比如开始 GC 。
SafePoint 指的特定位置主要有:
1.循环的末尾 (防止大循环的时候一直不进入 Safepoint ,其他线程在等待它进入 Safepoint )。
2.方法返回前。
3.调用方法的 Call 之后。
4.抛出异常的位置。
安全区域
如果程序长时间不执行,比如线程调用的 sleep 方法,这时候程序无法响应 JVM 中断请求这时候线程无法到达安全点,显然 JVM 也不可能等待程序唤醒,这时候就需要安全区域了。
安全区域是指一段代码片中,引用关系不会发生变化,在这个区域任何地方 GC 都是安全的,安全区域可以看做是安全点的一个扩展。
- 线程执行到安全区域的代码时,首先标识自己进入了安全区域,这样 GC 时就不用管进入安全区域的线程了.
- 线程要离开安全区域时就检查 JVM 是否完成了 GC Roots 枚举(或者整个 GC 过程),如果完成就继续执行,如果没有完成就等待直到收到可以安全离开的信号。
JVM 垃圾收集器有哪些?
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
CMS收集器
CMS(Conrrurent Mark Sweep)收集器是以获取最短回收停顿时间为目标的收集器。使用标记 - 清除算法,收集过程分为如下四步:
- 初始标记,标记GCRoots能直接关联到的对象,时间很短。
- 并发标记,进行GCRoots Tracing(可达性分析)过程,时间很长。
- 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长。全局停顿
-
并发清除,回收内存空间,时间很长。
其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。运行过程如下图所示
-
优点:并发收集,低停顿
缺点:
- 对CPU资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低
- 无法处理浮动垃圾,因为在并发清理阶段用户线程还在运行,自然就会产生新的垃圾,而在此次收集中无法收集他们,只能留到下次收集,这部分垃圾为浮动垃圾,同时,由于用户线程并发执行,所以需要预留一部分老年代空间提供并发收集时程序运行使用。
- 由于采用的标记 - 清除算法,会产生大量的内存碎片,不利于大对象的分配,可能会提前触发一次Full GC。虚拟机提供了-XX:+UseCMSCompactAtFullCollection参数来进行碎片的合并整理过程,这样会使得停顿时间变长,虚拟机还提供了一个参数配置,-XX:+CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC后,接着来一次带压缩的GC。
G1收集器
G1收集器的运作可以分为如下几步
- 初始标记,标记GCRoots能直接关联到的对象;修改TAMS(Next Top At Mark Start),使得下一阶段程序并发时,能够在可用的Region中创建新对象,需停顿线程,耗时很短。
- 并发标记,从GCRoots开始进行可达性分析,与用户程序并发执行,耗时很长。
- 最终标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,变动的记录将被记录在Remembered Set Logs中,此阶段会把其整合到Remembered Set中,需要停顿线程,与用户程序并行执行,耗时较短。
- 筛选回收,对各个Region的回收价值和成本进行排序,根据用户期望的GC时间进行回收,与用户程序并发执行,时间用户可控。
可以在新生代和老年代中只使用G1收集器。具有如下特点。
- 并行和并发。使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行。
- 分代收集。独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果。
- 空间整合。基于标记 - 整理算法,无内存碎片产生。
- 可预测的停顿。这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
使用G1收集器时,Java堆会被划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但两者已经不是物理隔离了,都是一部分Region(不需要连续)的集合。G1收集器中,Region之间的对象引用以及其他收集器的新生代和老年代之间的对象引用,虚拟机都使用Remembered Set来避免全堆扫描的。每个Region对应一个Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查老年代的对象是否引用了新生代的对象),如果是,则通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
1.G1 和 CMS 的区别?
- CMS :并发标记清除。他的主要步骤有:初始收集,并发标记,重新标记,并发清除(删除)、重置。
- G1:主要步骤:初始标记,并发标记,重新标记,复制清除(整理)
- CMS 的缺点是对 CPU 的要求比较高。G1是将内存化成了多块,所有对内段的大小有很大的要求。
- CMS是清除,所以会存在很多的内存碎片。G1是整理,所以碎片空间较小。
- G1 和 CMS 都是响应优先,他们的目的都是尽量控制 STW 时间。
- G1 和 CMS 的 Full GC 都是单线程 mark sweep compact 算法,直到 JDK10 才优化为并行的。
对象分配规则是什么?
对象优先分配在 Eden 区。
1.如果 Eden 区无法分配,那么尝试把活着的对象放到 Survivor0 中去(Minor GC)
2.如果 Survivor0 可以放入,那么放入之后清除 Eden 区。
3.如果 Survivor0 不可以放入,那么尝试把 Eden 和 Survivor0 的存活对象放到 Survivor1 中。
4.如果 Survivor1 可以放入,那么放入 Survivor1 之后清除 Eden 和 Survivor0 ,之后再把 Survivor1 中的对象复制到 Survivor0 中,保持 Survivor1 一直为空。
5.如果 Survivor1 不可以放入,那么直接把它们放入到老年代中,并清除 Eden 和 Survivor0 ,这个过程也称为分配担保。
ps:清除 Eden、Survivor 区,就是 Minor GC 。
总结来说,分配的顺序是:新生代(Eden => Survivor0 => Survivor1)=> 老年代大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。
这样做的目的是,避免在 Eden 区和两个 Survivor 区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。长期存活的对象进入老年代。
虚拟机为每个对象定义了一个年龄计数器,如果对象经过了 1 次 Minor GC 那么对象会进入 Survivor 区,之后每经过一次 Minor GC 那么对象的年龄加 1 ,知道达到阀值对象进入老年区。动态判断对象的年龄。
为了更好的适用不同程序的内存情况,虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代。
如果 Survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
- 空间分配担保。
每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次 Full GC ,如果小于检查 HandlePromotionFailure 设置,如果 true 则只进行 Monitor GC ,如果 false 则进行 Full GC 。
如下是一张对象创建时,分配内存的图:
Survivor的存在意义?
就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
为什么新生代内存需要有两个 Survivor 区?
设置两个Survivor区最大的好处就是解决了碎片化
详细的原因,可以看 《为什么新生代内存需要有两个 Survivor 区》 文章。
什么是新生代 GC 和老年代 GC?
- 新生代
- 一个 Eden 区
- 两个 Survivor 区
- 老年代
默认新生代(Young)与老年代(Old)的比例的值为1:2
(该值可以通过参数–XX:NewRatio
来指定)。默认的Eden:from:to=8:1:1
(可以通过参数–XX:SurvivorRatio
来设定)。
新生代GC(MinorGC/YoungGC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 MinorGC 非常频繁,一般回收速度也比较快。
老年代GC(MajorGC/FullGC):指发生在老年代的 GC,出现了 MajorGC,经常会伴随至少一次的 MinorGC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行 MajorGC 的策略选择过程)。MajorGC 的速度一般会比 MinorGC 慢 10 倍以上。
什么情况下会出现 Young GC?
对象优先在新生代 Eden 区中分配,如果 Eden 区没有足够的空间时,就会触发一次 Young GC 。
什么情况下回出现 Full GC?
Full GC 的触发条件有多个,FULL GC 的时候会 STOP THE WORD 。
- 1、在执行 Young GC 之前,JVM 会进行空间分配担保——如果老年代的连续空间小于新生代对象的总大小(或历次晋升的平均大小),则触发一次 Full GC 。
- 2、大对象直接进入老年代,从年轻代晋升上来的老对象,尝试在老年代分配内存时,但是老年代内存空间不够。
- 3、显式调用
System#gc()
方法时。
怎么获取 Java 程序使用的内存?
可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。
Runtime#freeMemory() 方法,返回剩余空间的字节数。
Runtime#totalMemory() 方法,总内存的字节数。
Runtime#maxMemory() 方法,返回最大内存的字节数。
常见 GC 的优化配置?
配置 | 描述 |
---|---|
-Xms | 初始化堆内存大小 |
-Xmx | 堆内存最大值 |
-Xmn | 新生代大小 |
-XX:PermSize | 初始化永久代大小 |
-XX:MaxPermSize | 永久代最大容量 |
-XX:SurvivorRatio | 设置年轻代中 Eden 区与 Survivor 区的比值 |
-XX:Xmn | 设置年轻代大小 |
如何排查线程 Full GC 频繁的问题?
JVM 的永久代中会发生垃圾回收么?
- Young GC 不会发生在永久代。
- 如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果我们仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。
类加载发生的时机是什么时候?
- 1、遇到
new
、getstatic
、putstatic
、invokestatic
这四条字节码指令时,如果类还没进行初始化,则需要先触发其初始化。 - 2、使用
java.lang.reflect
包的方法对类进行反射调用的时候,如果类还没进行初始化,则需要先触发其初始化。 - 3、当初始化了一个类的时候,如果发现其父类还没进行初始化,则需要先触发其父类的初始化。
- 4、当虚拟机启动时,用户需要指定一个执行的主类,即调用其
#main(String[] args)
方法,虚拟机则会先初始化该主类。 - 5、当使用 JDK7 的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类加载器
类加载器 ClassLoader 是具有层次结构的.
1.启动类加载器(Bootstrap ClassLoader):这个类加载器复杂将存放在 JAVA_HOME/lib 目录中的,或者被-Xbootclasspath 参数所指定的路径种的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不会重载)。
2.扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责夹杂JAVA_HOME/lib/ext 目录下的,或者被java.ext.dirs 系统变量所指定的路径种的所有类库。开发者可以直接使用扩展类加载器。
-
3.应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader 实现。由于这个类加载器是ClassLoader 种的getSystemClassLoader方法的返回值,所以也成为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
该模型要求除了顶层的 Bootstrap 启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承的关系来实现,而是通过组合关系来复用父加载器的代码。
每个类加载器都有自己的命名空间(由该加载器及所有父类加载器所加载的类组成。
1.在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
2.在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类)。
3.类加载器负责加载所有的类,同一个类(一个类用其全限定类名(包名加类名)标志)只会被加载一次。
Java 虚拟机是如何判定两个 Java 类是相同的?
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
双亲委派模型的工作过程?
- 1、当前 ClassLoader 首先从自己已经加载的类中,查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。(每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了)。
- 2、当前 ClassLoader 的缓存中没有找到被加载的类的时候
- 3、委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到 bootstrap ClassLoader。
- 4、当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,从缓存中获得 name 对应的类
Class<?> c = findLoadedClass(name);
if (c == null) { // 获得不到
try {
// 其次,如果父类非空,使用它去加载类
if (parent != null) {
c = parent.loadClass(name, false);
// 其次,如果父类为空,使用 Bootstrap 去加载类
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) { // 还是加载不到
// 最差,使用自己去加载类
c = findClass(name);
}
}
// 如果要解析类,则进行解析
if (resolve) {
resolveClass(c);
}
return c;
}
}
为什么优先使用父 ClassLoader 加载类?
1、共享功能:可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加载,一些 Framework 层级的类一旦被顶层的 ClassLoader 加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
2、隔离功能:主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,比如 String ,同时也避免了重复加载,因为 JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就是不同的两个类,如果相互转型的话会抛 java.lang.ClassCaseException 。
ps:即使我们自己定义了一个 java.util.String 类,也不会被重复加载。