JVM——自动内存管理

内存区域与内存溢出异常

运行时数据区域分为程序计数器、Java堆、虚拟机栈、本地方法栈、方法区五大块构成,下面将会对这五大块分别进行学习总结。对他们的内存分配,什么代码会导致内存溢出异常,如何避免异常及发生异常后该如何排查及解决等。除了这五个以外,还有运行时数据区域以外的直接内存和方法区里的运行时常量池也会拿出来简单的做下总结

  • 程序计数器

程序计数器属于线程私有,可以看做是当前线程运行的指示器。分支、循环、跳转、异常处理、线程恢复等都需要依赖这个计数器来完成。因为Java虚拟机是多线程轮流切换、分配处理器执行时间的方式来实现的,在任何情况下,处理器(多核处理器则是一个内核)都只会执行一条线程。所以每个线程都需要一个计数器来保证他们可以独立运行互不影响。

  • java堆

Java堆是虚拟机管理的内存中最大的一块。是所有线程共享的一块内存区域,在虚拟机启动时会创建。Java堆的唯一目的是存放对象实例。也可以说Java里所有的对象实例以及都是在堆里进行内存分配的。因为Java堆是垃圾收集器管理的内存区域,所以又会被称为是GC堆。并且Java堆还可以划分出多个内存私有的缓冲区,以提升对象分配时的速率。Java堆细分的目的也是为了更好地分配和回收内存。如果Java堆中内有内存完成实例分配,且堆也无法再扩展时,将抛出OutOfMemoryError异常。
因为Java堆是存储对象实例的,所以只要我们不断的创建对象,并保证GC Roots到对象之间有可达路径(来避免垃圾回收机制清除这些对象)。对象越来越多达到堆的最大容量限制后就会产生内存溢出异常。出现Java堆内存溢出时,异常信息会在OutOfMemoryError后面进一步提示“Java heap space”。
解决方法为:常规方法是首先通过内存映像分析工具(如 Eclipse里是Eclipse Memory Analyzer,ide可以用JProfilerl)进行分析。要先确认异常对象是否是必要的。也就是说分析清楚异常是属于内存泄漏还是内存溢出。如果是内存泄漏,可以进一步通过工具查看出对象到GC Roots的引用链,分析出垃圾收集器无法回收它们的原因。根据对象类型信息及它到引用链的信息基本可以查到导致内存泄漏的代码的位置。如果是内存溢出,这就说明这些对象也都是必须存活的,就应当检查Java虚拟机的堆参数设置,看看是否还有上调空间;再从代码上检查是否有对象设计不合理或者生命周期和持有时间过长,再从代码上做处理。

  • 虚拟机栈和本地方法栈

因为本地方法栈和虚拟机栈一样(唯一区别是本地方法栈运行在本地为虚拟机使用到本地方法服务),且都是线程私有。所以以下只写虚拟机栈·。
虚拟机栈描述的是Java方法执行的线程模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储局部变量表等信息,每个方法从开始调用到执行完毕的过程就是一个栈帧在虚拟机栈中入栈到出栈的过程。栈里面相对重要的也是局部变量表部分,局部变量表存放的各种Java基本数据类型、对象引用和returnAddress类型。这些数据在局部变量表中以变量槽(Slot)来表示,其中64位的Long和double占用两个变量槽,其余的数据类型占一个。局部变量表所需的内存空间在编译期间完成分配,在方法进入的时候,该方法需要在栈帧中分配的局部变量空间是完全确定的,在方法运行期间不会改变(即增加或减少Slot的数量)。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈容量可以动态扩展,当栈扩展时无法申请足够的内存则会抛出OutOfMemoryError异常。
栈容量只能由 -Xss来设定。Java虚拟机规范中允许虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机的选择是不支持(原因不知道)。所以,只有在创建线程的申请内存时就因为无法获得足够内存会出现OutOfMemoryError异常,无论是由于栈帧太大还是虚拟机栈的容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常。而使用Classic虚拟机就会有OutOfMemoryError异常出来。出现StackOverFlowError异常时,会有明确的错误堆栈可供分析,相对而言就比较容易定位问题所在而现在大多使用的64位虚拟机就很大程度的减少这种异常。

  • 方法区及其运行时常量池

方法区和Java堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。关于书中基于方法区而讲的永久代这里就不再做赘述。如果方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常。
运行时常量池则是方法区的一部分Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放在方法区的运行时常量池中。它有一个重要的特征是具备动态性。Java并不要求常量在编译时才能产生,运行期间也可以将新的常量放入池中,其中String的intern()方法就是利用这一特性。当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回常量池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。运行时常量池溢出时,在OutOfMemoryError异常后面会跟随提示:“PermGen space”,说明运行时常量池的确是属于方法区的一部分。因为JDK8以上已完全扔掉了永久代,而JDK8使用 -XX:MaxMeta-spaceSize的参数把方法区容量限制在6MB时,不会出现JDK6的那种异溢出异常。
当前很多主流框架,如Spring、Hibernate对类进行增强时,都会使用到CGLib这类字节码技术,当增强的类越多,就需要越大的方法区以保证动态生成的新类型可以载入内存。JDK8以元空间代替永久代。HotSpot提供的元空间的防御措施有:
-XX:MaxMetaspaceSize:设置元空间最大值,默认是一,即不限制,或者说只受限于本地内存大小。
-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMeta-spaceSize(如果设置了的情况下),适当提高该值。
-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似还有-XX:MaxMetaspaceFreeRatio,用于控制最大的元空间剩余的百分比。

  • 直接内存

直接内存并不是虚拟机运行时数据区域的一部分。但是这部分内存也频繁被使用到。而且也会导致OutOfMemoryError异常出现。有一个NIO类,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块儿内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了Java堆和Native堆之间来回复制数据。
直接内存可以通过 -XX:MaxDirectMemorySize参数来指定(如果不指定则默认与Java堆最大值一致)。而Java10申请分配内存的方法是Unsafe::allocateMemory()。
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况(如果内存溢出产生的Dump文件很小,而程序中由直接或者间接使用了DirectMemory,就可以考虑直接内存溢出的原因了)。

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

对象已死?

垃圾收集器在对堆进行垃圾回收之前,首先要确认这些对象中,那些是存活的(还在使用的),哪些是已死的(没有地方使用,需要回收的)

一、引用技术算法

判断对象是否存活的方法:在对象中添加一个引用计数器,每当有一个地方调用时,计数器就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。弊端是如果有两个对象互相引用的话,这种情况计数器是无法回收他们的。

二、可达性算法(目前使用相对较多的)

该算法是通过一些根对象作为起始节点集(这些对象称之为GC Roots),从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径成为引用链,如果某个对象到GC Roots间没有任何引用链相连,或者说该对象到GC Roots不可达时,证明该对象不再被使用。
Java体系里可以作为GC Roots的对象有:

  • 在虚拟机栈或本地方法栈(JNI)中引用的对象。如各个线程被调用的方法,堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,比如Java类的引用类型,静态变量。
  • 在方法区中常量引用的对象,比如字符串常量池里的引用。
  • Java虚拟机内部的引用,比如基本数据类型对应的Class对象,一些常驻的异常对象等。
  • 所有被同步锁(synchronized)持有的对象。
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  • 系统类加载器。
  • 其他一些临时加入的对象
三、引用

引用分为强引用、软引用、弱引用和虚引用

  • 强引用是传统引用的定义,是指在程序代码中普遍存在的引用赋值,即类似"Object obj = new Object()"这种引用关系。无论在任何情况下,只要强引用的关系还在垃圾收集器就不会回收掉被引用的对象。
  • 软引用是用来描述一些还在用,但非必须的对象。软引用关联着的对象在系统将要发生内存溢出之前,会把这些对象列为列进回收范围中进行第二次回收,如果这次回收还没有足够的内存,就会抛出内存溢出异常。用SoftReference类来实现软引用。
  • 弱引用也是描述那些非必须的对象,但它的强度比软引用更弱一些,弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收到只被弱引用关联的对象。用WeakReference类来实现弱引用。
  • 虚引用是最弱的引用关系。为一个对象设置虚引用的唯一目的是当这个对象被收集时会收到一个系统通知。
四、生存还是死亡

在可达性算法中,如果一个对象被判定为不可达(没有找对与GC Roots相连的引用链),这时候这些对象也不是就一定会被回收。而是会被第一次标记,然后筛选(判断此对象是否有finalize()方法,没有会被回收)。如果对象有finalize方法,会把对象放进一个F-Queue的队列之中。该对象如果在finalize方法里关联上了引用链上的任何一个对象,即可被移出待回收行列。否则该对象就要被回收。
注:当今finalize方法已经基本很少用了,因为finalize()能做的,用try-finally或者其他方法可以做的更好,更及时。

五、回收方法区

回收方法区的垃圾不会像回收Java堆那么重要和频繁,甚至有人认为方法区是没有垃圾收集行为的。但事实上方法区里确有未实现或未能完整实现方法区类卸载的收集器存在。方法区的回收主要有两部分内容:废弃的常量和不再使用的类型。
判定一个常量是否废弃有三个条件:

  • 该类所有实例都被回收,即Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是精心设计的可替换类加载器的场景如JSP的重加载等。
  • 该类对应的Java.lang.Class对象没有在任何地方被引用。无法在任何地方通过反射访问该类的方法。
    关于是否要对类型的回收。HotSpot提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:TraceClassUnLoading查看类加载和卸载信息。

垃圾收集算法

垃圾收集算法分为直接垃圾收集和间接垃圾收集。我们主要是来研究下间接垃圾收集。以及一些收集算法:标记-清除算法、标记-复制算法和标记-整理算法。

一、分代收集理论

我们通常把Java堆划分为新生代和老年代。顾名思义,新生代主要存放一些新生成的对象,每次垃圾收集都会发现有大批对象死去,而每次回收后存活的少量对象,会逐步的晋升到老年代中存放。
分代理论中有个一跨代引用假说。跨代引用相对于同代引用来说占极少数。(比如某个新生代对象和老年代存在跨代引用,因为老年代难以消亡,所以该新生代对象也得以存活。等到该新生代对象随着时间晋升到老年代时,这种跨代引用也就被消除了)
分代收集也称为部分收集(指目标不是完整收集整个Java堆的垃圾收集),部分收集又分为:

  • 新生代收集:指目标只是新生代的收集
  • 老年代收集:指目标只是老年代的收集(目前只有CMS收集器属于单独收集老年代的)
  • 混合收集:指目标是整个新生代和部分老年代的收集(目前只有GI收集器是这种收集)
  • 整堆收集:收集整个Java堆和方法区的垃圾收集。
二、标记-清除算法

该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。也可以反过来,标记存活的对象,统一回收未被标记的对象。
该算法有两个缺点:1、执行效率不稳定,如果Java堆里包含了大量对象,而且大部分是需要回收的,这时就需要大量的标记和清除动作,导致标记和清除得效率都随着对象的增加而降低。2、会造成内存空间碎片化的问题,标记、清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致以后程序运行中需要分配大对象的时候无法找到足够内存而不得不提前触发另一次垃圾收集动作。(或者抛出内存溢出异常)。

三、标记-复制算法

该算法是将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还存活着的对象复制到另一块上面,然后再把快用完的那一块全部清理掉。如果对象中大部分都是存活的,那么该算法会产生大量的内存间复制的开销,如果大多数对象都是可回收的,那么算法就只是复制少数的存活的对象。而且每次都是半区进行内存回收,所以不用考虑产生空间碎片的情况,只要移动堆顶指针,按顺序分配即可。
该算法的缺点就是该算法将内存缩小为之前的一半,空间浪费过多。不过现在也有很多商用Java虚拟机是优先采用这种算法去回收新生代的。但是老年代基本不用该算法。

四、标记-整理算法

标记整理算法其中的标记过程和标记清除算法一样。但后续不是清除了,就如字面意思是整理,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。两种算法的本质差异是:前者是非移动性的回收算法,后者是移动式的。是否移动的优缺点为:
如果移动存货对象,尤其在老年代这种每次回收都有大量对象存活的区域,移动存活对象并更新所有引用这些对象的地方是一种极为负重的操作。且这种对象移动操作必须全程暂停用户应用程序才能进行。但如果不考虑移动和整理存活对象的话,弥散在堆中的存货对象导致的空间碎片问题就只能依赖更为复杂的内存分配器和内存访问器来解决(如分区空闲分配链表),这样就会影响应用程序的吞吐量。
所以,基于这两点,移动则内存回收时更为复杂,不移动则内存分配时更为复杂。所以,要根据实际情况,需要停顿时间不能太长的话,可以选择不移动;需要吞吐量不能太低的话,可以选择移动。
书中介绍一种和稀泥的办法:即平时多数时间虚拟机都采用标记清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经达到影响对象分配时,再采用标记整理算法,以获得规整的内存空间。而基于标记清除算法的CMS收集器在面临空间碎片过多时就是采用这种算法。

经典垃圾收集器

一、Serial收集器

这个收集器是单线程工作的新生代收集器,因为它在垃圾收集时必须暂停其他所有工作线程,直到它收集结束。所以,该收集器简单而高效,是所有收集器里额外内存消耗最小的。对于单核处理器或者处理器较少的环境来说,Serial收集器是单线程收集效率最高的。Serial收集器对于运行在客户端模式下的虚拟机来说还是不错的。

二、ParNew收集器

该收集器实际上是Serial收集器的多线程版本,并且也是目前除了Serial以外唯一可以与GMS收集器配合使用的收集器(GMS收集器属于老年代收集器)。之后ParNew收集器也直接合入GMS收集器了。该收集器可以用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。在此,额外说下并行和并发:

  • 并行:并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是出于等待状态。
  • 并发:并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时,应用程序所处理的吞吐量将受到一定影响。
三、Parallel Scavenge收集器

该收集器也是新生代收集器,采用的是标记-复制算法,也是能够并行收集的多线程收集器。该收集器的主要目标侧重达到一个可控制的吞吐量。
吞吐量:处理器用于运行用户代码的时间与处理器总消耗时间的比值。即(运行用户代码时间/(运行用户代码时间+运行垃圾收集时间))。
停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
该收集器提供两个参数来精准控制吞吐量:

  • -XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间,该参数允许的值是一个大于0的毫秒数
  • -XX:GCTimeRetio 直接设置吞吐量大小。该参数的值应当是一个大于0小于100的整数。举个例子,如果该参数设为19,则垃圾收集时间应为总时间的5%(1/(1+19))。也可以这样理解:吞吐量=该参数/(该参数+1)。
四、Serial Old 收集器

该收集器是Serial收集器的老年代版本。所以也是单线程,使用标记-整理算法。

五、Parallel Old 收集器

该收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。该收集器与Parallel Scavenge配合多用于吞吐量优先的情况。

六、CMS收集器

CMS 收集器是以最短回收停顿时间为目标的收集器,基于标记-清除算法。它的运作过程分为四步:初始标记——并发标记——重新标记——并发清除。
CMS收集器的内存回收过程是与用户线程一起并发执行的。也因为该收集器的并发收集,所以停顿时间很低。但是也就造成了该收集器对处理器资源非常敏感且无法处理浮动垃圾。同时,因为该处理器使用的是标记-清除算法,所以会导致收集结束后出现大量的空间碎片。之前提过,空间碎片过多的话,很难为大对象分配内存,容易触发Full GC。

Garbage First收集器(G1收集器)

G1收集器在Oracle官方被称为全功能的垃圾收集器。之前介绍的收集器都是基于新生代/老年代或者整个Java堆(Full GC)进行收集的。而G1可以面向堆内存的任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要扮演新生代或老年代空间。
而G1的“化整为零”的方法也有许多细节要注意:
比如:将Java堆划分为多个独立的Region后,Region与Region之间存在跨Region引用对象要如何解决? 在并发标记阶段如何保证收集线程与用户线程互不相干地运行?怎样建立可靠的停顿预测模型?等。
如果不计算用户线程运行的动作,G1分为以下4个步骤:初始标记——并发标记——最终标记——筛选回收。G1整体上是采用标记-整理算法,但局部(两个Region之间)又是基于标记复制算法。
很多人会拿GMS与G1来比较。在此呢,我来总结一句话:目前小内存应用上GMS的表现大概率会优于G1,而大内存应用上G1更有优势。而这个Java堆容量内存大小的平衡点在6——8G之间。

低延迟垃圾收集器

衡量一款垃圾收集器有三个指标:内存占用、吞吐量和延迟。而这三种通常不会全部满足,最多会满足其中两项。而接下来要讲的两款收集器都是低延迟收集器

一、Shenandoah 收集器

该收集器Oracle公司不支持,所以OracleJDK是没有的,只有OpenJDK上才会存在。所以在此也不多做介绍,如果以后有机会用到的话在做补充。

二、ZGC收集器

ZGC收集器的目标和Shenandoah一样,尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。介绍:ZGC收集器是一款基于Region内存布局的,目前没有设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为目标的一款收集器。
ZGC的Region具有动态性——动态创建和销毁。一般分为大中小三种类型。
大型Region:容量固定为2MB,用于放置小于256KB的小对象。
小型Region:容量固定为32MB,用于放置大于等于256KB小于4MB的对象。
大型Region:不固定,可以动态变化,但必须为2MB的整数倍。用于放置4MB或以上的大对象,且只能放一个。
ZGC收集器的核心——并发整理算法的实现:
它有一个标志性设计是它采用的染色指针技术,对象标记的过程中需要给对象打上三色标记,这些标记的本质上就只和对象的引用有关,而与对象本身无关——某个对象只有它的引用关系能决定它存活与否,对象上的其他所有属性都不能够影响它的存活判定结果。
染色指针是一种直接将少量额外的信息存储在指针上的技术。
1、染色指针可以使得一旦某个Region的存货对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。
2、染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写内存屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然可以省去一些专门记录的操作。
3、染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,一遍日后进一步提高性能。
ZGC的运作过程为:并发标记——并发预备重分配——并发重分配——并发重映射。ZGC做到了几乎整个收集过程都全程可并发、短暂停顿也至于GC Roots大小相关而与堆内存大小无关,因而实现了任何堆上停顿都小于10毫秒的目标。
G1是通过写屏障来维护记忆集,才能处理跨代指针,得以实现Region的增量回收。因为记忆集要占用大量内存空间,所以,G1一般就需要很大的空间内存。而ZGC就不需要使用记忆集,连分代都没有。但这也限制了它所能承受的对象分配速率也不会太高。有些情况下还会产生大量的浮动垃圾。
性能方面ZGC目前还处于实验状态。虽然ZGC目前还不是很成熟,但它在低延迟即停顿时间方面目前已经是遥遥领先的。

其他收集器及收集器的权衡、日志信息总结

一、Epsilon收集器

该收集器不能进行垃圾收集,记住,是不能进行垃圾收集!
如果我们要求应用只要运行数分钟甚至数秒,只要虚拟机能够正确分配内存,在堆耗尽之前就退出,那么该收集器就是个不错的选择。
另外,一个垃圾收集器除了垃圾收集这个本职工作之外,它还要负责堆的管理与布局、对象的分配、与解释器的协作、与编译器的协作、与监控子系统协作等职责,其中至少堆的管理和对象的分配这部分功能是Java虚拟机能够正常运作的必要支持,是一个最小化功能的垃圾收集器也必须实现的功能。

二、收集器的权衡

一般来讲,选择一款适合自己应用的收集器需要考虑以下三个因素:

  • 应用的主要关注点是什么?如果是数据分析、科学类计算类的任务,目标是能尽快算出结果,那吞吐量就是主要关注点;如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,这样延迟就是主要的关注点;而如果是客户端应用或者嵌入式应用,那垃圾收集的内存占用则是不可忽视的。
  • 运行应用的基础设施如何?比如硬件规格,要涉及的系统架构是什么?处理器数量(几核)?分配内存的大小;选择的操作系统是什么(Linux,Windows等)
  • 使用的JDK是什么?版本号多少?是OracleJDK,OpenJDK,OpenJ9等。
三、虚拟机及垃圾收集器日志

在JDK9以前,HotSpot并没有提供统一的日志处理框架,虚拟机各个功能模块的日志开关分布在不同的参数上,日志级别、循环日志大小、输出格式、重定向等设置在不同功能上都要单独解决。JDK9之后,这些日志都收归到“-Xlog”参数上,这个参数的能力也相应被扩展了。

Xlog [ : [selector] [ : [output] [ : [decorators] [ : output-options]]]]

日志级别从低到高一共有Trace、Debug、Info、Warning、Error、Off六个级别,默认级别为Info。以G1收集器为例,JDK9前后采集日志方式对比。

  • 查看GC基本信息,在JDK9之前使用-XX:+PrintGC,JDK9之后用-Xlog:gc
  • 查看GC详细信息,在JDK9之前使用-XX:PrintGCDetails,之后用-X-log:gc*
  • 查看GC前后的堆、方法区可用容量变化,在JDK9之前使用-XX:+PrintHeapAtGC,之后使用-Xlog:gc+heap=debug
  • 查看GC过程中用户线程并发时间以及停顿的时间,在JDK9之前使用-XX:Print-GCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime,之后使用-Xlog:safepoint
  • 查看收集器Ergonomics机制(自动设置堆空间各分代区域大小、收集目标等内容)自动调节的相关信息。之前用-XX:PrintAdaptive-SizePolicy,之后用-Xlog:gc+ergo*=trace
  • 查看熬过收集后剩余对象的年龄分布信息,之前-XX:PrintTenuring-Distribution,之后使用-Xlog:gc+agc=trace

实战:内存分配与回收策略

Java自动内存管理的最根本目标是自动化的解决两个问题:自动给对象分配内存以及自动回收分配给对象的内存

一、对象优先在新生代的Eden分配

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

二、大对象直接进入老年代

大对象就是指需要大量连续内存空间的对象,比如那种很长的字符串、元素数量很庞大的数组。大对象对虚拟机内存分配来说是一个很坏的消息,更糟糕的是一群时间很短的大对象,这是我们在写代码是要注意避免出现的。因为在分配空间时,大对象容易导致明明还有不少空间时就提前触发了垃圾收集,以获取足够的连续空间才能安置好他们,而复制对象时,大对象就意味着高额的内存复制开销。

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

一般是这样的:对象通常在Eden区里诞生,如果经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,该对象就会被移动到Survivor空间中,并为其标记为1岁,此后在Survivor中每熬过一次MinorGC,年龄就会加1,等年龄到一定程度(一般默认为15,也可以通过-XX:MaxTenuringThreshold参数进行设置)就会进入老年代。

四、动态对象年龄判定

如果Survivor中的相同年龄所有对象大小总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。

五、空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,那这一次MinorGC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFalure参数的设置值是否允许担保失败;如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次FullGC。不过通常情况下,-XX:HandlePromotionFalure开关一般是打开的为了避免Full GC过于频繁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值