JVM系列之四:运行时数据区
1. JVM架构图
Java虚拟机主要分为五大模块:类装载器子系统、运行时数据区、执行引擎、本地方法接口和垃圾收集模块。
2. JDK1.7内存模型-运行时数据区域
根据《Java 虚拟机规范(Java SE 7 版)》规定,Java 虚拟机所管理的内存如下图所示。
1-3为线程私有,4-5为线程共享
1、程序计数器:为了线程切换后能恢复到正确的执行位置。线程私有
2、Java虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型:方法被调用时创建栈帧-->局部变量表->局部变量、对象引用。线程私有
3、本地方法栈:为虚拟机执使用到的Native方法服务。线程私有
4、Java堆:存放所有new出来的东西,在虚拟机启动时创建,垃圾收集器管理的主要区域。线程共享
5、方法区:存储被虚拟机加载的类信息、常量、静态常量、静态方法等。线程共享
6、运行时常量池(方法区的一部分)
2.1 运行时数据区域存储的内容
2.2 程序计数器
举个例子 程序先去执行A线程,执行到一半,然后就去执行B线程,然后又跑回来接着执行A线程,那程序是怎么记住A线程已经执行到哪里了呢?这就需要程序计数器了
1.什么是程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码行号的指示器。
2.程序计数器的作用
-
字节码解释器工作通过改变程序计数器的值来选取下一条需要执行的字节码指令。如:分支、循环、跳转、异常处理、线程恢复等基础功能。
-
多线程情况下,程序计数器表示当前线程执行的位置,从而在线程切换的时候知道此线程上一次执行的位置在哪里。
3.程序计数器的特点
-
一块较小的内存空间。
-
每个线程独立的程序计数器,各线程间的程序计数器互不影响,独立存储,称为“线程私有内存”。
-
生命周期随着线程的创建而创建,随着线程的结束而死亡。
-
如果当前线程执行的是Java方法,程序计数器记录的是当前正在执行的虚拟机字节码指令的地址。如果当前线程正在执行的是Native方法,程序计数器的值则为空(Undefined)。
-
此内存区域是Java虚拟机规范中没有规定任何的OutOfMemoryError(OOM)情况的区域。
-
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。
- 为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器
2.3 Java 虚拟机栈
1.什么是Java虚拟机栈
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
2.什么是局部变量表
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
3.Java虚拟机栈的特点
-
每个线程私有的,生命周期和线程相同。
-
每个方法在执行的同时创建一个栈帧。
-
局部变量表所需的内存空间在编译期间完成分配,在运行期间不会改变局部变量表的大小。
-
在Java虚拟机规范中,对这个区域规定了两种异常情况:
(1)StackOverFlowError:如果线程请求的栈深度太深,超出了虚拟机所允许的深度(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
或者创建线程数量较多时(每个线程都会创建私有的栈内存)会出现栈内存溢出StackOverflow(2)OutOfMemoryError:虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间
-
JDK 1.5 以后每个线程堆栈默认大小为1M,以前每个线程栈大小为256K。可以通过 -Xss 参数来设置每个线程的堆栈大小。
线程栈的大小是个双刃剑,如果设置过小,可能会出现栈溢出,特别是在该线程内有递归、大的循环时出现溢出的可能性更大;如果该值设置过大,就会影响到创建线程的数量,当遇到多线程的应用时可能出现内存溢出的错误。
2.4 本地方法栈
(1)本地方法栈与虚拟机栈锁发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
(2)有的虚拟机(比如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
(3)也会有 StackOverflowError 和 OutOfMemoryError 异常。
2.5 Java 堆
1.什么是堆
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在堆上进行分配。1.7后,字符串常量池从永久代中剥离出来,存放在堆中。
1. 堆(Java堆) 堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域, 在JVM启动时创建,该内存区域存放了对象实例(包括基本类型的变量及其值)及数组(所有new的对象)。 但是并不是所有的对象都在堆上,由于栈上分配和标量替换,导致有些对象不在堆上。 其大小通过-Xms(最小值)和-Xmx(最大值)参数设置, 1. -Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G, 2. -Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G, 3. 默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation 来指定这个比列; 4. 当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation来指定这个比例, 5. 对于运行系统,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。 由于现在收集器都是采用分代收集算法,堆被划分为新生代和老年代。 新生代主要存储新创建的对象和尚未进入老年代的对象。 老年代存储经过多次新生代GC(Minor GC)仍然存活的对象。 堆中没有足够的内存完成实例分配,并且堆也无法扩展时,将会出现OOM异常。(内存泄漏 / 内存溢出)。满足下面两个条件就会抛出OOM。 (1)JVM 98% 的时间都花费在内存回收。 (2)每次回收的内存小于2%。 同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。 1.1 为什么要分代 堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。 给堆内存分代是为了提高对象内存分配和垃圾回收的效率。 试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集, 而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率,这简直太可怕了。 有了内存分代,情况就不同了, 1. 新创建的对象会在新生代中分配内存, 2. 经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中, 3. 新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC, 4. 老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收, 5. 永久代中回收效果太差,一般不进行垃圾回收 还可以根据不同年代的特点采用合适的垃圾收集算法。 分代收集大大提升了收集效率,这些都是内存分代带来的好处。 1.2 新生代 程序新创建的对象都是从新生代分配内存, 新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,默认比例为8:1:1。 划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。 新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 1. GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。 2. GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区, 3. 而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。 (默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中) 4. 接着清空Eden区和From Survivor区, 5. 新生代中存活的对象都在To Survivor区。 6. 接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区, 总之,不管怎样都会保证To Survivor区在一轮GC后是空的。 7. GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。 可通过-Xmn参数来指定新生代的大小, 也可以通过-XX:SurvivorRation来调整Eden Space及Survivor Space的大小。 1.3 老年代 用于存放经过多次新生代GC仍然存活的对象,老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。 老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。 主要存储的有:如缓存对象,新建的对象也有可能直接进入老年代, 主要有两种情况: ①大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。 ②大的数组对象,且数组中无引用外部对象。
2.堆的特点
(1)Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例和数组,几乎所有的对象实例都在这里分配内存。
(2)Java堆是垃圾收集器管理的主要区域。
(3)Java堆还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor控件等。
(4)内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
(5)OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
(6) 堆分为初生代(Young Gen)和老年代(Tenured Gen),比例默认为1:2,而初生代又分为Eden和From和To三个区域,比例默认为8:1:1
(7) -Xms:最大堆大小,默认为物理内存的1/4但小于1G
-Xsx:初始堆大小,默认为操作系统物理内存的1/64但小于1G
-Xmn:新生代大小
(8) Java堆可以处理物理上不连续的内存空间中,只要逻辑上连续即可。
堆有自己进一步的内存分块划分。
“方法区”是JVM的规范,而“永久代”是方法区的一种实现,并且只有HotSpot才有“PermGen space”,而对于其他类型的虚拟机并没有“PermGen space”。
在JDK1.8中,HotSpot已经没有“PermGen space”这个区间了,取而代之是Metaspace(元空间)
2.6 虚拟机栈和堆
为更好的理解Java线程栈和堆,我们简单的认为Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。
每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程依然在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。
所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。
堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。
下面这张图演示了调用方法和本地变量存放在线程栈上,对象存放在堆上。
- 一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。
- 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
- 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。
- 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
- 静态成员变量跟随着类定义一起也存放在堆上。
- 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。
下图演示了上面提到的点:
两个线程拥有一些列的本地变量。其中一个本地变量(Local Variable 2)执行堆上的一个共享对象(Object 3)。这两个线程分别拥有同一个对象的不同引用。这些引用都是本地变量,因此存放在各自线程的线程栈上。这两个不同的引用指向堆上同一个对象。
注意,这个共享对象(Object 3)持有Object2和Object4一个引用作为其成员变量(如图中Object3指向Object2和Object4的箭头)。通过在Object3中这些成员变量引用,这两个线程就可以访问Object2和Object4。
2.7 方法区
属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和 -XX:MaxPermSize 参数限制方法区的大小。
运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
方法区空间不够的时候出现OOM。(主流框架中,通过字节码技术动态生成大量的Class)
2.8 运行时常量池
属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。r。
(1)运行时常量池是方法区的一部分,用于存放编译期生成的各种 字面量 和 符号引用。
(2)字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。
(3)符号引用则属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名、字段的名称和描述符 和 方法的名称和描述符。
(4)因为运行时常量池是方法区的一部分,那么当常量池无法再申请到内存时也会抛出 OutOfMemoryError 异常。
2.9 直接内存
1.什么是直接内存
非虚拟机运行时数据区的部分
在 JDK 1.4 中新加入 NIO (New Input/Output) 类,引入了一种基于通道(Channel)和缓存(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。可以避免在 Java 堆和 Native 堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存限制从而导致动态扩展时出现该异常。
NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理内存的类——DirectBuffer。 DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制;而DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
2. 区别和应用场景
-
直接内存的读写操作比普通Buffer快,但它的创建、销毁比普通Buffer慢。
-
因此直接内存使用于需要大内存空间且频繁访问的场合,不适用于频繁申请释放内存的场合。
3. JDK1.8内存模型-运行时数据区域
3.1 JDK1.8 内存模型
3.2 JDK1.8 V.S. JDK1.7
- 1.8同1.7比,最大的差别就是:元数据区取代了永久代。
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。所以原来属于方法区的运行时常量池就属于元空间了
- 元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存。
- 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代(运行常量池和字符串常量池都在JVM中)
- 在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代(运行常量池在JVM中,字符串常量池在堆中)
- 在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace) (运行常量池在直接内存元空间中,字符串常量池在堆中)
3.3 堆
在JDK1.8中把存放元数据中的永久内存从堆内存中移到了本地内存中,JDK1.8中JVM堆内存结构就变成了如下:
1.4 Java8 内存分代的改进 在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区域了,取而代之是一个叫做 Metaspace(元空间) 的东西。 实际上在JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。 但永久代仍存在于JDK1.7中,并没完全移除, 譬如符号引用(Symbols)转移到了native heap; 字面量(interned strings)转移到了java heap; 类的静态变量(class statics)转移到了java heap。 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。 不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小: -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。 -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。 取消永久代的原因: (1)字符串存在永久代中,容易出现性能问题和内存溢出。 (2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。 (3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
3.4 元空间
1.什么是元数据区
JDK 8的HotSpot JVM现在使用的是本地内存来表示类的元数据,这个区域就叫做元空间。
2.元空间的特点
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
- 不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
- 在JDK1.8中,永久代已经不存在,存储的类信息、编译后的代码数据等已经移动到了MetaSpace(元空间)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。
- 原来类的静态变量和字面量Interned Strings 都被转移到了java堆区
- 持久代删除了之后,很明显,JVM会忽略PermSize和MaxPermSize这两个参数,再也看不到java.lang.OutOfMemoryError: PermGen error的异常了。
- 元空间的大小仅受本地内存限制,可以通过以下参数来指定元空间大小:
-
- -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
- -XX:MaxMetaspaceSize,最大空间,默认是没有限制的
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
- -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
这样就从一定程度上解决了原来在运行时生成大量类造成经常Full GC问题,如运行时使用反射、代理等。所以升级以后Java堆空间可能会增加。
3.5 方法区
所以对于方法区,Java8之后的变化:
- 移除了永久代(PermGen),替换为元空间(Metaspace);
- 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);
- 永久代中的 interned Strings 和 class static variables 转移到了 Java heap;
- 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)
4. OOM && SOF
OutOfMemoryError异常: 除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能,
内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
内存溢出:指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
从定义上可以看出内存泄露是内存溢出的一种诱因,不是唯一因素。
4.1 发生了内存泄露或溢出怎么办?
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
(1)通过参数 -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现OOM异常的时候Dump出内存映像以便于分析。
(2)一般手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。(到底是出现了内存泄漏还是内存溢出)
哪些对象被怀疑为内存泄漏,哪些对象占的空间最大及对象的调用关系,还可以分析线程状态,可以观察到线程被阻塞在哪个对象上,从而判断系统的瓶颈。
(3)如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象时通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。 找到引用信息,可以准确的定位出内存泄漏的代码位置。(HashMap中的元素的某些属性改变了,影响了hashcode的值会发生内存泄漏)
(4)如果不存在内存泄漏,就应当检查虚拟机的参数(-Xmx与-Xms)的设置是否适当,是否可以调大;修改代码逻辑,把某些对象生命周期过长,持有状态时间过长等情况的代码修改。
4.2 Java Heap 溢出
在jvm规范中,堆中的内存是用来生成对象实例和数组的。
如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。
当生成新对象时,内存的申请过程如下:
- jvm先尝试在eden区分配新建对象所需的内存;
- 如果内存大小足够,申请结束,否则下一步;
- jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
- Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
- 当OLD区空间不够时,JVM会在OLD区进行full GC;
- full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”: outOfMemoryError:java heap space
4.3 虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
- 不断创建线程,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
- 这里需要注意当栈的大小越大可分配的线程数就越少。
- 用Xss设置
4.4 运行时常量池溢出
- 异常信息:java.lang.OutOfMemoryError:PermGen space
- 如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。
- 该方法的作用是:如果池中已经包含一个等于此String的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
- 由于常量池分配在方法区内,我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小,从而间接限制其中常量池的容量。
4.5 方法区溢出
异常信息:java.lang.OutOfMemoryError: PermGen space
方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
所以如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。
我们可以通过-XX:PermSize和-XX:MaxPermSize限制方法区的大小
4.6 java.lang.OutOfMemoryError: GC overhead limit exceeded
原因:执行垃圾收集的时间比例太大, 有效的运算量太小. 默认情况下, 如果GC花费的时间超过 98%, 并且GC回收的内存少于 2%, JVM就会抛出这个错误。
目的是为了让应用终止,给开发者机会去诊断问题。一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象,从而导致该异常。
解决方法:
1. 大对象在使用之后指向null。
2. 增加参数,-XX:-UseGCOverheadLimit,关闭这个特性;
3. 增加heap大小,-Xmx1024m
4.7 SOF (堆栈溢出 StackOverflow)
StackOverflowError 的定义:当应用程序递归太深而发生堆栈溢出时,抛出该错误。 因为栈一般默认为1-2M,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1M而导致溢出。
栈溢出的原因:
递归调用
大量循环或死循环
全局变量是否过多
数组、List、map数据过大
4.8 内存泄漏的场景
(1)使用静态的集合类
静态的集合类的生命周期和应用程序的生命周期一样长,所以在程序结束前容器中的对象不能被释放,会造成内存泄露。
解决办法是最好不使用静态的集合类,如果使用的话,在不需要容器时要将其赋值为null。
修改hashset中对象的参数值,且参数是计算哈希值的字段
(2)单例模式可能会造成内存泄露(长生命周期的对象持有短生命周期对象的引用)
单例模式只允许应用程序存在一个实例对象,并且这个实例对象的生命周期和应用程序的生命周期一样长,如果单例对象中拥有另一个对象的引用的话,这个被引用的对象就不能被及时回收。
解决办法是单例对象中持有的其他对象使用弱引用,弱引用对象在GC线程工作时,其占用的内存会被回收掉。
(3)数据库、网络、输入输出流,这些资源没有显示的关闭
垃圾回收只负责内存回收,如果对象正在使用资源的话,Java虚拟机不能判断这些对象是不是正在进行操作,比如输入输出,也就不能回收这些对象占用的内存,所以在资源使用完后要调用close()方法关闭。
4.9 如何避免发生内存泄露和溢出
1、尽早释放无用对象的引用
2、使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
3、尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
4、避免在循环中创建对象
5、开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
5. 参考网址
- https://blog.csdn.net/qq_41701956/article/details/81664921
- https://blog.csdn.net/csdnliuxin123524/article/details/81303711
- https://www.jianshu.com/p/904b15a8281f
- JVM原理最全、清晰、通俗讲解,五天40小时吐血整理
- Java虚拟机(JVM)你只要看这一篇就够了!(**************)
- JVM基础(一):JAVA内存区域
- 面试问题:你了解Java内存模型么(Java7、8、9内存模型的区别)
- JDK1.7和JDK1.8的内存模型比较
- JDK1.8 JVM内存模型
- JVM——Java内存模型 (JDK1.8)
- Java8虚拟机内存模型
- Java内存模型
- http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
- 【Java面试整理之JVM】深入理解JVM结构、类加载机制、垃圾回收GC原理、JVM内存分配策略、JVM内存泄露和溢出
- jvm原理,内存模型及GC机制
- JVM内存管理及GC机制