Fork me on GitHub

Java内存区域和内存溢出异常

注意:所有文章除特别说明外,转载请注明出处.

自动内存管理机制

[TOC]

第二章 Java内存区域和内存溢出异常

因为Java将内存控制权利交给Java虚拟机,所以一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎么样使用内存的,那么排错将会是一个很难的任务。

2.2 运行时数据区域

Java虚拟机在执行Java程序过程中会将它所管理的内存划分成若干不同的数据区域。这些==区域各自有不同的用途==,创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域随着虚拟机进程的结束而销毁。

2.2.1 程序计数器

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

提示:Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程的指令。

在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令,所以为了线程在切换之后能恢复到正确的执行位置,==每条线程都需要有一个独立的程序计数器==,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

2.2.2 Java虚拟机栈

Java虚拟机栈是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型。每个方法在执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

Java内存可以粗糙的分为:堆内存 | 栈内存。

栈,表示现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。局部变量表主要存放了编译器可知的==各种数据类型、对象引用==。不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其它与此对象相关的位置。

2.2.3 本地方法栈

本地方法栈和虚拟机栈所发挥的作用类似,它们之间的区别是:虚拟机栈为虚拟机执行Java方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

2.2.4 Java堆

Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。

从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:在细致一点有:Eden空间、From Survivor、To Survivor空间等。

Java堆可以处于物理不连续的内存空间中,只要逻辑上是连续即可,就像磁盘空间一样。在实现的时候可以是固定大小,也是可以扩展的,当前主流是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果堆中没有完成实例的分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

提示:==新生代的目标就是尽可能的快速收集那些生命周期短的对象==。新生代(==Eden空间、From Survivor、To Survivor空间==)。eden空间存放新创建的对象。From区和To区都是救助空间Survivor Space。当Eden区满时,JVM执行垃圾回收GC(Garbage Collection),垃圾收集器暂停应用程序,并会将Eden Space还存活的对象复制到当前的From救助空间,一旦当前的From救助空间充满,此区的存活对象将被复制到另外一个To区,当To区也满了的时候,从From区复制过来并且依然存活的对象复制到Old区,从而From和To救助空间互换角色,维持活动的对象将在救助空间不断复制,直到最终转入Old域。

提示:堆进一步划分的目的是更好地回收内存,或者更快地分配内存。Java堆可以处于物理不连续的内存空间,只要逻辑上连续即可。

2.2.5 方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。别名:Non-Heap(非堆)。

HotSpot虚拟机中方法区也常被称为“永久代”,本质上两者并不等价。仅仅是因为HotSpot虚拟机设计团队用永久代来实现方法区而已,这样HotSpot虚拟机的垃圾收集器就可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。

但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。相对而言,垃圾收集行为在这个区域是经常出现的,但并非数据进入方法区后就“永久存在”。

提示:根据Java虚拟机规范规定,当方法区无法满足内存分配需求时,将会抛出 OutOfMemoryError 异常。

2.2.6 运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用),存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言不要求常量一定只有编译期才能产生,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中。

2.2.7 直接内存

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

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

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

2.3 HotSpot虚拟机对象探秘

我们来详细的了解一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。

2.3.1 对象的创建

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

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

注意:==指针碰撞==:假设Java堆中的内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是将那个指针向空闲空间那边挪动一段与对象大小相同的距离。

注意:==空闲列表==:如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,那没有办法进行指针碰撞,在分配的时候从列表中找到一块足够大的空间划分给对象实例,更新列表上记录。

上诉分配内存的方式是由Java堆是否规整决定,而Java堆是否规整是由所采用的垃圾收集器是否带有压缩整理功能决定。

注意:在并发情况下,对象的创建是线程不安全的,所以对于这一问题的两种解决方案:1.对分配内存空间的动作进行同步处理(==CAS配上失败重试==的方式保证更新操作的原子性)。2.==本地线程分配缓冲(TLAB)==,将内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。那个线程要分配内存,就在哪个线程的TLAB上分配,只有在TLAB用完并分配新的TLAB时才需要锁定。

提示:CAS操作是乐观锁,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。而CAS操作是compare and swap(比较与替换),是一种无锁算法。在Java中通过CAS提供线程安全操作。

3.接下来,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象头中,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会与不同的设置方式。

提示:new指令执行完后,再按照程序员的意愿执行init方法后一个真正可用的对象才诞生。

2.3.2 对象的内存布局

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

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等),这部分数据的长度在32位和64位的虚拟机中是32bit和64bit,官方称为:Mark World。

提示:==Mark World 被设计成一个非固定的数据结构==以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

==对象头的另一部分是类型指针,即对象指向它的类元数据的指针==,虚拟机通过这个指针来确定这个对象是哪个类的实例。

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

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

2.3.3 对象的访问定位

建立对象就是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种:

1.如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

2.如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何防止访问类型数据的相关信息,reference中存储的直接就是对象的地址。

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

2.4 OutOfMemoryError 异常实战

除了程序计数器之外,虚拟机内存中的其他几个运行时区域都会发生 OutOfMemoryError(OOM) 异常的可能。

2.4.1 Java堆溢出

Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象到达最大堆容量限制后就会抛出异常。

提示:1.通过参数 -XX:+HeapDumpOnOutOfMemoryError 可以让虚拟机在出现内存溢出异常时 Dump 出当前内存堆转储快照。2.堆的最小值 -Xms参数,堆的最大值 -Xmx参数。

出现OOM异常,首先确定是内存泄露还是内存溢出。如果是内存泄漏:通过工具查看泄漏对象到GC Roots的引用链。内存溢出:检查虚拟机堆参数(-Xmx与-Xms),与物理内存对比是否可以调大。

2.4.2 虚拟机栈和本地方法栈溢出

在HotSpot虚拟机中不区分虚拟机栈和本地方法栈,所以对于 HotSpot 来说,虽然 -Xoss 参数(设置本地方法栈大小)存在,实际无效。栈容量只由 -Xss参数决定。

关于虚拟机栈和本地方法栈在Java规范中描述了两种异常:

1.如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出 StackOverflowError 异常。

2.如果虚拟机在扩展时无法申请到足够空间,抛出 OutOfMemoryError 异常。

提示:这里存在重叠,如果栈空间无法继续分配时,到底是内存太小还是已经使用的栈空间太大。

2.4.3 方法区和运行时常量池溢出

因为运行时常量池是方法区的一部分。可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制方法区的大小,从而间接限制其中常量池容量大小。

方法区存放Class的相关信息(类名、访问修饰符、常量池、字段描述、方法描述等)。

方法区溢出是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判断条件是苛刻的。

2.4.4 本机直接内存溢出

DirectMemory容量可以通过 -XX:MaxDirectMemorySize 指定,如果不指定则默认与Java堆最大值(-Xmx指定)一样。

提示:DirectMemory导致的直接内存溢出在Heap dump文件中看不出明显的异常。


第三章 垃圾收集器与内存分配策略

3.1 概述

GC需要完成的三件事情:

1.哪些垃圾需要回收?

2.什么时候回收?

3.如何回收?

总结:当需要排查各种内存溢出问题、当垃圾收集称为系统达到更高并发的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。

3.2 判断对象是否已死

在堆中几乎放着==所有的对象实例==,垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些对象还活着,哪些已经死亡。

3.2.1 引用计数法

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

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是==它很难解决对象之间相互循环引用的问题==。

3.2.2 可达性分析算法

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

在Java语言中,可作为GC Roots的对象包括:

1.虚拟机栈(栈帧中的本地变量表)中引用的对象

2.方法区中类静态属性引用的对象

3.方法区中常量引用的对象

4.本地方法栈中JNI(Native方法)引用的对象

3.2.3 再谈引用

JDK1.2以后,Java对引用的感念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)。

1.强引用

以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

2.软引用(SoftReference)

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

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

3.弱引用(WeakReference)

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。

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

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

4.虚引用(PhantomReference)

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

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

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

3.2.4 生存还是死亡

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程。

1.可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。

2.当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

3.2.5 回收方法区

提示:方法区(或Hotspot虚拟中的永久代)的垃圾收集主要回收两部分内容:==废弃常量和无用的类==。

判定一个常量是否是“废弃常量”比较简单,只需要当前系统中没有任何对象引用之且在其它地方也没有引用此常量值,此时可以进行常量回收。而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是 “无用的类” :

1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

2.加载该类的ClassLoader已经被回收。

3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

注意:虚拟机可以对满足上面三个条件的无用类进行回收,这里仅仅是可以,==不像对象那般不使用就必然回收==。

是否对类进行回收,HotSpot虚拟机提供参数:-Xnoclassgc -XX:+TraceClassLoading -XX:+TraceClassUnLoading查看类加载和卸载信息。

3.3 垃圾回收算法

在Java内存空间不够用时都会促使JVM执行垃圾回收,基于回收类型的不同,可以将回收划分为:

1.Minor GC(young GC):回收新生代内存空间(Eden | From Survivor | To Survivor)

    当新对象生成,并且在Eden申请空间失败,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且将存活的对象移动到Survivor区,然后整理Survivor的两个区。这种方式的GC只是在新生区的Eden区进行,不会影响到Old区。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的Minor GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。

2.Full GC:回收新生代 | 年老代 | 持久代内存空间

    Full GC要对整个Heap区进行回收,包括New、Old和PermGen,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM性能调优的过程中,很大一部分工作就是对于Full GC的调节。

    以下原因可能会导致Full GC:

        1.年老区被写满

        2.持久区被写满

        3.System.gc()被显示调用

        4.上一次GC之后Heap的各域分配策略动态变化

3.3.1 标记-清除算法

该算法分为标记和清除两个阶段:1.标记,表示标记出所有需要回收的对象。2.在标记完成之后统一回收所有被标记的对象。

提示:它有两个不足的地方:1.效率问题,标记和清除两个过程效率不高。2.空间问题,标记清除之后会产生大量不连续内存碎片,空间碎片太多会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

3.3.2 复制算法

在前面算法的基础上,为了解决效率问题,使用复制算法,它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。所以在内存分配时不用考虑内存碎片问题,只移动堆顶指针,按照顺序分配内存即可。

注意:该算法在对象存活较多的情况下就需要进行较多的复制操作,效率将会变低。更关键的是如果不想浪费百分之五十的空间,就需要额外的空间进行分配担保,以应用被使用的内存中所有对象存活的情况。所以在老年代不能选用此算法。

3.3.3 标记-整理算法

根据 老年代 的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端(边界)移动,然后直接清理掉端边界以外的内存。

3.3.4 分代收集算法

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

提示:比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的所以我们可以选择“标记-清理”或“标记-整理”算法进行垃圾收集。

提示:延伸面试问题: HotSpot为什么要分为新生代和老年代?
解答:根据上面的对分代收集算法的介绍回答。

3.4 HotSpot的算法实现

3.5 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

注意:虽然我们对各个收集器进行比较,但并非了挑选出一个最好的收集器。因为知道现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的HotSpot虚拟机就不会实现那么多不同的垃圾收集器了。

3.5.1 Serial收集器

Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(”Stop The World”),直到它收集结束。

提示:”Stop The World” 这项工作实际上是虚拟机自动发起和完成的,在用户不可见的情况下将用户工作线程全部停掉。

所以HotSpot虚拟机开发团队知道 “Stop The World” 带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间不断缩短(现仍然还有停顿,寻找最优秀的垃圾收集器过程还在继续)。

但是Serial收集器也有优于其他垃圾收集器的地方:

1.它简单而高效(与其他收集器的单线程相比)。
2.Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
3.Serial收集器对于运行在Client模式下的虚拟机来说是个不错的选择。

3.5.2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。

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

-XX:+UseConcMarkSweepGC 选项默认新生代收集器

-XX:+UseParNewGC 强制指定其实新生代默认收集器

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

3.5.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个==新生代收集器==,它也是使用复制算法的收集器,又是并行的的多线程收集器等。

特点:1.Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU),CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

2.Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。

-XX:MaxGCPauseMillis 参数控制最大垃圾收集停顿时间

-XX:GCTimeRatio 参数设置吞吐量大小

3.5.4 Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

3.5.5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器。

3.5.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。

从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种“标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:

1.初始标记: 暂停所有的其它线程,并记录下直接与root相连的对象,速度很快。

2.并发标记: 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

3.重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。

4.并发清除: 开启用户线程,同时GC线程开始对为标记的区域做清扫。

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:

1.对CPU资源敏感。

2.无法处理浮动垃圾。

3.它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

3.5.7 G1收集器

上一代的垃圾收集器(串行serial, 并行parallel, 以及CMS)都把堆内存划分为固定大小的三个部分: 年轻代(young generation), 年老代(old generation), 以及持久代(permanent generation).

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

被视为JDK1.7中HotSpot虚拟机的一个重要进化特征。它具备以下特点:

1.并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2.分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。

3.空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。

4.可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

G1收集器的运作大致分为以下几个步骤:

1.初始标记

2.并发标记

3.最终标记

4.筛选回收

上面几个步骤的运作过程和CMS有很多相似之处。

1.初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短。

2.并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。

3.最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。

4.最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

3.5.8 理解GC日志

阅读GC日志是处理Java虚拟机内存问题的基本技能。

System.gc() 表示调用了该方法触发垃圾回收 -XX:+PrintGCDetails

3.6 内存分配与回收策略

对象的内存分配在大方向是堆上分配,对象主要分配在新生代的 Eden 区,如果启动本地线程分配缓冲,将按照线程优先在 TLAB 上分配。

提示:新生代GC(Minor GC):表示发生在新生代的垃圾收集动作,Java对象都是朝生夕死的特性,所以Minor GC频繁,回收速度很快。老年代GC(Full GC):表示发生在老年代的GC,Full GC 的速度会比Minor GC的速度慢上10倍以上。

3.6.1 对象优先在Eden区分配

大多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

对象主要分配在新生代的Eden区上,如果启动本地线程分配缓冲,将按照线程优先在TLAB上分配。少数情况下也可能还会直接分配在老年代中。

注意:Minor GC 与 Full GC的区别:

1.新生代GC(Minor GC):指发生新生代的的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特点,Minor GC非常频繁,回收速度一般也比较快。

2.老年代GC(Major GC | Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

3.6.2 大对象直接进入老年代

提示:大对象是指:需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。如果经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来放置它们。

虚拟机提供参数:-XX:PretenureSizeThreshold 令大于这个设置值的对象直接进入老年代分配。

目的是为了避免在Eden区一级两个Survivor区之间发生大量的内存复制。

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

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。

为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次的Minor GC后仍然存活,移动到Survivor空间,对象年龄设置为1。该对象每熬过一次Minor GC,年龄就增加1岁,当它年龄增加到一定程度(默认15岁),就会晋升到老年代。

对象晋升老年代的阈值可以通过参数 ==-XX:MaxTenuringThreshold== 设置。

3.6.4 动态对象年龄判定

为了更好的适应不同程序的内存情况,虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代,如果Survivor 空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需达到要求的年龄。

3.6.5 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试进行一次Minor GC。

3.7 总结

内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,虚拟机之所以提供多种不同的收集器以及大量调节参数,是因为只有根据实际应用的需求、实现方式选择最优的收集方式才能获取最高的性能。没有固定收集器、参数组合、也没有最优的调优方法,那么必须了解每一个具体收集器的行为、优势和劣势、调节参数。

第四章 虚拟机性能监控与故障处理工具

4.1 概述

给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。经常使用适当的虚拟机监控和分析工具可以加速我们分析数据、定位解决问题的速度。

4.2 JDK命令行工具(JDK安装的bin目录下)

JDK监控和故障处理工具

jps:JVM Process Status Tool ,显示指定系统内所有的HotSpot虚拟机进程

jstat: JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据。

jinfo: Configuration Info for Java,显示虚拟机配置信息

jmap: Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)

jhat: JVM Heap Dump Browser,用于分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果

jstack: Stack Trace for Java,显示虚拟机的线程快照

4.2.1 jps 虚拟机进程状况工具

JDK的很多小工具的名字都参考了UNIX命令的命名方式,jps(JVM Process Status)是其中的典型。除了名字像UNIX的ps命令外,它的功能也和ps命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的 本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)

提示:本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。

虽然功能比较单一,但它是使用最高的JDK命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程。

提示:对于本地虚拟机进程来说,LVMID与操作系统的进行ID(pid)是一致的。

//该命令表示输出正在运行程序的包名下的类名
C:\Program Files (x86)\Java\jdk1.8.0_65\bin>jps -l >d:\\test.txt //打印到对应目录下的文件

jps工具主要选项:

选项 | 作用
---|---
-q | 只输出LVMID,省略主类的名称
-m | 输出虚拟机进程启动时传递给主类main()方法参数
-l | 输出主类的全名,如果进程执行的是jar包,输出jar路径
-v | 输出虚拟机进行启动时JVM参数

4.2.2 jstat 虚拟机统计信息监视工具

jstat(JVM Statistics Monitoring Tool) 使用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程(需要远程主机提供RMI支持)虚拟机进程中的类信息、内存、垃圾收集、JIT编译等运行数据,在没有GUI,只提供了纯文本控制台环境的服务器上,它将是运行期间定位虚拟机性能问题的首选工具。

jstat命令格式:

jstat [option vmid [interval[s | ms] [count]]]

//输出vmid=4784的统计信息
C:\Program Files (x86)\Java\jdk1.8.0_65\bin>jstat -gc 4784
S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
11712.0 11712.0  0.0    0.0   94208.0  24278.8   235128.0   119360.0  376736.0 351986.5 50652.0 45309.4    925   11.019  28  7.281   18.299

option 表示用户希望查询的虚拟机主机信息:类装载 | 垃圾收集 | 运行期编译状况。
interval 表示查询间隔
count 表示次数

提示:对于命令格式中VMID和LVMID说明:1.如果是本地虚拟机进程,VMID和LVMID是一致的,如果是远程虚拟机进程,那VIMD格式是:

[protocol:][//]lvmid@hostname[:port]/servername]

jstat工具主要选项:

选项 | 作用
---|---
-class | 监视类装载、卸载数量、总空间以及类装载所耗费的时间
-gc | 监视Java堆状况,包括Eden区、两个survivor区、老年代、元空间的容量、已用空间、GC时间合计等信息
-gccapacity | 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大、最小空间
-gcutil | 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比

提示:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于元空间并不在虚拟机中,而是使用本地内存。因此在默认情况下,元空间的大小仅受本地内存限制。

4.2.3 jinfo Java配置信息工具

jinfo(Configuration Info for Java) 的作用是实时地查看和调整虚拟机各项参数。使用jps命令的-v可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,可以使用jinfo的-flag选项进行查询,jinfo还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来。

jinfo 格式:

jinfo [option] pid

4.2.4 jmap Java内存映像工具

jmap(Memory Map for Java)命令==用于生成堆转储快照(一般称为heapdump或者dump文件)==。从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,检查系统中什么对象最多,各种对象所占内存的大小等。

注意:其它可生成heapdump的方式:1.使用参数:-XX:+HeapDumpOnOutOfMemoryError。2.使用参数:-XX:HeapDumpOnCtrlBreak,然后使用ctrl+break生成。3.Linux系统通过kill -3发送进程退出信号也能拿到dump文件

提示:dump 反应Java堆使用情况的内存镜像,主要包括:系统信息|虚拟机属性|完成的线程DUMP|所有类和对象的状态等。一般在内存不足、GC异常等情况下,我们就会怀疑有内存泄露。这个时候我们就可以制作堆Dump来查看具体情况。分析原因。

提示:虚拟机OOM异常,

jmap的作用并不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前使用的是哪种收集器等。和jinfo一样,jmap有不少功能在Windows平台下也是受限制的,除了生成dump文件的-dump选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux和Solaris系统下使用。

jmap工具主要选项:

选项 | 作用
---|---
-dump | 生成java堆转储快照。
-heap | 显示Java堆详细信息。

...

4.2.5 jhat 虚拟机堆转储快照分析工具

jhat是用来分析dump文件的一个微型的HTTP/HTML服务器,它能将生成的dump文件生成在线的HTML文件,让我们可以通过浏览器进行查阅。

然而实际中我们很少使用这个工具,因为一般服务器上设置的堆、栈内存都比较大,生成的dump也比较大,直接用jhat容易造成内存溢出,而是我们大部分会将对应的文件拷贝下来,通过其他可视化的工具进行分析。

4.2.6 jstack Java堆栈跟踪工具

jstack 命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。

生成线程快照的目的主要是定位线程长时间出现停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者在等待些什么资源。

jstack命令格式:

jstack [option] vmid

option选项:

选项 | 作用
--- | ---
-F | 当正常输出的请求不被响应时,强制输出线程堆栈
-l | 除堆栈外,显示关于锁的附加信息
-m | 如果调用到本地方法的话,可以显示c/c++的堆栈

4.2.7 HSDIS JIT生成代码反编译

4.3 JDK可视化工具

4.3.1 JConsole Java监视与管理控制台

该工具是一种基于JMX的可视化监视、管理工具。

4.3.1 VisualVM 多合一故障处理工具

在IDE中下载安装插件。


第五章 调优案例分析与实战

5.1 案例分析

1.高性能硬件上程序部署策略

...

在经过上面的探讨之后,可以确定式因为程序部署上的过大的堆内存进行回收时带来的长时间的停顿,在用户升级之前使用的32位系统,1.5GB的堆,没有明显的卡顿现象,如果再回到之前的Java堆内存分配则浪费硬件上的投资。

在高性能硬件上部署程序,目前主要有两种方式:

1.通过64位JDK来使用大内存。
2.使用若干个32位虚拟机建立逻辑集群来利用硬件资源。

提示:对于用户交互性强、对停顿时间敏感的系统,可以给Java虚拟机分配超大堆的前提是有把握将应用程序的 Full GC 频率控制得足够低,至少低到不影响用户使用。

控制Full GC 频率的关键是看应用中绝大多数的对象是否符合 朝生夕灭 的原则,即大多数对象生存时间不宜过长,尤其不能有成批量的,长生存时间的大对象产生,这样才能保证老年代空间的稳定性。

在大多数网站形式的应用里面,主要对象的生存周期都应该是请求级或页面级的,会话级和全局级的长生命对象相对较少。在使用64位JDK来管理大内存时,还需要考虑:

1. 内存回收导致的长时间停顿

本文标题:Java内存区域和内存溢出异常

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:18:57

原始链接:http://bangjinhu.github.io/undefined/自动内存管理机制/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.