Java笔记-----(4)JVM内存机制

Java笔记-----(4)JVM内存机制

(1)JVM中的内存划分(重点掌握)

JVM中的内存主要划分为5个区域,即方法区堆内存程序计数器虚拟机栈以及本地方法栈。下边是Java虚拟机运行时数据区示意图:
在这里插入图片描述

JDK1.8之前:

在这里插入图片描述

JDK1.8:

在这里插入图片描述

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 方法区
  • 直接内存 (非运行时数据区的一部分)

(1.1) 方法区

方法区是一个线程之间共享的区域。常量,静态变量以及JIT编译后的代码都在方法区。主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过-XX:MaxPermSize控制上限。
(JIT编译器可以对是否需要编译做判断)

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

① 方法区和永久代的关系

《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。

② 常用参数

JDK 1.8 之前:
永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小

-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会
					// 抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

JDK 1.8 :
的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存,其常用参数有:

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

③ 为什么要使用元空间代替永久代?

JDK8中在内存管理上的变化:
JDK8中出现了元空间代替了永久代。元空间和永久代类似,都是对JVM规范中方法区的实现。区别在于元空间并不在虚拟机中,而是使用本地内存,默认情况下元空间的大小仅受本地内存限制,也可以通过-XX:MetaspaceSize指定元空间大小。

字符串在永久代中,容易出现性能问题和内存溢出的问题。类和方法的信息等比较难确定大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。使用元空间则使用了本地内存。

现实使用中,由于永久代内存经常不够用或发生内存泄露,报出异常java.lang.OutOfMemoryError: PermGen
基于此,将永久区废弃,而改用元空间,改为了使用本地内存空间。


  1. 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
    当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace,可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

  2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

  3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。

(1.2) 堆内存

堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例,通过-Xmx-Xms 可以控制大小。

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存

堆这里最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有几种,比如:

  • OutOfMemoryError: GC Overhead Limit Exceeded :
    当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  • java.lang.OutOfMemoryError: Java heap space :
    假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发java.lang.OutOfMemoryError: Java heap space错误。(和本机物理内存无关,和你配置的内存大小有关!)

① JDK1.7的堆内存模型

在这里插入图片描述

  • Young 年轻区(代)
    Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用,在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
  • Tenured 年老区
    Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
  • Perm 永久区
    Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性 加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到 java.lang.OutOfMemoryError :PermGen space 的错误,造成这个错误的很大原因 就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。
  • Virtual 区:
    最大内存和初始内存的差值,就是 Virtual区。

② JDK1.8的堆内存模型

在这里插入图片描述
由上图可以看出, jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。
年轻代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中变化最大的Perm区,用Metaspace(元数据空间)进行了替换。
需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。
在这里插入图片描述

(1.3) 虚拟机栈(栈内存)

栈内存中主要保存局部变量、基本数据类型变量、堆内存中某个对象的引用变量。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表操作数栈,动态链接,方法出口等信息。栈中的(栈帧)随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。

局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈。与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

Java 虚拟机栈会出现两种错误:StackOverFlowErrorOutOfMemoryError

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java
    虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误。

扩展:那么方法/函数如何调用?

Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。

Java 方法有两种返回方式

  • return 语句。
  • 抛出异常。

不管哪种返回方式都会导致栈帧被弹出。

(1.4) 程序计数器

程序计数器是当前线程执行的字节码的位置指示器字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域


程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存

从上面的介绍中我们知道程序计数器主要有两个作用:

  1. 字节码解释器通过改变程序计数器依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡

(1.5) 本地方法栈

主要是为JVM提供使用native 方法的服务。

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

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息

方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowErrorOutOfMemoryError 两种错误。

02-JVM内存模型:虚拟机栈与本地方法栈

(1.6) 运行时常量池

常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 错误。

JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
在这里插入图片描述

(1.7) 直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库(直接分配堆外内存),然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

(2)HotSpot 虚拟机对象探秘

(2.1)Java对象的创建过程

在这里插入图片描述

① 类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池定位某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载解析和初始化。如果没有,那么会执行类加载过程。

② 分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。

内存分配的两种方式

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

对象的内存分配有两种方式,即指针碰撞和空闲列表方式。

  • 指针碰撞方式
    使用场合:Java堆中的内存是绝对规整的,没有内存碎片
    原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着(没用过的内存方向)将该指针移动(对象内存大小位置)即可
    GC收集器:Serial、ParNew
  • 空闲列表方式
    使用场合:Java堆内存中不是规整的,已使用和未使用的内存相互交错,
    原理:虚拟机会维护一个列表用来记录哪块内存是可用的,在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。
    GC收集器:CMS

Java 堆内存是否规整是由所使用的垃圾收集器是否拥有压缩整理功能来决定的

内存分配并发问题

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机对分配内存空间的动作进行同步处理,采用 “CAS + 失败重试”的方式保证更新操作的原子性
  • 本地线程分配缓存(TLAB): 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
    把分配内存的动作按照线程划分在不同的空间之中,即给每一个线程都预先分配一小段的内存只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。虚拟机是否使用TLAB,可以通过-XX: +/-UserTLAB参数来设定

③ 初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

④ 设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

⑤ 执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

(2.2)对象的内存布局

在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头实例数据对齐填充

Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例

实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

(2.3)对象的访问定位

当创建一个对象的时候,在栈内存中会有一个引用变量,指向堆内存中的某个具体的对象实例。通过栈上的 reference 数据来操作堆上的具体对象。

Java虚拟机规范中并没有规定这个引用变量应该以何种方式去定位和访问堆内存中的具体对象。目前常见的对象访问方式有两种,即句柄访问方式和直接指针访问方式,分别介绍如下:

  • 句柄访问方式:
    JVM的堆内存中划分出一块内存来作为句柄池,引用变量(reference)中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。在内存垃圾收集之后,对象会移动,但是引用reference中存储的是(稳定的句柄地址),但是句柄地址方式不直接,访问速度较慢

在这里插入图片描述

  • 直接指针访问方式:
    引用变量中存储的就是对象的直接地址,通过指针直接访问对象。直接指针的访问方式节省了一次指针定位的时间开销,速度较快。Sun HotSpot使用了直接指针方式进行对象的访问。
    如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

在这里插入图片描述

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销

(3)JVM内存分配与垃圾回收(重点掌握)

JVM的内存可以分为堆内存和非堆内存。堆内存分为年轻代和老年代。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。如下图所示:
在这里插入图片描述

(3.1)JVM堆内存的分配与回收

  • JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64。JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。
  • 通过参数-Xmn2G可以设置年轻代大小为2G。通过-XX:SurvivorRatio可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域。

① 对象优先在eden区分配

目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

② 大对象直接进入老年代

如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个-XX:PretenureSizeThreshold参数,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

③ 长期存活的对象将进入老年代

每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过-XX:MaxTenuringThreshold设置晋升年龄。

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

(3.2)JVM非堆内存的分配与回收

JVM使用-XX:PermSize 设置非堆内存初始值,默认是物理内存的1/64。由-XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4

(3.3)垃圾回收什么时候开始?Minor Gc和Full GC有什么不同?

垃圾回收主要是完成清理对象,整理内存的工作。上面说到GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC老年代区域发生的Full GC,分别介绍如下。

  • Minor GC(年轻代GC):
    对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快
    指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
  • Full GC/Major GC(老年代GC):
    Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC
    指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上

(3.4)内存分配与回收的两个重要概念

① 动态对象年龄判定

如果Survivor空间中(相同年龄所有对象的大小总和)大于(Survivor空间的一半),那么年龄大于等于(该对象年龄)的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold(设置晋升年龄的参数)。

Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值

默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6。

② 空间分配担保

发生Minor GC(年轻代GC)时,虚拟机会检测(之前每次晋升到老年代的平均大小)是否大于(老年代的剩余空间大小)。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC。

空间分配担保详细说明:

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间

  • 如果大于,则此次Minor GC是安全的
  • 如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。
    如果HandlePromotionFailure=true,那么会继续检查(老年代最大可用连续空间)是否大于(历次晋升到老年代的对象的平均大小),如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

上面提到了Minor GC依然会有风险,是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。

老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。

取平均值仍然是一种概率性的事件,如果某次Minor GC后存活对象陡增,远高于平均值的话,必然导致担保失败,如果出现了分配担保失败,就只能在失败后重新发起一次Full GC。虽然存在发生这种情况的概率,但大部分时候都是能够成功分配担保的,这样就避免了过于频繁执行Full GC。

(4)JVM如何判定一个对象是否应该被回收 / 对象已经死亡?(重点掌握)

判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法。

(4.1)引用计数法

是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的

优点:

  • 实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
  • 在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报 outofmember 错误。
  • 区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。

缺点:

  • 每次对象被引用时,都需要去更新计数器,有一点时间开销。
  • 浪费 CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
  • 无法解决循环引用问题。(最大的缺点)

循环引用:

class TestA{
  public TestB b;
}

class TestB{
  public TestA a;
}

public class Main{
    public static void main(String[] args){
        A a = new A();
        B b = new B();
        a.b=b;
        b.a=a;
        a = null;
        b = null;
    }
}

虽然a和b都为null,但是由于a和b存在循环引用,这样a和b永远都不会被回收。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

    }
}

(4.2)root根搜索方法 / 可达性分析算法

root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。以下对象会被认为是root对象:

  • 栈内存中引用的对象
  • 方法区中静态引用和常量引用指向的对象
  • 被启动类(bootstrap加载器)加载的类和创建的对象
  • Native方法中JNI引用的对象。

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

(4.3)对象的引用

无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表一个引用。JDK1.2以后将引用分为强引用,软引用,弱引用和虚引用四种。

① 强引用(StrongReference)

普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。

当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题

② 软引用(SoftReference)

通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉

如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。

③ 弱引用(WeakReference)

通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

ThreadLocal 内存泄露问题:
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。

④ 虚引用(PhantomReference)

也称为幽灵引用或者幻影引用,通过PhantomReference类实现。

"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收

设置虚引用只是为了对象被回收时候收到一个系统通知虚引用主要用来跟踪对象被垃圾回收的活动。

虚引用与软引用和弱引用的一个区别在于: 虚引用必须引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以(在所引用的对象的内存被回收之前)采取必要的行动

特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

(4.4)如何判断一个常量是废弃常量?

运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?

假如在常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池。

注意:JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

(4.5)如何判断一个类是无用的类?

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :

  • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

(5)JVM垃圾回收算法有哪些?(重点掌握)

HotSpot 虚拟机采用了root根搜索方法来进行内存回收,常见的回收算法有标记-清除算法,复制算法和标记整理算法

(5.1)标记-清除算法(Mark-Sweep)

标记-清除算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,并且会产生内存碎片。
在这里插入图片描述

标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
标记:从根节点开始标记引用的对象。
清除:未被标记引用的对象就是垃圾对象,可以被清理。

原理:
在这里插入图片描述

这张图代表的是程序运行期间所有对象的状态,它们的标志位全部是 0(也就是未标记,以下默认0就是未标记,1为已标记),假设这会儿有效内存空间耗尽了,JVM将会停止应用程序的运行并开启GC线程,然后开始进行标记工作,按照根搜索算法,标记完以后,对象的状态如下图。
在这里插入图片描述
可以看到,按照根搜索算法,所有从 root对象可达的对象就被标记为了存活的对象,此时已经完成了第一阶段标记。接下来,就要执行第二阶段清除了,那么清除完以后,剩下的对象以及对象的状态如下图所示。
在这里插入图片描述
可以看到,没有被标记的对象将会回收清除掉,而被标记的对象将会留下,并且会将标记位重新归0。接下来就不用说了,唤醒停止的程序线程,让程序继续运行即可。

优点:
可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。
缺点:

  • 效率问题
    效率较低,标记和清除两个动作都需要遍历所有的对象,并且在 GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
  • 空间问题
    通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于 内存的各个角落,所以清理出来的内存是不连贯的。

(5.2)复制算法

复制算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。复制算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

在这里插入图片描述

复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。

如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。

  1. 在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。
  2. 紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。
  3. 经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。
  4. GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

优点:

  • 在垃圾对象多的情况下,效率较高
  • 清理后,内存无碎片

缺点:

  • 在垃圾对象少的情况下,不适用,如:老年代内存
  • 分配的 2块内存空间,在同一个时刻,只能使用一半,内存使用率较低

(5.3)标记-整理算法 / 标记-压缩算法

标记-整理算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题

在这里插入图片描述

标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的清理未标记的对象,而是将存活的对象压缩到内存的一端,然后清理边界以外的垃圾,从而解决了碎片化的问题。

优点:

  • 解决了引用计数算法中的循环引用的问题
  • 解决了标记清除算法的碎片化的问题

缺点:
标记清除算法效率较低,标记和清除两个动作都需要遍历所有的对象,并且在 GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。标记压缩算法还多了一步,对象移动内存位置的步骤,其效率也有有一定的影响。

(5.4)分代算法

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

垃圾回收算法是垃圾收集器的算法实现基础。分代算法其实就是这样的,根据回收对象的特点进行选择。

  • 新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
  • 老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

(6)JVM中的垃圾收集器(重点掌握)

JVM中的垃圾收集器主要包括7种,即Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old以及CMS,G1收集器。如下图所示:
在这里插入图片描述

(6.1) Serial 收集器(单线程 新生代)

Serial收集器是一个单线程的垃圾收集器,并且在执行垃圾回收的时候需要 Stop The World

垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停,等待垃圾回收的完成。这种现象称之为STW(Stop-The-World)

在这里插入图片描述

虚拟机运行在Client模式下的默认新生代收集器。Serial收集器的优点是简单高效,对于限定在单个CPU环境来说,Serial收集器没有多线程交互的开销

对于交互性较强的应用而言,这种垃圾收集器是不能够接受的。
一般在Javaweb应用中是不会采用该收集器的。

-XX:+UseSerialGC:指定年轻代老年代都使用串行垃圾收集器。

(6.2) Serial Old 收集器(单线程 老年代)

Serial Old是Serial收集器的老年代版本,也是一个单线程收集器。主要也是给在Client模式下的虚拟机使用。在Server模式下存在主要是做为CMS垃圾收集器的后备预案,当CMS并发收集发生Concurrent Mode Failure时使用

主要有两大用途:

  • 在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用
  • 作为 CMS 收集器的后备方案

(6.3) ParNew 收集器(多线程 新生代)

ParNew是Serial收集器的多线程版本,新生代并行的(多线程的)老年代串行的(单线程的),新生代采用复制算法,老年代采用标记整理算法。可以使用参数:-XX:UseParNewGC使用该收集器,使用 -XX:ParallelGCThreads可以限制线程数量

它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作

并行垃圾收集器在收集的过程中也会暂停应用程序STW,这个和串行垃圾回收器是一样的,只是并行执行,速度更快些,暂停的时间更短一些

并行和并发概念补充:

  • 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。

(6.4) Parallel Scavenge 垃圾收集器(多线程 新生代 吞吐量)

Parallel Scavenge是一种新生代收集器,使用复制算法的收集器,而且是并行的多线程收集器

Paralle收集器特点是更加关注吞吐量

(吞吐量就是cpu用于运行用户代码的时间与cpu总消耗时间的比值)
CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)

  • +XX:+UseParallelGC 年轻代使用 ParallelGC垃圾回收器,老年代使用串行回收器。
  • -XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间
  • -XX:GCTimeRatio参数直接设置吞吐量大小
  • -XX:+UseAdaptiveSizePolicy参数可以打开GC自适应调节策略,该参数打开之后虚拟机会根据系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。

在这里插入图片描述

自适应调节策略是Parallel Scavenge收集器和ParNew的主要区别之一。

(6.5) Parallel Old 收集器 (多线程 老年代 吞吐量)

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

在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

-XX:+UseParallelOldGC
年轻代使用 ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。

(6.6) CMS(Concurrent Mark Sweep)收集器(多线程 老年代)

Concurrent Mark Sweep 并发标记清除,获取最短回收停顿时间

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于(标记-清除算法)实现的,并发的,是一种老年代收集器,通常与ParNew年轻代一起使用。

CMS也确实是我们服务中最常使用的垃圾收集器。利用CMS并发标记和清理的特性,可以有效降低用户的停顿时间,对于服务的稳定性有一个非常显著的提升

‐XX:+UseConcMarkSweepGC

CMS的垃圾收集过程分为4步:

  • 初始标记:需要“Stop the World”,初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快。
  • 并发标记:是主要标记过程,这个标记过程是和用户线程并发执行的。
  • 重新标记:需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)。
  • 并发清除:和用户线程并发执行的,基于标记结果来清理对象。

在这里插入图片描述

那么问题来了,如果在重新标记之前刚好发生了一次MinorGC年轻代GC,会不会导致重新标记阶段Stop the World时间太长?
答:不会的,在并发标记阶段其实还包括了一次并发的预清理阶段,虚拟机会主动等待年轻代发生垃圾回收,这样可以将重新标记对象引用关系的步骤放在并发标记阶段,有效降低重新标记阶段Stop The World的时间

在这里插入图片描述

CMS垃圾回收器的优点:
CMS以降低垃圾回收的停顿时间为目的,很显然其具有并发收集,停顿时间低的优点。
缺点:

  • 对CPU资源非常敏感,因为并发标记和并发清理阶段和用户线程一起运行,当CPU数变小时,性能容易出现问题。
  • 收集过程中会产生浮动垃圾,所以不可以在老年代内存不够用了才进行垃圾回收,必须提前进行垃圾收集。通过参数-XX:CMSInitiatingOccupancyFraction的值来控制内存使用百分比。如果该值设置的太高,那么在CMS运行期间预留的内存可能无法满足程序所需,会出现Concurrent Mode Failure失败,之后会临时使用Serial Old收集器做为老年代收集器,会产生更长时间的停顿。
  • 标记-清除方式会产生内存碎片,可以使用参数-XX:UseCMSCompactAtFullCollection来控制是否开启内存整理(无法并发,默认是开启的)。参数-XX:CMSFullGCsBeforeCompaction用于设置执行多少次不压缩的Full GC后进行一次带压缩的内存碎片整理(默认值是0)

浮动垃圾:
由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾

(6.7) G1(Garbage-First)收集器

G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

G1 的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

  1. 第一步,开启G1垃圾收集器
  2. 第二步,设置堆的最大内存
  3. 第三步,设置最大的停顿时间

G1中提供了三种模式垃圾回收模式,Young GCMixed GCFull GC,在不同的条件下被触发。

G1收集器将新生代和老年代取消了,取而代之的是将堆划分为若干的区域,仍然属于分代收集器,区域的一部分包含新生代。这样做的好处就是,我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够

其中,新生代采用复制算法,老年代采用标记-整理算法

通过将JVM堆分为一个个的区域(region),G1收集器可以避免在Java堆中进行全区域的垃圾收集。G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表每次根据回收时间来优先回收价值最大的region

G1收集器的特点:

  • 并行与并发:G1能充分利用多CPU,多核环境下的硬件优势,来缩短Stop the World,是并发的收集器。
  • 分代收集:G1不需要其他收集器就能独立管理整个GC堆,能够采用不同的方式去处理新建对象、存活一段时间的对象和熬过多次GC的对象。
  • 空间整合:G1从整体来看是基于标记-整理算法,从局部(两个Region)上看基于复制算法实现,G1运作期间不会产生内存空间碎片
  • 可预测的停顿:能够建立可以预测的停顿时间模型,预测停顿时间。

和CMS收集器类似,G1收集器的垃圾回收工作也分为了四个阶段:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

其中,筛选回收阶段首先对各个Region的回收价值和成本进行计算,根据用户期望的GC停顿时间来制定回收计划。

在这里插入图片描述


Young GC
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


Mixed GC
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


G1收集器相关参数
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(6.8) 常用的启动参数

JAVA_OPTS="
-Xms4096m //等价于-XX:InitialHeapSize, 设置JVM初始堆内存大小
-Xmx4096m //等价于-XX:MaxHeapSize, 设置JVM最大堆内存大小
-XX:NewRatio=2 //新生代(eden+2*s)和老年代(不包含永久区)的比值
			   // 2 表示新生代:老年代=1:2,即年轻代占堆的1/3
-XX:SurvivorRatio=8 //设置两个survivor区和eden区的比
					// 8 表示两个survivor:eden=2:8,即一个survivor占年轻代的1/10
-Xloggc:/home/work/log/serviceName/gc.log //日志文件的输出路径
-XX:+PrintGCDetails //输出GC的详细日志
-XX:+PrintGCTimeStamps //输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCApplicationStoppedTime //打印垃圾回收期间程序暂停的时间
-XX:+UseConcMarkSweepGC //老年代使用CMS垃圾收集器
-XX:+UseParNewGC //年轻代使用ParNew垃圾收集器
-XX:CMSInitiatingOccupancyFraction=75 //使用cms作为垃圾回收,使用75%后开始CMS收集
-XX:+UseCMSCompactAtFullCollection 
//-XX:UseCMSCompactAtFullCollection来控制是否开启内存整理(无法并发,默认是开启的)
-XX:CMSFullGCsBeforeCompaction=10 
//参数-XX:CMSFullGCsBeforeCompaction用于设置执行多少次不压缩的 Full GC 后,
//进行一次带压缩的内存碎片整理(默认值是0)。
"

(7)JVM内存调优

(7.1)常见的内存调优命令

JVM在内存调优方面,提供了几个常用的命令,分别为jps,jinfo,jstack,jmap以及jstat命令。分别介绍如下:

① jps,查看所有的jvm进程,包括进程id,启动路径等

主要用来输出JVM中运行的进程状态信息,一般使用jps命令来查看进程的状态信息,包括JVM启动参数等。
jps -l 显示当前所有java进程pid。

jps 用于显示当前所有java进程pid,jps经常使用的参数如下:
-q:仅输出VM标识符,不包括class name,jar name,arguments in main method 
-m:输出main method的参数 
-l:输出完全的包名,应用主类名,jar的完全路径名 
-v:输出jvm参数

② jinfo,观察进程运行的环境参数

主要用来观察进程运行环境参数等信息。
jinfo 可以观察进程运行环境参数,通过jinfo pid可以查看指定进程的运行环境参数。
查看正在运行的JVM参数 jinfo -flags pid

③ jstack,观察jvm中当前所有线程的运行情况和线程当前状态

主要用来查看某个Java进程内的线程堆栈信息。jstack pid 可以看到当前进程中各个线程的状态信息,包括其持有的锁和等待的锁。
jstack 用于显示jvm中当前所有线程的运行情况和线程当前状态,一般使用的参数为-l,表示长列表,并且打印锁的相关附加信息。jstack的输出中还可以看到每一个线程当前所处的状态以及其当前所占用的锁和等待的锁,还可以检测是否存在死锁。
jstack pid 查看线程状态
注意实战 分析死锁问题

④ jmap,监视进程运行中jvm物理内存的占用情况

用来查看堆内存使用状况。jmap -heap pid可以看到当前进程的堆信息和使用的GC收集器,包括年轻代和老年代的大小分配等。
jmap 用来打印内存映射以及查看堆内存细节等,一般情况下使用-heap参数来 打印堆内存的概要信息,GC使用的算法以及堆的一些配置等信息。
jmap -heap pid 查看内存使用情况
jmap -histo pid | more 查看所有对象
jmap -histo:live pid | more 查看活跃对象
jmap -dump:format=b,file=/tmp/dump.dat pid 将JVM当前内存中的情况dump到文件中
jhat -port 9999 /tmp/dump.dat 通过jhat对dump文件进行分析,还可以通过MAT工具进行分析

⑤ jstat,利用jvm内建的指令对java应用程序的资源和性能进行实时的命令行的监控

进行实时命令行的监控,包括堆信息以及实时GC信息等。可以使用jstat -gcutil pid1000来每隔一秒来查看当前的GC信息。

jstat 一般用来观察GC情况,并且进行实时的分析与监控,可以使用的参数如下:
-class 显示ClassLoad的相关信息
-compiler 显示JIT编译的相关信息
-gc 显示和gc相关的堆信息
-gccapacity 显示各个代的容量以及使用情况
-gcmetacapacity 显示metaspace的大小
-gcnew 显示新生代信息
-gcnewcapacity 显示新生代大小和使用情况
-gcold 显示老年代和永久代的信息
-gcoldcapacity 显示老年代的大小
-gcutil   显示垃圾收集信息
-gccause 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因
-printcompilation 输出JIT编译的方法信息

jstat [-命令选项] [进程id] [间隔时间/毫秒] [查询次数]
查看class加载统计 jstat -class [进程id]
查看编译统计 jstat -compiler [进程id]
垃圾回收统计 jstat -gc [进程id]
eg: jstat -gc 6219 1000 5
垃圾回收统计, 每一秒钟打印一次, 共打印五次

(7.2)VisualVM工具的使用

  • VisualVM,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。
  • VisualVM使用简单,几乎0配置,功能还是比较丰富的,几乎囊括了其它JDK自带命令的所有功能。
  • 在jdk的安装目录的bin目录下,jvisualvm.exe。
  • VisualJVM不仅是可以监控本地jvm进程,还可以监控远程的jvm进程,需要借助于JMX技术实现。

(7.3)排查线上的服务异常的步骤

  • 首先查看当前进程的JVM启动参数,查看内存设置是否存在明显问题。
  • 查看GC日志,看GC频率和时间是否明显异常。
  • 查看当前进程的状态信息top -Hp pid,包括线程个数等信息。
  • jstack pid查看当前的线程状态,是否存在死锁等关键信息。
  • jstat -gcutil pid查看当前进程的GC情况。
  • jmap -heap pid查看当前进程的堆信息,包括使用的垃圾收集器等信息。
  • 用jvisiual工具打开dump二进制文件,分析是什么对象导致了内存泄漏,定位到代码处,进行code review。

一般情况下,我们在测试环境上线新服务的时候,应该重点关注并且查看当前新服务的内存使用以及回收情况,避免新服务种出现内存异常导致服务崩溃的现象发生。

(8)Java中的类加载机制

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

(8.1)类加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。类加载过程则包括前面五个阶段。

  • 加载:
    加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构

  • 验证:
    验证的作用是确保被加载的类的正确性,包括文件格式验证,元数据验证,字节码验证以及符号引用验证

  • 准备:
    准备阶段为类的静态变量分配内存,并将其初始化为默认值。
    假设一个类变量的定义为public static int val = 3;那么变量val在准备阶段过后的初始值不是3而是0。

  • 解析:
    解析阶段将类中符号引用转换为直接引用。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。

  • 初始化:
    初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

(8.2)类加载器的分类

  • 启动类加载器(Bootstrap ClassLoader):
    最顶层的加载类
    启动类加载器负责加载存放在 JDK\jre\lib (JDK代表JDK的安装目录,下同) 下的jar包和类,
    或被-Xbootclasspath参数指定的路径中的所有类。

  • 扩展类加载器(ExtClassLoader):
    扩展类加载器负责加载JDK\jre\lib\ext目录中,
    或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。

  • 应用类加载器(AppClassLoader):
    应用类加载器负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。

(8.3)类加载器的职责

  • 全盘负责:
    当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。

  • 父类委托:
    类加载机制会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
    父类委托机制是为了防止内存中出现多份同样的字节码,保证java程序安全稳定运行。

  • 缓存机制:
    缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

(9)双亲委派模型

(9.1)双亲委派模型介绍

每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。

加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader当父类加载器无法处理时,才由自己来处理当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器

在这里插入图片描述

(9.2)双亲委派模型的好处

双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

(9.3)如果我们不想用双亲委派模型怎么办?

为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写 loadClass() 即可。

(9.4)自定义类加载器

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值