JVM堆空间结构及常用的jvm内存分析命令和工具

jdk8之前的运行时数据区域

 

 程序计数器

 是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,这类内存区域为“线程私有”,此内存区域是唯一一个没有OutOfMenoryError的区域。

java虚拟机栈

   java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

虚拟机栈的局部变量表存放了编译期可知的各种基本数据类型(8中基本数据类型是啥?boolean、byte、int、double、short、long、char、float)、对象引用、returnAddress类型

其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余的数据类型只会占用1个。局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

本地方法栈

  本地方法栈是虚拟机使用到的Native方法服务。

Java堆

  Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,所有的对象都分配在堆上也不那么“绝对”了。

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OOM异常。

方法区(在HotSpot中也称为永久代)

  方法区是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。当方法区无法满足内存分配的 需求时,将抛出OOM异常。

运行时常量池 - 是方法区的一部分

  存放编译期生成的各种字面量和符号引用,这部分内存在类加载后进入方法区的运行时常量池中存放。

  运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性。不要求常量一定只有编译期才能产生,运行期间也有可能将新的常量放入池中,这种特性利用比较多的是String.intern()方法

直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。本机直接内存的分配不会受到Java堆大小的限制,但是会收到本机总内存和处理器寻址空间的限制。

虚拟机中的对象

对象的创建

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

       在类加载检查通过之后,接下来虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定(具体可以看对象的内存布局)为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”,如果Java对中的内存并不是规整的,已使用内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”,选择哪种分配方式是由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  因此,在使用Serial、ParNew等带Compact过程的收集器是,系统采用的分配方法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,通常采用空闲列表。

  还有另外一个问题需要考虑:对象创建在虚拟机是否非常频繁的行为,即使是紧紧修改一个指针所指向的位置,在并发的情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况,解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性,另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个先测浩哥你在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

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

  接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄、是否启用偏向锁等信息。这些信息存放在对象的对象头中。

  上面的工作完成了之后,从虚拟机的角度看一个新的对象已经产生了,但是从Java程序的角度看,对象创建才刚刚开始-init方法还没有执行,所有的字段都还为零。所以,执行完new指令后还会接着执行init方法。

大概总结下:

  1. 类加载检查:检查是否在常量池中(Class文件的静态常量池中)定位到这个类的符号引用,如果没有就执行相应的类加载过程,如果有说明这个类已经加载过了
  2. 分配内存:由于对象所需的内存的大小在类加载完成后便可以完全确认,但是不同的垃圾回收算法使用不同的分配方式:
    • 指针碰撞:堆内存绝对规整,用过的内存和空闲的内存使用一个指针作为分界点的指示器。
    • 空闲列表:堆内存相互交错,虚拟机维护一个列表,看哪些内存块是可用的。
  3. 分配内存的并发问题:两种解决方案:一、失败重试:对分配内存空间的动作同步处理,虚拟机采用CAS配上失败重试的方式保证更新的原子性;二、TLAB(本地线程分配缓冲):哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB分配完了,才需要同步锁定。
  4. 内存空间初始化为零值:保证对象的实例字段在不赋初始值就可以直接使用.
  5. 对象设置:处理对象头中的元数据信息、对象的哈希码等.
  6. 实例对象的init方法:虽然对象产生了,但是init方法还没有执行,init方法包括:成员属性赋值,普通语句块执行,构造函数执行等。

 对象的内存布局 - 可以参考之前的文章:https://www.cnblogs.com/fengtingxin/p/12019544.html

对象在内存中存储的布局可以分为3个区域:对象头(Header)、实例数据(Intance Data)和对齐填充(Padding)、类型指针、数组长度。

 对象头(Header)

  用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit官方称为“Mark Word”。Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储更多的信息,它会根据对象的状态复用自己的存储空间。

类型指针

  即对象指向它的类元数据的指针,虚拟机通过这个指针来确认这个对象是哪个类的实例。but,并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。

数组长度

  如果当前对象是一个数组,那么对象头中还要有一块用户记录数组长度的数据

实例数据

  是对象真正存储的有效信息,也是在程序中定义的各种类型的字段内容。包括父类的属性

对齐填充

  并不是必然存在的,仅仅起着占位符的作用 这是因为HotSpot要求对象起始地址必须是8个整数倍,那么就通过对齐填充来补全。

  

对象的访问定位

  创建完对象是为了使用对象,我们Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范汇总只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式取定位、访问堆中的对象的具体位置,所以对象的访问方式也是取决于虚拟机实现的。目前主流的访问方式由两种:句柄访问、直接指针访问。

  句柄访问:Java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

  直接指针访问:reference中存储的直接就是对象地址

句柄访问(句柄访问) (直接指针)

两种对象访问方式优点比较:

  句柄访问:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。

  直接指针访问:速度更快,节省了一次指针定位的时间开销,由于对象的访问在Java中非常的频繁。HotSpot使用的是直接指针访问的。

对象是“生”是“死”?

对象的四种引用:

引用分为强引用,软引用,弱引用和虚引用四种,这四种饮用强度依次逐渐减弱。

1.强饮用就是在程序代码中普遍存在的,是指创建一个对象并把这个对象赋值给一个引用变量。只要强引用还存在,垃圾收集器就永远不会回收被引用的对象。

2.软引用是用来描述一些有用但并非必须的对象。对于软引用关联着的对象,在系统将发生内存溢出异常之前,SoftReference的特点就是它的一个实例保存一个Java对象的软引用,该软引用的存在不妨碍垃圾收集器线程

对该Java对象的回收。

3.弱引用也是用来描述非必需对象的。当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。 WeakReference

4.虚应用和前面的软应用和弱引用不同,它并不影响对象的生命周期。为对象设置需引用的目的是能在这个对象被垃圾收集器回收时收到一个系统通知。虚引用必须和引用队别关联使用。

怎么判断对象已经死了?

引用计数法

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

优缺点:实现简单,判断效率高,大部分情况下是个不错的算法。但是致命问题是没有解决对象之前循环依赖的问题。

可达性分析

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

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

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈JNI(即一般说的native方法)引用的对象

即使在可达性分析法中不可达的对象,也并不是“非死不可”,他们还有拯救自己的机会。要宣告一个对象死亡,至少要经过两次标记过程:如果对象在进行可达性分析之后没有与GC Roots的引用链,那么他将被第一次标记,并且此时需要判断是否有必须要执行finalize()方法。没有必要的话,那么这个对象就宣告死亡,可以回收了。

如果有必要执行,那么这个对象。会被放置在一个叫做F-Queue的队列中,并在稍后由虚拟机自动建立的低优先级的Finalizer线程去执行它。finalize()是对象拯救自己的最后一次机会-只要重新与引用链上的的任何一个对象关联即可(譬如把自己赋值给某个类变量或者对象的成员变量),那么在第二次标记的时候它就会被移除可回收的集合,如果对象还没有逃躲,基本上就真的被回收了。

具体过程如图:

回收方法区(hotSpot中的永久代)

大部分情况下方法区是没有垃圾回收的,在Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾回收,而且在方法区中实现垃圾收集的“性价比”一般比较低。

默认不回收,但,通过 -Xnoclassgc 参数的配置是可以实现方法区的垃圾回收的,适用在大量适用反射、动态代理、动态生成JSP等需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

判断一个常量是否为“废弃常量”,必须要同时满足以下3个条件:

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

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

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

垃圾收集算法

标记-清除算法

同名字一样,算法分为标记和清除两个阶段:首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程就是使用引用计数法或者可达性分析法。之所以说它是最基础的算法,是因为后续的算法都是基于这种思路对其不足进行改进。

它的不足有两个:

  1.效率问题:标记和清除两个过程的效率都不高

  2.空间问题:标记清除之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致在程序运行过程中需要分配较大的对象时,无法找到足够的内存而不得以再次触发一次垃圾回收操作。

复制算法

为了解决效率问题,“复制”算法出现了,它把内存分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块内存中,然后再把已使用过的内存空间再一次清理掉。

优点:回收的时候不需要考虑空间碎步的问题,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:将内存缩小了原来的一半。

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司研究表明,新生代的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一个较大的Eden空间和两块较小的Servior空间,每次使用Eden和其中一个Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%在“被浪费”,赋值到的Survivor空间不够使,需要依赖老年代进行分配担保。

标记-整理算法

老年代一般不能使用复制算法。根据老年代的特点,提出了一种新的“标记-整理”算法,标记过程仍然跟“标记-清除”算法一样,但是后续步骤不能直接对可回收对象进行清理,而是让所有存活的对象都像一端移动,然后直接清理掉边界以外的内存。

HotSpot的算法实现

枚举根节点

在可达性分析中,需要对根节点枚举,枚举根节点时也是必须要停顿的。

在类加载完成的时候,HotSpot就把对象内什么偏移量是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器哪些位置是引用。HotSpot使用一组OopMap的数据结构来达到这个目的。

安全点

在OopMap的帮助下,HotSpot可以快速且准确的完成GC Roots枚举,HotSpot没有为每个指令都生成OopMap,前面已经提到,只是在“特定的位置”记录了这些信息,这些位置称为安全点(Safepoint),即程序在执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。安全点的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增加运行时的负荷。所以安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准选定的,“长时间执行”的最明显的特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等。具备这些功能的指令才会产生安全点。

对于安全点,另外需要考虑的问题是如何在GC发生时让所有的线程都跑到最近的安全点再停顿下来。

  1.抢先式中断:不需要线程的执行代码主动配合,在GC发生时,首先把所有的线程中断,如果发现有线程没有到安全点,就恢复线程,让它执行到安全点上。(现在几乎没有虚拟机采用这种方式)

  2.主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标识,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点时重合的。

安全区域(Safe Region)

当线程处于Sleep或者Block状态时,线程无法响应JVM的中断请求,这种就需要安全区域解决

安全区域是指在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。

当线程执行到安全区域时,首先标识自己已经进入了安全区域,那样,在此期间发生的GC跟这个线程无关。在线程要离开时,需要先检查虚拟机是否已经完成了根节点枚举(或者是整个GC过程)如果完成,就继续执行,否则就一直等到可以执行为止。

垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同的版本的虚拟机所提供的垃圾收集器都可能会有较大的区别。

图片来源于:https://www.jianshu.com/p/bfafd1f9a93a

Serial收集器

Serial收集器是单线程的收集器,它会STW

Serial/Serial Old收集器运行示意图

图片来源于:https://www.jianshu.com/p/bfafd1f9a93a

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本也是作用在新生代,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都跟Serial收集器完全一样。

ParNew收集器是除了Serial收集器外,目前只有它能跟CMS收集器配合工作。CMS收集器是一款真正意义上的并发收集器,CMS收集器第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。CMS收集器是作用在老年代的,在使用-XX:UseMarkSweepGC选项后的默认的新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

ParNew收集器在单CPU的环境绝对没有Serial收集器的效果好,当然随着CPU数目的增加,它对于GC时系统资源的利用还是有很大的好处。他默认开启的GC收集线程数与CPU的数量相同,也可以通过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数。

先解释两个名词:

并行:这里指的是垃圾收集器线程能够并行工作,但此时用户线程仍处于等待状态。

并发:指用户线程和垃圾收集器线程同时执行,(但不一定是并行,有可能是交替执行),用户程序在继续执行,而垃圾收集器线程运行在另一个CPU上。

Parallel Scavenge收集器

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

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程时的停顿时间,而Parallel Scavenge收集器的目标是达成一个可控制的吞吐量。所谓吞吐量就是CPU用于执行用户代码的时间与CPU总消耗时间的比值,即:

 高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数

MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器尽可能的保证内存回收花费的时间不超过设定值。

GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率。

Parallel Scavenge收集器还有一个-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开后不需要手工指定新生代的大小和eden、servivor的比例,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量,这种调节叫做GC自适应调节策略。

-------中间空两行,下面开始说老年代的垃圾收集器-----------

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,是一个单线程收集器,使用“标记-整理”算法。

主要用于Client模式下,在Server模式下,1.给Jdk1.5之前的版本与Parallel Scavenge收集器搭配使用,另一个就是CMS的后备预案。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。1.6之前,新生代如果选择Parallel Scavenge收集器,老年代只能选择 Serial Old(并非直接使用,而是有PS MarkSweep,实现同old类似)。

(这里注意哈,由于Parallel Scavenge是并行的,所以《深入理解Java虚拟机》一书中并没有说这里会暂停用户线程)

CMS收集器 - (并发收集、低停顿)重头戏

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

CMS是基于“标记-清除”算法实现的,它的运作过程可以分为4个步骤:

  • 初始标记 - 需要STW,只是标记一下GC Roots能直接关联到的对象
  • 并发标记 - 进行GC Roots Tracing(遍历跟GC Root有关的对象)的过程
  • 重新标记 - 需要STW,是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记时间长一点
  • 并发清除

从整体上看,CMS收集器的内存回收过程是与用户线程一起并发执行的。但是CMS还达不到完美,它有以下3个明显的缺点:

  • CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量变低。CMS默认启动的回收线程数是(CPU数量+3)/4,它会随着cpu数量的增加而下降,但是当CPU不足4个时,CMS对用户线程的影响可能就变得很大。
  • CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次full GC的产生。由于CMS并发清理阶段用户线程还在运行,伴随着程序运行还是会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能等待下一次清理。这一部分垃圾就叫做浮动垃圾。因此CMS不能像其他收集器那样等到老年代几乎填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活。在JDK1.6中,提升到92%,也可以通过参数:-XX:CMSInitiatingOccupancyFraction的值来设置。要是CMS运行期间预留的内存无法满足程序的需要,就会出现一次“Concurrent Mode Failure”失败,这个时候将启动后备预案:临时启用Serial Old收集器来重新就行老年代的收集。
  • CMS是基于“标记-清除”算法的,收集结束会有大量的空间碎片产生。往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发一次Full GC。为了解决这个问题,CMS提供了-XX:+UseCMSCompactAtFulllCollection开关参数,默认开启,用于在CMS收集器顶不住要进行FullGC时开启内存碎片的整理过程,内存整理过程无法并发,所以停顿时间会变长。还提供了另一个参数:-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的full GC后,跟着来一次带压缩的(默认每次 full GC都进行碎片整理)

--后续单独有一篇文章写G1收集器!

内存分配策略

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

Minor GC(新生代GC)、Marjor GC/Full GC(老年代GC)

了解几个比较常用的jvm配置:

-Xms:初始化堆内存大小

-Xmx:最大堆内存大小

-Xmn:年轻代堆内存大小

-XX:SurvivorRatio=8 :决定了新生代中Eden区与一个Survivor区的空间比例是8:1

几个常见的GC日志:

GC日志开头的GC / FullGC说明这次GC的停顿类型,是否STW

DefNew(Serial收集器的新生代)、ParNew(ParNew收集器的新生代)、PSYoungGen(Parallel Scavenge收集器的新生代)、Tenured(老年代)、Perm(永久代)表示GC发生的区域

2.大对象直接进入老年代

在Serial和ParNew收集器中可以设置-XX:PretenureSizeThreshold参数,令大于这个值的对象直接进入老年代分配。

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

虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并且经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区没经过一次Minor GC,年龄都会+1,当年龄到一定程度(默认为15),将被晋升到老年代。

4.动态对象年龄判定

如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等待MaxTenuringThreshold中设置的年龄。

5.空间分配担保

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

虚拟机性能监控、故障处理工具

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

用于见识虚拟机各种运行状态信息的命令行工具,显示虚拟机进程中的类加载、内存、垃圾回收、及时编译等运行时数据

例如:jstat -gcutil

这里S0的意思是新生代Survivor0,S1的意思是新生代Survivor1,E表示新生代Eden ;O表示老年代Old;P表示永久代Permanent;YGC表示共发生Minor GC(年轻代GC)次数,YGCT表示Minor GC总耗时;FGC表示Full GC的次数,FGCT表示Full GC的时间;

GCT表示所有GC的总耗时

jinfo-Java配置信息工具

作用是实时查看和调整虚拟机各项参数。

jmap-Java内存映像工具(最常用) 

jmap - dump 生成Java堆转储快照。 

jmap - heap 显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况。

jmap - histo 显示堆中对象统计信息包括类、实例数量、合计容量   --另如果执行该命令会触发JVM执行一次FULL GC

jstack-Java堆栈跟踪工具

 jstack用于生成虚拟机当前时刻的线程快照,通常用来定位线程长时间停顿的原因。线程出现停顿时通过jstack查看各个线程的调用堆栈。

jmap和jstack打印的信息我查看的方式一般有两种:

1.通过VisualVM查看

2.通过fastthread.io查看

参考:https://www.jianshu.com/p/bfafd1f9a93a

 
一个入行不久的Java开发,越学习越感觉知识太多,自身了解太少,只能不断追寻
原文地址:https://www.cnblogs.com/fengtingxin/p/13966982.html