Java内存区域怎么划分的?

Java内存区域怎么划分的?

运行时数据区域包含以下五个区域:程序计数器,Java虚拟机栈,本地方法栈,堆,方法区(其中前三个区域各线程私有,相互独立,后面两个区域所有线程共享) 

线程私用的部分(Java虚拟机栈,本地方法栈,程序计数器)

Java虚拟机栈

执行一个Java方法时,虚拟机都会创建一个栈帧,来存储局部变量表,操作数栈等,方法调用完毕后会对栈帧从虚拟机栈中移除。

局部变量表中存储了Java基本类型,对象引用(可以是对象的存储地址,也可以是代表对象的句柄等)和returnAddress类型(存储了一条字节码指令的地址)。

本地方法栈

本地方法栈与Java虚拟机栈类似,只不过是执行Native方法(C++方法等)。

程序计数器

计数器存储了当前线程正在执行的字节码指令的地址(如果是当前执行的是Native方法,那么计数器为空),字节码解释器就是通过改变计数器的值来选取下一条需要执行的字节码指令。程序计数器是线程私有的,便于各个线程切换后,可以恢复到正确的执行位置。

线程共享的部分(堆,方法区)

Java 堆

堆存储了几乎所有对象实例和数组,是被所有线程进行共享的区域。在逻辑上是连续的,在物理上可以是不连续的内存空间(在存储一些类似于数组的这种大对象时,基于简单和性能考虑会使用连续的内存空间)。

方法区

存储了被虚拟机加载的类型信息,常量,静态变量等数据,在JDK8以后,存储在元空间中(以前是存储在堆中的永久代中,JDK8以后已经没有永久代了)。

运行时常量池是方法区的一部分,会存储各种字面量和符号引用。具备动态性,运行时也可以添加新的常量入池(例如调用String的intern()方法时,如果常量池没有相应的字符串,会将它添加到常量池)。

直接内存区(不属于虚拟机运行时数据区)

直接内存区不属于虚拟机运行时数据区的一部分。它指的是使用Native方法直接分配堆外内存,然后通过Java堆中的DirectByteBuffer来对内存的引用进行操作(可以避免Java堆与Native堆之间的数据复制,提升性能)。

Java中对象的创建过程是怎么样的?

这是网上看到的一张流程图:

java对象创建流程

1.类加载检查

首先代码中new关键字在编译后,会生成一条字节码new指令,当虚拟机遇到一条字节码new指令时,会根据类名去方法区运行时常量池找类的符号引用,检查符号引用代表的类是否已加载,解析和初始化过。如果没有就执行相应的类加载过程。

2.分配内存

虚拟机从Java堆中分配一块大小确定的内存(因为类加载时,创建一个此类的实例对象的所需的内存大小就确定了),并且初始化为零值。内存分配的方式有指针碰撞空闲列表两种,取决于虚拟机采用的垃圾回收期是否带有空间压缩整理的功能。

指针碰撞

如果垃圾收集器是Serial,ParNew等带有空间压缩整理的功能时,Java堆是规整的,此时通过移动内存分界点的指针,就可以分配空闲内存。

空闲列表

如果垃圾收集器是CMS这种基于清除算法的收集器时,Java堆中的空闲内存和已使用内存是相互交错的,虚拟机会维护一个列表,记录哪些可用,哪些不可用,分配时从表中找到一块足够大的空闲内存分配给实例对象,并且更新表。

3.对象初始化(虚拟机层面)

虚拟机会对对象进行必要的设置,将对象的一些信息存储在Obeject header 中。

4.对象初始化(Java程序层面)

在构造一个类的实例对象时,遵循的原则是先静后动,先父后子,先变量,后代码块,构造器。在Java程序层面会依次进行以下操作:

  • 初始化父类的静态变量(如果是首次使用此类)

  • 初始化子类的静态变量(如果是首次使用此类)

  • 执行父类的静态代码块(如果是首次使用此类)

  • 执行子类的静态代码块(如果是首次使用此类)

  • 初始化父类的实例变量

  • 初始化子类的实例变量

  • 执行父类的普通代码块

  • 执行子类的普通代码块

  • 执行父类的构造器

  • 执行子类的构造器

PS:如何解决内存分配时的多线程并发竞争问题?

内存分配不是一个线程安全的操作,在多个线程进行内存分配是,可能会存在数据不同步的问题。所以有两种方法解决:

添加CAS锁

对内存分配的操作进行同步处理,添加CAS锁,配上失败重试的方式来保证原子性。(默认使用这种方式)。

预先给各线程分配TLAB

预先在Java堆中给各个线程分配一块TLAB(本地线程缓冲区)内存,每个线程先在各自的缓冲区中分配内存,使用完了再通过第一种添加CAS锁的方式来分配内存。(是否启动取决于-XX:+/-UseTLAB参数)。

Java对象的内存布局是怎么样的?

对象在内存中存储布局主要分为对象头,实例数据和对齐填充三部分。

Serial收集器中,新生代与老年代的内存分配是1:2,然后新生代分为Eden区,From区,To区,比例是8:1:1。

新生代

分为Eden,From Survivor,To Survivor,8:1:1

Eden用来分配新对象,满了时会触发Minor GC。

From Survivor是上次Minor GC后存活的对象。

To Survivor是用于下次Minor GC时存放存活的对象。

老年代

用于存放存活时间比较长的对象,大的对象,当容量满时会触发Major GC(Full GC)

内存分配策略:

  1. 对象优先在Eden分配

当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。

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

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组,为了避免大对象在Eden和两个Survivor区之间进行来回复制,所以当对象超过-XX:+PrintTenuringDistribution参数设置的大小时,直接从老年代分配

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

当对象在新生代中经历过一定次数(XX:MaxTenuringThreshold参数设置的次数,默认为15)的Minor GC后,就会被晋升到老年代中。

  1. 动态对象年龄判定。

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

MinorGC和FullGC是什么?

Minor GC:对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。

Full GC:也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满和System.gc()被显式调用等。

触发Minor GC的条件有哪些?

1.为新对象分配内存时,新生代的Eden区空间不足。 新生代回收日志:

2020-05-12T16:15:10.736+0800: 7.803: [GC (Allocation Failure) 2020-05-12T16:15:10.736+0800: 7.803: [ParNew: 838912K->22016K(943744K), 0.0982676 secs] 838912K->22016K(1992320K), 0.0983280 secs] [Times: user=0.19 sys=0.01, real=0.10 secs]

触发Full GC的条件有哪些?

主要分为三种:

1.system.gc()

代码中调用system.gc()方法,建议JVM进行垃圾回收。

2.方法区空间不足

方法区中存放的是一些类的信息,当系统中要加载的类、反射的类和调用的方法较多时,方法区可能会被占满,触发 Full GC

3.老年代空间不足

而老年代空间不足又有很多种情况: 3.1 Promotion Failed 老年代存放不下晋升对象 在进行 MinorGC 时, Survivor Space 放不下存活的对象,此时会让这些对象晋升,只能将它们放入老年代,而此时老年代也放不下时造成的。 还有一些情况也会导致新生代对象晋升,例如存活对象经历的垃圾回收次数超过一定次数(XX:MaxTenuringThreshold参数设置的次数,默认为15),那么会导致晋升, 或者在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 3.2 Concurrent Mode Failure 在执行 CMS GC 的过程中,同时有对象要放入老年代,而此时老年代空间不足造成的。 3.3 历次晋升的对象平均大小>老年代的剩余空间 这是一个较为复杂的触发情况, HotSpot为了避免由于新生代对象晋升到老年代导致老年代空间不足的现象, 在进行 Minor GC时,做了一个判断,如果之前统计所得到的 MinorGC 晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发 Full GC。 3.4 老年代空间不足以为大对象分配内存 因为超过阀值(-XX:+PrintTenuringDistribution参数设置的大小时)的大对象,会直接分配到老年代,如果老年代空间不足,会触发Full GC。

垃圾收集器有哪些?

一般老年代使用的就是标记-整理,或者标记-清除+标记-整理结合(例如CMS)

新生代就是标记-复制算法

垃圾收集器特点算法适用内存区域
Serial单个GC线程进行垃圾回收,简单高效标记-复制新生代
Serial Old单个GC线程进行垃圾回收标记-整理老年代
ParNew是Serial的改进版,就是可以多个GC线程一起进行垃圾回收标记-复制新生代
Parallel Scanvenge收集器(吞吐量优先收集器)高吞吐量,吞吐量=执行用户线程的时间/CPU执行总时间标记-复制新生代
Parallel Old收集器支持多线程收集标记-整理老年代
CMS收集器(并发低停顿收集器)低停顿标记-清除+标记-整理老年代
G1收集器低停顿,高吞吐量标记-复制算法老年代,新生代

Serial收集器(标记-复制算法)

就是最简单的垃圾收集器,也是目前 JVM 在 Client 模式默认的垃圾收集器,在进行垃圾收集时会停止用户线程,然后使用一个收集线程进行垃圾收集。主要用于新生代,使用标记-复制算法。

优点是简单高效(与其他收集器的单线程比),内存占用小,因为垃圾回收时就暂停所有用户线程,然后使用一个单线程进行垃圾回收,不用进行线程切换。

缺点是收集时必须停止其他用户线程。

Serial Old收集器(标记-整理算法)

跟Serial收集器一样,不过是应用于老年代,使用标记-整理算法。

ParNew收集器(标记-复制算法)

ParNew收集器是Serial收集器的多线程并行版本,在进行垃圾收集时可以使用多个线程进行垃圾收集。

与Serial收集器主要区别就是支持多线程收集,ParNew收集器应用广泛(JDK9以前,服务端模式垃圾收集组合官方推荐的是ParNew+CMS),因为只有Serial和ParNew才能配合CMS收集器(应用于老年代的并发收集器)一起工作。

image-20200228185412716.pngimage-20200228185412716

Parallel Scanvenge收集器(吞吐量优先收集器)

也支持多线程收集,它的目标是达到一个可控制的吞吐量,就是运行用户代码的时间/CPU消耗的总时间的比值。高吞吐量可以最高效率地利用处理器资源,尽快完成程序运算任务,适合不需要太多的交互分析任务。不支持并发收集,进行垃圾回收时会暂停用户线程,使用多个垃圾回收线程进行垃圾回收。

Parallel Old收集器

是Parallel Scanvenge老年代版本,支持多线程收集,使用标记整理法实现的,

  

G1对象分配策略

说起对象的分配,我们不得不谈谈对象的分配策略。它分为4个阶段:

  1. 栈上分配
  2. TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区
  3. 共享Eden区中分配
  4. Humongous区分配(超过Region大小50%的对象)

对象在分配之前会做逃逸分析,如果该对象只会被本线程使用,那么就将该对象在栈上分配。这样对象可以在函数调用后销毁,减轻堆的压力,避免不必要的gc。 如果对象在栈是上分配不成功,就会使用TLAB来分配。TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。

对TLAB空间中无法分配的对象,JVM会尝试在共享Eden空间中进行分配。如果是大对象,则直接在Humongous区分配。

最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。

G1 Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

Region如何解决跨代指针?

因为老年代old区也会存在对新生代Eden区的引用,如果只是为了收集Eden区而对整个老年代进行扫描,那样开销太大了,所以G1其实会将每个Region分为很多个区,每个区有一个下标,当这个区有对象被其他Region引用时,那么CardTable对应下标下值为0,然后使用一个Rset来存储其他Region对当前Region的引用,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。对跨代引用的扫描,只需要扫描RSet就行了。

在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

但在G1中,并没有使用point-out(就是记录当前Region对其他Region中对象的引用),这是由于一个Region太小,Region数量太多,如果是用point-out的话,如果需要计算一个Region的可回收的对象数量,需要把所有Region都是扫描一遍会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些Region引用了当前Region中的对象。这样只需要将当前Region中这些对象当做初始标记时的根对象来扫描就可以扫描出因为有跨代引用需要存活的对象,避免了无效的扫描。

由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代的Region对新生代的这个Region之间的引用即可。

需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。CardTable通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未引用。当一个地址空间有引用时,这个地址空间对应的数组索引的值被标记为"0",即标记为脏引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table集合(每个线程对应一个Hash Table,主要是为了减少多线程并发更新RSet的竞争),每个哈希表的Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

如果Rset是记录每个外来Region对当前Region中对象的引用,这样数量就太多了,所以Card Table只是有很多Byte字节,每个字节记录了Region对应的一个内存区域(卡页)是否是dirty的,为1代表dirty,也就是有其他Region对这个卡页中的对象进行引用。

 

  • 阶段1:根扫描 表态和本地对象被扫描
  • 阶段2:更新RS 处理dirty card队列更新RS
  • 阶段3:处理RS 检测从新生代指向老年代的对象
  • 阶段4:对象拷贝 拷贝存活的对象到survivor/old区域
  • 阶段5:处理引用队列 软引用、弱引用、虚引用处理

G1 MixGC

MixGC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。Young GC回收是把新生代活着的对象都拷贝到Survivor的特定区域(Survivor to),剩下的Eden和Survivor from就可以全部回收清理了。那么,mixed GC就是把一部分老年区的region加到Eden和Survivor from的后面,合起来称为collection set, 就是将被回收的集合,下次mixed GC evacuation把他们所有都一并清理。选old region的顺序是垃圾多的(存活对象少)优先。

它的GC步骤分2步:

  1. 全局并发标记(global concurrent marking)
  2. 拷贝存活对象(evacuation)

在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?

在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

  • 初始标记(initial mark,STW) 在此阶段,G1 GC 对根进行标记。该阶段与常规的(STW) 新生代垃圾回收密切相关。
  • 根区域扫描(root region scan) G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 新生代垃圾回收。
  • 并发标记(Concurrent Marking) G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 新生代垃圾回收中断
  • 最终标记(Remark,STW) 该阶段是 STW 回收,帮助完成标记周期。G1 GC清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
  • 清除垃圾(Cleanup,STW) 在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

收集步骤:

image-20200302181639409.pngimage-20200302181639409

初始标记 只标记GC Roots直接引用的对象

并发标记 从GC Roots开始对堆中对象进行可达性分析,扫描整个对象图,找出要回收的对象。(可以与用户线程并发执行)

最终标记 对并发标记阶段,由于用户线程执行造成的改动进行修正,使用原始快照方法。

筛选回收 对Region进行排序,根据回收价值,选择任意多个Region构成回收集,将存活对象复制到空的Region中去,因为涉及到存活对象的移动,所以是暂停用户线程的。

垃圾收集器相关的参数

-XX:+UseSerialGC, 虚拟机运行在Client 模式下的默认值,打开此开关后,使用Serial + Serial Old 的收集器组合进行内存回收

-XX:+UseConcMarkSweepGC,打开此开关后,使用ParNew + CMS + Serial Old 的收集器组合进行内存回收。Serial Old 收集器将作为CMS 收集器出现Concurrent Mode Failure失败后的后备收集器使用。(我们的线上服务用的都是这个)

-XX:+UseParallelGC,虚拟机运行在Server 模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器组合进行内存回收。

-XX:+UseParallelOldGC,打开此开关后,使用Parallel Scavenge + Parallel Old 的收集器组合进行内存回收。

-XX:+UseG1GC,打开此开关后,采用 Garbage First (G1) 收集器

-XX:+UseParNewGC,在JDK1.8被废弃,在JDK1.7还可以使用。打开此开关后,使用ParNew + Serial Old 的收集器组合进行内存回收

目前通常使用的是什么垃圾收集器?

怎么查询当前JVM使用的垃圾收集器?

使用这个命令可以查询当前使用的垃圾收集器 java -XX:+PrintCommandLineFlags -version,

另外这个命令可以查询到更加详细的信息

java -XX:+PrintFlagsFinal -version | grep GC

我们在IDEA中启动的一个Springboot的项目,默认使用的垃圾收集器参数是 -XX:+UseParallelGC

-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
java version "1.8.0_73"
Java(TM) SE Runtime Environment (build 1.8.0_73-b02)
Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode)

Parallel Scavenge+Serial Old

JDK8默认情况下服务端模式下JVM垃圾回收参数是-XX:+UseParallelGC参数,也就是会使用Parallel Scavenge+Serial Old的收集器组合,进行内存回收。

ParNew+CMS

但是一般如果我们的后端应用不是那种需要进行大量计算的应用,基于低延迟的考虑,可以考虑使用-XX:+UseConcMarkSweepGC进行垃圾收集,这种配置下会使用ParNew来收集新生代内存,CMS垃圾回收器收集老年代内存。

G1

在JDK9时,默认的垃圾收集器是G1收集器,也可以使用-XX:+UseG1GC参数来启动G1垃圾收集器。

 

容器的内存和 jvm 的内存有什么关系?参数怎么配置?

在JDK8以后,JVM增加了容器感知功能,就是如果不显示指定-Xmx2048m 最大堆内存大小, -Xms2048m最小堆内存大小,会取容器所在的物理机的内存的25%作为最大堆内存大小, 也可以通过这几个参数来设置堆内存占容器内存的比例 -XX:MinRAMPercentage -XX:MaxRAMPercentage -XX:InitialRAMPercentage

如何大体估算java进程使用的内存呢?

Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss]

-Xss128k:设置每个线程的堆栈大小.JDK5.0以后每个线程的栈大小为1M。

-Xms 堆内存的初始大小,默认为物理内存的1/64 -Xmx 堆内存的最大大小,默认为物理内存的1/4 -Xmn 堆内新生代的大小。通过这个值也可以得到老生代的大小:-Xmx减去-Xmn

-Xss 设置每个线程可使用的内存大小,即栈的大小。在相同物理内存下,减小这个值能生成更多的线程,当然操作系统对一个进程内的线程数还是有限制的,不能无限生成。线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大,如果该值设置过大,就有影响到创建栈的数量,如果是多线程的应用,就会出现内存溢出的错误。

怎么获取 dump 文件?怎么分析?

  1. 启动时配置,出现OOM问题时自动生成 JVM启动时增加两个参数:
//出现 OOME 时生成堆 dump: 
-XX:+HeapDumpOnOutOfMemoryError
//生成堆文件地址:
-XX:HeapDumpPath=/home/liuke/jvmlogs/

2.执行jmap命令立即生成 发现程序异常前通过执行指令,直接生成当前JVM的dmp文件,6214是指JVM的进程号

jmap -dump:format=b,file=/home/admin/logs/heap.hprof 6214

获得heap.hprof以后,执行jvisualvm命令打开使用Java自带的工具Java VisualVM来打开heap.hprof文件,就可以分析你的Java线程里面对象占用堆内存的情况了

由于第一种方式是一种事后方式,需要等待当前JVM出现问题后才能生成dmp文件,实时性不高,第二种方式在执行时,JVM是暂停服务的,所以对线上的运行会产生影响。所以建议第一种方式。

gc日志怎么看?

这是一条Minor GC的回收日志

2020-05-07T16:28:02.845+0800: 78210.469: [GC (Allocation Failure) 2020-05-07T16:28:02.845+0800: 78210.469: [ParNew: 68553K->466K(76672K), 0.0221963 secs] 131148K->63062K(2088640K), 0.0223082 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 

GC (Allocation Failure)

代表Eden区分配内存失败触发Minor GC。 2020-05-07T16:28:02.845+0800: 78210.469

是发生的时间。 68553K->466K(76672K)

代表垃圾回收前新生代使用内存是68MB,剩余0.4MB,总内存是76MB。 0.0221963 secs

是垃圾回收耗时。 131148K->63062K(2088640K)

代表堆区回收前使用131MB,63MB,总内存是2088MB。

[Times: user=0.02 sys=0.00, real=0.02 secs]

用户态耗时0.02s,内核态耗时0s,总耗时0.02s

cpu 使用率特别高,怎么排查?通用方法?定位代码?cpu高的原因?

CPU飙高,频繁GC,怎么排查?

jstack命令:教你如何排查多线程问题

 jstat -gcutil 29530 1000 10
 垃圾回收信息统计,29530是pid,1000是每1秒打印一次最新信息,10是最多打印10次

怎么排查CPU占用率过高的问题?

1.首先使用top命令查看CPU占用率高的进程的pid。

top - 15:10:32 up 523 days,  3:47,  1 user,  load average: 0.00, 0.01, 0.05
Tasks:  95 total,   1 running,  94 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.7 us,  0.5 sy,  0.0 ni, 95.7 id,  2.2 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16267904 total,  6940648 free,  2025316 used,  7301940 buff/cache
KiB Swap: 16777212 total, 16776604 free,      608 used. 13312484 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
14103 hadoop    20   0 2832812 203724  18392 S   3.7  1.3 977:08.04 java
14010 hadoop    20   0 2897344 285392  18660 S   0.3  1.8 513:30.49 java
14284 hadoop    20   0 3052556 340436  18636 S   0.3  2.1   1584:47 java
14393 hadoop    20   0 2912460 504112  18632 S   0.3  3.1 506:43.68 java
    1 root      20   0  190676   3404   2084 S   0.0  0.0   4:31.47 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:04.77 kthreadd
    3 root      20   0       0      0      0 S   0.0  0.0   0:10.16 ksoftirqd/0

2.使用top -Hp 进程id获得该进程下各个线程的CPU占用情况,找到占用率最高的线程的pid2, 使用printf "%x\n" pid2命令将pid2转换为16进制的数number。

top - 15:11:01 up 523 days,  3:48,  1 user,  load average: 0.00, 0.01, 0.05
Threads:  69 total,   0 running,  69 sleeping,   0 stopped,   0 zombie
%Cpu(s): 12.8 us,  0.1 sy,  0.0 ni, 87.0 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16267904 total,  6941352 free,  2024612 used,  7301940 buff/cache
KiB Swap: 16777212 total, 16776604 free,      608 used. 13313188 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
14393 hadoop    20   0 2912460 504112  18632 S  0.0  3.1   0:00.01 java
14411 hadoop    20   0 2912460 504112  18632 S  0.0  3.1   0:01.95 java
14412 hadoop    20   0 2912460 504112  18632 S  0.0  3.1   0:16.18 java
14413 hadoop    20   0 2912460 504112  18632 S  0.0  3.1   0:12.79 java
14414 hadoop    20   0 2912460 504112  18632 S  0.0  3.1   8:09.10 java

3.使用jstack pid获得进程下各线程的堆栈信息,nid=0xnumber的线程即为占用率高的线程,查看它是在执行什么操作。(jstack 5521 | grep -20 0x1596可以获得堆栈信息中,会打印匹配到0x1596的上下20行的信息。)

例如这个线程是在执行垃圾回收:

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f338c01f000 nid=0x1593 runnable

JVM相关的异常

1.stackoverflow

这种就是栈的空间不足,就会抛出这个异常,一般是递归执行一个方法时,执行方法深度太深时出现。Java执行一个方法时,会创建一个栈帧来存放局部变量表,操作数栈,如果分配栈帧时,栈空间不足,那么就会抛出这个异常。

(栈空间可以设置-Xss参数实现,默认为1M,如果参数)

双亲委派机制是什么?

 

就是类加载器一共有三种:

启动类加载器:主要是在加载JAVA_HOME/lib目录下的特定名称jar包,例如rt.jar包,像java.lang就在这个jar包中。

扩展加载器:主要是加载JAVA_HOME/lib/ext目录下的具备通用性的类库。

应用程序加载器:加载用户类路径下所有的类库,也就是程序中默认的类加载器。

工作流程:

除启动类加载器以外,所有类加载器都有自己的父类加载器,类加载器收到一个类加载请求时,首先会判断类是否已经加载过了,没有的话会调用父类加载器的的loadClass方法,将请求委派为父加载器,当父类加载器无法完成类加载请求时,子加载器才尝试去加载这个类。 目的是为了保证每个类只加载一次,并且是由特定的类加载器进行加载(都是首先让启动类来进行加载)。

public abstract class ClassLoader {
    ...
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                ...
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    ...
                    c = findClass(name);
                    // do some stats
                    ...
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    ...
}

怎么自定义一个类加载器?

加载一个类时,一般是调用类加载器的loadClass()方法来加载一个类,loadClass()方法的工作流程如下:

1.先调用findLoadedClass(className)来获取这个类,判断类是否已加载。

2.如果未加载,如果父类加载器不为空,调用父类加载器的loadClass()来加载这个类,父类加载器为空,就调用父类加载器加载这个类。

3.父类加载器加载失败,那么调用该类加载器findClass(className)方法来加载这个类。

所以我们我们一般自定义类加载器都是继承ClassLoader,来重新findClass()方法,来实现类加载。

public class DelegationClassLoader extends ClassLoader {
  private String classpath;

  public DelegationClassLoader(String classpath, ClassLoader parent) {
    super(parent);
    this.classpath = classpath;
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    InputStream is = null;
    try {
      String classFilePath = this.classpath + name.replace(".", "/") + ".class";
      is = new FileInputStream(classFilePath);
      byte[] buf = new byte[is.available()];
      is.read(buf);
      return defineClass(name, buf, 0, buf.length);
    } catch (IOException e) {
      throw new ClassNotFoundException(name);
    } finally {
      if (is != null) {
        try {
          is.close();
        } catch (IOException e) {
          throw new IOError(e);
        }
      }
    }
  }

  public static void main(String[] args)
      throws ClassNotFoundException, IllegalAccessException, InstantiationException,
      MalformedURLException {
    sun.applet.Main main1 = new sun.applet.Main();

    DelegationClassLoader cl = new DelegationClassLoader("java-study/target/classes/",
        getSystemClassLoader());
    String name = "sun.applet.Main";
    Class<?> clz = cl.loadClass(name);
    Object main2 = clz.newInstance();

    System.out.println("main1 class: " + main1.getClass());
    System.out.println("main2 class: " + main2.getClass());
    System.out.println("main1 classloader: " + main1.getClass().getClassLoader());
    System.out.println("main2 classloader: " + main2.getClass().getClassLoader());
    ClassLoader itrCl = cl;
    while (itrCl != null) {
      System.out.println(itrCl);
      itrCl = itrCl.getParent();
    }
  }
}

类加载的过程是什么样的?

类加载器

类加载器是 Java 运行时环境(Java Runtime Environment)的一部分,负责动态加载 Java 类到 Java 虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。 由于有了类加载器,Java 运行时系统不需要知道文件与文件系统。每个 Java 类必须由某个类加载器装入到内存。

 

类装载器除了要定位和导入二进制 class 文件外,还必须负责验证被导入类的正确性,为变量分配初始化内存,以及帮助解析符号引用。这些动作必须严格按一下顺序完成:

  1. 装载:查找并装载类型的二进制数据。
  2. 链接:执行验证、准备以及解析(可选) - -验证:确保被导入类型的正确性 -准备:为类变量分配内存,并将其初始化为默认值。
  • 解析:把类型中的符号引用转换为直接引用。
  1. 初始化:把类变量初始化为正确的初始值。

装载

类加载器分类

在Java虚拟机中存在多个类装载器,Java应用程序可以使用两种类装载器:

  • Bootstrap ClassLoader:此装载器是 Java 虚拟机实现的一部分。由原生代码(如C语言)编写,不继承自 java.lang.ClassLoader 。负责加载核心 Java 库,启动类装载器通常使用某种默认的方式从本地磁盘中加载类,包括 Java API。
  • Extention Classloader:用来在<JAVA_HOME>/jre/lib/ext ,或 java.ext.dirs 中指明的目录中加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。
  • Application Classloader:根据 Java应用程序的类路径( java.class.path 或 CLASSPATH 环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。
  • 自定义类加载器:可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求而不需要完全了解 Java 虚拟机的类加载的细节。

全盘负责双亲委托机制

在一个 JVM 系统中,至少有 3 种类加载器,那么这些类加载器如何配合工作?在 JVM 种类加载器通过 全盘负责双亲委托机制 来协调类加载器。

  • 全盘负责:指当一个 ClassLoader 装载一个类的时,除非显式地使用另一个 ClassLoader ,该类所依赖及引用的类也由这个 ClassLoader 载入。
  • 双亲委托机制:指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。

全盘负责双亲委托机制只是 Java 推荐的机制,并不是强制的机制。实现自己的类加载器时,如果想保持双亲委派模型,就应该重写 findClass(name) 方法;如果想破坏双亲委派模型,可以重写 loadClass(name) 方法。

装载入口

所有Java虚拟机实现必须在每个类或接口首次主动使用时初始化。以下六种情况符合主动使用的要求:

  • 当创建某个类的新实例时(new、反射、克隆、序列化)
  • 调用某个类的静态方法
  • 使用某个类或接口的静态字段,或对该字段赋值(用final修饰的静态字段除外,它被初始化为一个编译时常量表达式)
  • 当调用Java API的某些反射方法时。
  • 初始化某个类的子类时。
  • 当虚拟机启动时被标明为启动类的类。

除以上六种情况,所有其他使用Java类型的方式都是被动的,它们不会导致Java类型的初始化。 当类是被动引用时,不会触发初始化:

1.通过子类去调用父类的静态变量,不会触发子类的初始化,只会触发包含这个静态变量的类初始化,例如执行这样的代码SubClass.fatherStaticValue只会触发FatherClass的初始化,不会触发SubClass的初始化,因为fatherStaticValue是FatherClass的变量

2.通过数组定义类引用类,SuperClass[] array = new SuperClass[10];

不会触发SuperClass类的初始化,但是执行字节码指令newarray会触发另外一个类[Lorg.fenixsoft.classloading.SuperClass的初始化,这个类继承于Object类,是一个包装类,里面包含了访问数组的所有方法,

3.只引用类的常量不会触发初始化,因为常量在编译阶段进入常量池

class SuperClass {
		public static final String str = "hello";
}

//引用常量编译时会直接存入常量池
System.out.println(SuperClass.str);

对于接口来说,只有在某个接口声明的非常量字段被使用时,该接口才会初始化,而不会因为事先这个接口的子接口或类要初始化而被初始化。

父类需要在子类初始化之前被初始化。当实现了接口的类被初始化的时候,不需要初始化父接口。然而,当实现了父接口的子类(或者是扩展了父接口的子接口)被装载时,父接口也要被装载。(只是被装载,没有初始化)

验证

确认装载后的类型符合Java语言的语义,并且不会危及虚拟机的完整性。

  • 装载时验证:检查二进制数据以确保数据全部是预期格式、确保除 Object 之外的每个类都有父类、确保该类的所有父类都已经被装载。
  • 正式验证阶段:检查 final 类不能有子类、确保 final 方法不被覆盖、确保在类型和超类型之间没有不兼容的方法声明(比如拥有两个名字相同的方法,参数在数量、顺序、类型上都相同,但返回类型不同)。
  • 符号引用的验证:当虚拟机搜寻一个被符号引用的元素(类型、字段或方法)时,必须首先确认该元素存在。如果虚拟机发现元素存在,则必须进一步检查引用类型有访问该元素的权限。

准备

在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。但在到到初始化阶段之前,类变量都没有被初始化为真正的初始值。

类型默认值
int0
long0L
short(short)0
char’\u0000’
byte(byte)0
blooeanfalse
float0.0f
double0.0d
referencenull

解析的过程就是在类型的常量池总寻找类、接口、字段和方法的符号引用,把这些符号引用替换为直接引用的过程

  • 类或接口的解析:判断所要转化成的直接引用是数组类型,还是普通的对象类型的引用,从而进行不同的解析。
  • 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,

初始化

所有的类变量(即静态量)初始化语句和类型的静态初始化器都被Java编译器收集在一起,放到一个特殊的方法中,这个步骤就是初始化类静态变量和执行静态代码块。 对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化方法。在类和接口的 class 文件中,这个方法被称为<clinit>

  1. 如果存在直接父类,且直接父类没有被初始化,先初始化直接父类。
  2. 如果类存在一个类初始化方法,执行此方法。

这个步骤是递归执行的,即第一个初始化的类一定是Object

Java虚拟机必须确保初始化过程被正确地同步。 如果多个线程需要初始化一个类,仅仅允许一个线程来进行初始化,其他线程需等待。

这个特性可以用来写单例模式。

Clinit 方法

  • 对于静态变量和静态初始化语句来说:执行的顺序和它们在类或接口中出现的顺序有关。
  • 并非所有的类都需要在它们的class文件中拥有()方法, 如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有<clinit>()方法。如果类声明了类变量,但没有明确的使用类变量初始化语句或者静态代码块来初始化它们,也不会有<clinit>()方法。如果类仅包含静态final常量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有<clinit>()方法。只有那些需要执行Java代码来赋值的类才会有()
  • final常量:Java虚拟机在使用它们的任何类的常量池或字节码中直接存放的是它们表示的常量值。

JVM调优有哪些工具?

jstat

jstat可以打印出当前JVM运行的各种状态信息,例如新生代内存使用情况,老年代内存使用情况,Minor GC发生总次数,总耗时,Full GC发生总次数,总耗时。

//5828是java进程id,1000是打印间隔,每1000毫秒打印一次,100是总共打印100次
jstat -gc 5828 1000 100

打印结果如下:

各个参数的含义如下:

S0C 新生代中第一个survivor(幸存区)的总容量 (字节)

S1C 新生代中第二个survivor(幸存区)的总容量 (字节)

S0U 新生代中第一个survivor(幸存区)目前已使用空间 (字节)

S1U 新生代中第二个survivor(幸存区)目前已使用空间 (字节)

EC 新生代中Eden(伊甸园)的总容量 (字节)

EU 新生代中Eden(伊甸园)目前已使用空间 (字节)

OC 老年代的总容量 (字节)

OU 老年代代目前已使用空间 (字节)

YGC 目前新生代垃圾回收总次数

YGCT 目前新生代垃圾回收总消耗时间

FGC 目前full gc次数总次数

FGCT 目前full gc次数总耗时,单位是秒

GCT 垃圾回收总耗时

一般还可以使用jstat -gcutil <pid>:统计gc信息,这样打印出来的结果是百分比,而不是实际使用的空间,例如jstat -gcutil 1 1000 100

例如,S0代表 新生代中第一个survivor区的空间使用了73.19%,E代表新生代Eden区使用了51%,O代表老年代食堂了98%

 

参数描述
S0年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
s1年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
E年轻代中Eden已使用的占当前容量百分比
Oold代已使用的占当前容量百分比
M元空间(MetaspaceSize)已使用的占当前容量百分比
CCS压缩使用比例
YGC年轻代垃圾回收次数
FGC老年代垃圾回收次数
FGCT老年代垃圾回收消耗时间
GCT垃圾回收消耗总时间

 

jstack

jstack可以生成当前JVM的线程快照,也就是当前每个线程当前的状态及正在执行的方法,锁相关的信息。jstack -l 进程id ,-l代表除了堆栈信息外,还会打印锁的附加信息。jstack还会检测出死锁信息。一般可以用于定位线程长时间停顿,线程间死锁等问题。

例如在下面的例子中,第一个线程获取到lock1,再去获取lock2,第二个线程先获取到lock2,然后再去获取lock1。每个线程都只获得了一个锁,同时在获取另外一个锁,就会进入死锁状态。

public static void main(String[] args) {
        final Integer lock1 = new Integer(1);
        final String  lock2 = new String();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                synchronized (lock1) {
                    System.out.println("线程1获得了lock1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程1休眠结束");
                    System.out.println("线程1开始尝试获取lock2");
                    synchronized (lock2) {
                        System.out.println("线程1获得了lock2");
                    }
                }
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                synchronized (lock2) {
                    System.out.println("线程2获得了lock2");
                    System.out.println("线程2开始尝试获取lock1");
                    synchronized (lock1) {
                        System.out.println("线程2获得了lock2");
                    }
                }
            }
        });
    }

使用jstack -l 进程id就可以打印出当前的线程信息

 

以及各个线程的状态,执行的方法(pool-1-thread-1和pool-1-thread-2分别代表线程池的第一个线程和第二个线程):

 

jmap

一般可以生成当前堆栈快照。使用 jmap -heap可以打印出当前各个分区的内存使用情况,使用jmap -dump:format=b,file=dump.hprof 进程id可以生成当前的堆栈快照,堆快照和对象统计信息,对生成的堆快照进行分析,可以分析堆中对象所占用内存的情况,检查大对象等。执行jvisualvm命令打开使用Java自带的工具Java VisualVM来打开堆栈快照文件,进行分析。可以用于排查内存溢出,内存泄露问题。

也可以配置启动时的JVM参数,让发送内存溢出时,自动生成堆栈快照文件。

//出现 OOM 时生成堆 dump: 
-XX:+HeapDumpOnOutOfMemoryError
//生成堆文件地址:
-XX:HeapDumpPath=/home/liuke/jvmlogs/

查看内存使用情况

 

jmap -histo打印出当前堆中的对象统计信息,包括类名,每个类的实例数量,总占用内存大小。

instances列:表示当前类有多少个实例。
bytes列:说明当前类的实例总共占用了多少个字节
class name列:表示的就是当前类的名称,class name 对于基本数据类型,使用的是缩写。解读:B代表byte ,C代表char ,D代表double, F代表float,I代表int,J代表long,Z代表boolean 
前边有[代表数组,[I 就相当于int[] 
对象数组用`[L+类名`表示 

 

使用jmap -dump:format=b,file=/存放路径/heapdump.hprof 进程id就可以得到堆转储文件,然后执行jvisualvm命令就可以打开JDK自带的jvisualvm软件。

例如在这个例子中会造成OOM问题,通过生成heapdump.hprof文件,可以使用jvisualvm查看造成OOM问题的具体代码位置。

public class Test018 {

    ArrayList<TestObject> arrayList = new ArrayList<TestObject>();

    public static void main(String[] args) {
        Test018 test018 =new Test018();
        Random random = new Random();
        for (int i = 0; i < 10000000; i++) {
            TestObject testObject = new TestObject();
            test018.arrayList.add(testObject);
        }
    }
    private static class TestObject {
        public byte[] placeholder = new byte[64 * 1024];//每个变量是64k
    }
}

-Xms20m -Xmx20m -verbose:gc -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/存放路径/heapdump.hprof

造成OOM问题的代码位置:

 

然后在主页点击Histogram,进入Histogram页面可以看到对象列表,with incomming references 也就是可以查看所有对这个对象的引用(思路一般优先看占用内存最大对象;其次看数量最多的对象。)。我们这个例子中主要是byte[]数组分配了占用了大量的内存空间,而byte[]主要来自于Test018类的静态变量arrayList的每个TestObject类型的元素的placeholder属性。

 

同时可以点击 内存快照对比 功能对两个dump文件进行对比,判断两个dump文件生成间隔期间,各个对象的数量变化,以此来判断内存泄露问题。 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值