JVM内存介绍

JVM内存介绍

计算机内存

​ 我们知道,计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,用户磁盘和CPU的交互,而CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存,用户缓冲用户IO等待导致CPU的等待成本,但是随着CPU的发展,内存的读写速度也远远跟不上CPU的读写速度,因此,为了解决这一纠纷,CPU厂商在每颗CPU上加入了高速缓存,用来缓解这种症状,因此,现在CPU同内存交互就变成了下面的样子。

​ 基于高速缓存的存储交互很好的解决了处理器与内存之间的矛盾,也引入了新的问题:缓存一致性问题。在多处理器系统中,每个处理器有自己的高速缓存,而他们又共享同一块内存(下文成主存,main memory 主要内存),当多个处理器运算都涉及到同一块内存区域的时候,就有可能发生缓存不一致的现象。为了解决这一问题,需要各个处理器运行时都遵循一些协议,在运行时需要将这些协议保证数据的一致性。这类协议包括MSI、MESI、MOSI、Synapse、Firely、DragonProtocol等。

​ 如下图所示

JAVA虚拟机内存

​ Java虚拟机内存模型中定义的访问操作与物理计算机处理的基本一致!

​ Java中通过多线程机制使得多个任务同时执行处理,所有的线程共享JVMmain memory+JVM—JVM

在之前,我们也已经提到,JVM的逻辑内存模型如下:

img

​ 我们现在来逐个的看下每个到底是做什么的!

GC作用区域:方法区、堆。

  1. 程序计数器

    ​ 内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成

    ​ 如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

  2. Java 虚拟机栈

    ​ 线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

    ​ 局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

    ​ StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。 ​ OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

  3. 本地方法栈

    ​ 区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。

  4. Java 堆

    ​ 对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java 虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配①,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换②优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。

    ​ Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”(Garbage Collected Heap,幸好国内没翻译成“垃圾堆”)。如果从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java 堆中还可以细分为:新生代和老年代;再细致一点的有Eden 空间、From Survivor 空间、To Survivor 空间等。如果从内存分配的角度看,线程共享的Java 堆中可能划分出多个线程私有的分配缓冲区(Thread LocalAllocation Buffer,TLAB)。不过,无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。在本章中,我们仅仅针对内存区域的作用进行讨论,Java 堆中的上述各个区域的分配和回收等细节将会是下一章的主题。

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

  5. 方法区(1.8后该区域被废弃)

    ​ 方法区(Method Area)与Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java 堆区分开来。对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。即使是HotSpot 虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。

    ​ Java 虚拟机规范对这个区域的限制非常宽松,除了和Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun 公司的BUG 列表中,曾出现过的若干个严重的BUG 就是由于低版本的HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。

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

    ​ 而java1.8为了解决这个问题,就提出了meta space(元空间)的概念,就是为了解决永久代内存溢出的情况,一般来说,在不指定 meta space大小的情况下,虚拟机方法区内存大小就是宿主主机的内存大小。

  6. 运行时常量池

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

    ​ Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中①。

    ​ 运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。

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

  7. 直接内存

    ​ 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

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

    ​ 显然,本机直接内存的分配不会受到Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括RAM 及SWAP 区或者分页文件)的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

Heap与Stack的区别

  1. Heap是 Stack的一个子集。

  2. Stack存取速度仅次于寄存器,存储效率比heap高,可共享存储数据,但是其中数据的大小和生存期必须在运行前确定。

  3. Heap是运行时可动态分配的数据区,从速度看比Stack慢,Heap里面的数据不共享,大小和生存期都可以在运行时再确定。

  4. new关键字 是运行时在Heap里面创建对象,每new一次都一定会创建新对象,因为堆数据不共享。

示例

String str1= new String("abc"); (1)
String str2= "abc"; (2)

  1. str1是在Heap里面创建的对象。

  2. str2是指向Stack里面值为“abc”的引用变量,语句(2)的执行,首先会创建引用变量str2, 再查找Stack里面有没有“abc”,有则将 str2指向 “abc”,没有则在Stack里面创建一个“abc”,再将str2指向“abc”。

由此可终结为在建立一个对象时从两个地方分配内存,在堆中分配的内存实际建立这个对象,而在栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

heap和stack在内存中的区别

栈是一种线形集合,其添加和删除元素的操作应在同一端尾部完成。栈按照后进先出的方式进行处理。堆是栈的一个组成元素。
在JVM中,内存分为两个部分,Stack(栈)和Heap(堆),这里,我们从JVM的内存管理原理的角度来认识Stack和Heap,并通过这些原理认清Java中静态方法和静态属性的问题。 一般,JVM的内存分为两部分:Stack和Heap。
Stack(栈)是JVM的内存指令区。Stack管理很简单,push一 定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop一定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简 单,并且每次操作的数据或者指令字节长度是已知的。所以Java 基本数据类型,Java 指令代码,常量都保存在Stack中。
Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配一定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在 Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。而对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
由于Stack的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap 则是随机分配内存,不定长度,存在内存分配和回收的问题;因此在JVM中另有一个GC进程,定期扫描Heap ,它根据Stack中保存的4字节对象地址扫描Heap ,定位Heap 中这些对象,进行一些优化(例如合并空闲内存块什么的),并且假设Heap 中没有扫描到的区域都是空闲的,统统refresh(实际上是把Stack中丢失了对象地址的无用对象清除了),这就是垃圾收集的过程。

堆内存分配与回收策略

​ Java 中的堆是 JVM 管理的最大的一块内存空间,主要用于存放Java类的实例对象,其被划分为两个不同的区域:新生代 ( Young )和老年代 ( Old ),其中新生代 ( Young ) 又被划分为:Eden、From Survivor和To Survivor三个区域,如下图所示:

before JDK8 从JDK8开始,Metaspace(元空间)替代了永久代,如下图所示:

AFTER JDK8

  1. 堆大小 = 新生代( Young ) + 老年代( Old ),其可以通过参数 –Xms、Xmn、--Xmx 来指定:–Xms用于设置初始分配大小,默认为物理内存的1/16;-Xmn:young generation的heap大小;-Xmx用于设置最大分配内存,默认为物理内存的1/4。默认情况下,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小,老年代 ( Old ) = 2/3 的堆空间大小;

  2. 新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,为了便于区分,两个 Survivor 区域分别被命名为 from 和 to。默认情况下,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。JVM 每次只使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的,因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
  • 生命周期
    1. Eden区为Java对象分配堆内存,当 Eden 区没有足够空间分配时,JVM发起一次Minor GC,将Eden区仍然存活的对象放入Survivor from区,并清空 Eden 区;
    2. Eden区被清空后,继续为新的Java对象分配堆内存;
    3. 当Eden区再次没有足够空间分配时,JVM对Eden区和Survivor from区同时发起一次 Minor GC,把存活对象放入Survivor to区,同时清空Eden 区和Survivor from区;
    4. Eden区继续为新的Java对象分配堆内存,并重复上述过程:Eden区没有足够空间分配时,把Eden区和某个Survivor区的存活对象放到另一个Survivor区;
    5. JVM给每个对象设置了一个对象年龄(Age)计数器,每熬过一场Minor GC,对象年龄增加1岁,当它的年龄增加到阈值(默认为15,可以通过-XX:MaxTenuringThreshold 参数自定义该阀值),将被“晋升”到老年代,当 Old 区也被填满时,JVM发起一次 Major GC,对 Old 区进行垃圾回收。

说明:

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

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

  3. 发生在老年代的垃圾回收动作,出现了老年代 GC (Major GC / Full GC) 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

  4. JVM有2个GC线程:第一个线程负责回收Heap的Young区,第二个线程在Heap不足时,遍历Heap,将Young 区升级为Older区。

    Older区的大小等于-Xmx减去-Xmn,不能将-Xms的值设的过大,因为第二个线程被迫运行会降低JVM的性能。

  5. Permanent区则负责保存反射对象。

JAVA对象内存构建

​ 逻辑内存模型我们已经看到了,那当我们建立一个对象的时候是怎么进行访问的呢?

​ 在Java 语言中,对象访问是如何进行的?对象访问在Java 语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会却涉及Java 栈、Java 堆、方法区这三个最重要内存区域之间的关联关系,如下面的这句代码:

​ Object obj = new Object();

​ 假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java 栈的本地变量表中,作为一个reference 类型数据出现。而“new Object()”这部分的语义将会反映到Java 堆中,形成一块存储了Object 类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java 堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。由于reference 类型在Java 虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java 堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄和直接指针。

​ 如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如下图所示。

​ 如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的就是对象地址,如下图所示

​ 这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference 中存会改变句柄中的实例数据指针,而reference 本身不需要被修改。使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java 中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。就本书讨论的主要虚拟机Sun HotSpot 而言,它是使用第二种方式进行对象访问的,但从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

JAVA对象加载

首先了解类加载在Java程序运行图

​ Java的类加载机制所做的工作就是将经编译器编译后的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。 .class文件可能来源于本地磁盘、数据库、网络传输或者jar包等。

Java类加载的流程

Java的类加载主要分为以下5个阶段:

1. 加载

在本阶段,JVM主要做以下3个事情: (1) 通过一个类的全限定名来获取其定义的二进制字节流; (2) 将这个字节流所代表的静态存储结构转为方法区的运行时数据结构; (3) 在Java 堆中生成一个代表这个类的java.lang.Class 对象,作为方法区中这些数据的访问入口。

2.验证

即验证所加载类的正确性,是JVM保证安全的一道重要屏障,主要验证以下几个方面:

  • 文件格式验证:验证所加载的二进制文件格式是否正确。
  • 元数据验证:对类文件中所定义的语义进行验证,如访问修饰符使用是否正确等等。
  • 字节码验证:主要通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:负责对各种符号引用进行匹配性校验,保证外部依赖真实存在,并且符合外部依赖类、字段、方法的访问性。

3. 准备

该阶段为类变量(static)分配内存并设置初始值,但是对实例变量不会。如: 对于private static String a = "s",会分配内存并设置初始值,注意这里的初始值是变量类型的默认值null,并不是s。 对于常量(同时用final和static修饰)private static final String b = "s"会分配值s。 对于private String b = new String("b")则不会处理。

4. 解析

该阶段JVM将符号引用转化为直接引用 符号引用:即用一组符号代表引用的目标。如在学校中同学给你取的外号。 直接引用:目标的指针、相对偏移量或者句柄。即在学校中唯一指向你本人的学号。

5.初始化

类加载过程的最后一步,在该阶段中JVM为类中的变量赋值, 如在准备阶段中只赋了默认值的变量,在这里会赋上真实值,同时也会为实例变量赋值。即该阶段会类的构造器。

Java中的类加载器

Java中自带三个加载器: 1. Bootstrap ClassLoader 最顶层的加载器,加载核心类库,即jre/lib底下的文件,如rt.jar。 2. Extension or Ext Class-Loader 扩展类库加载器,加载jre/lib/ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。 3. Appclass Loader 应用类加载器,加载当前应用classpath下的所有类。

除了以上类加载器之外,还可以自定义加载器。Java的类加载器是一个分级结构:

双亲委派模型

​ Java的类加载采用了双亲委派模型,如上面的类加载器层次图所示,当一个类加载器收到加载任务时,会先交予它的父加载器去执行,如此一层一层往上知道最顶级的类加载器。只有当父类加载器无法完成任务时,才会交由自己来加载,这就是双亲委派模型。

双亲委派模型的好处:

  1. 避免重复加载,父类已经加载过的类,子类无需重新加载。
  2. 更加安全,可以保证同一个类被特定的加载器加载,防止用户随意定义加载器来加载核心的api。

JVM的体系结构

​ 我们首先要搞清楚的是:什么是数据以及什么是指令。然后要搞清楚对象的方法和对象的属性分别保存在哪里。

  1. 方法本身是指令的操作码部分,保存在Stack中;
  2. 方法内部变量作为指令的操作数部分,跟在指令的操作码之后,保存在Stack中(实际上是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操作码和指令操作数构成了完整的Java 指令。
  3. 对象实例包括其属性值作为数据,保存在数据区Heap 中。 非静态的对象属性作为对象实例的一部分保存在Heap 中,而对象实例必须通过Stack中保存的地址指针才能访问到。因此能否访问到对象实例以及它的非静态属性值完全取决于能否获得对象实例在Stack中的地址指针。

常见内存溢出错误

有了对内存结构清晰的认识,就可以帮助我们理解不同的OutOfMemoryErrors,下面列举一些比较常见的内存溢出错误,通过查看冒号“:”后面的提示信息,基本上就能断定是JVM运行时数据的哪个区域出现了问题。

Exception in thread “main”: java.lang.OutOfMemoryError: Java heap space

原因:对象不能被分配到堆内存中。

Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space

原因:类或者方法不能被加载到老年代。它可能出现在一个程序加载很多类的时候,比如引用了很多第三方的库。

Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因:创建的数组大于堆内存的空间。

Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?

原因:分配本地分配失败。JNI、本地库或者Java虚拟机都会从本地堆中分配内存空间。

Exception in thread “main”: java.lang.OutOfMemoryError: <reason> <stack trace>(Native method)

原因:同样是本地方法内存分配失败,只不过是JNI或者本地方法或者Java虚拟机发现。

问题处理

内存溢出

java.lang.OutOfMemoryError: Java heap space\PermGen space

解决方法:

  • 代码层面

    ​ 要解决这个区域的异常,一般的手段是首先通过内存映像分析工具(如EclipseMemory Analyzer、IDE的JProfiler)对dump 出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。

    ​ 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots 引用链的信息,就可以比较准确地定位出泄漏代码的位置。

    ​ 如果不存在泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

    服务器内存快照dump:

    1. echo $JAVA_HOME 获取jmap地址
    2. ps -ef|grep java 获取java线程id
    3. ../bin/jmap -dump:live,format=b,file=heap.hprof 线程id
  • 服务器层面

    • Java heap space

    ​ JVM堆的设置是指java程序运行过程中JVM可以调配使用的内存空间的设置。JVM在启动的时候会自动设置Heapsize的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。

提示:

  1. 在JVM中如果98%的时间是用于GC且可用的Heap size 不足2%的时候将抛出此异常信息。
  2. Heap Size 最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx选项设置为相同,而-Xmn为1/4的-Xmx值。
    • PermGen space PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,这块内存主要是被JVM存放Class和Meta信息的,Class在被Loader时就会被放到PermGen space中,它和存放类实例(Instance)的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的应用中有很多CLASS的话,就很可能出现PermGen space错误。

分析示例

  1. 内存溢出问题分析

    • 准备工具

      • JProfiler分析工具
      • JDK
    • 步骤
      1. echo $JAVA_HOME 获取jmap地址
      2. ps -ef|grep java 获取java线程id
      3. ../bin/jmap -dump:live,format=b,file=heap.hprof 线程id