1.JDK1.7内存模型-运行时数据区域
根据《Java虚拟机规范(Java SE 7 版)》规定,Java虚拟机所管理的内存如下图所示
- 1.堆:存放所有new出来的东西,在虚拟机启动时创建,垃圾收集器管理的主要区域。线程共享
- 2.方法区:存储被虚拟机加载的类信息、常量、静态常量、静态方法等。线程共享。
- 3.本地方法栈:为虚拟机执行使用到的Native方法服务,线程私有。
- 4.虚拟机栈:虚拟机栈描述的是Java方法执行的内存模型:方法被调用时创建栈帧 ------>局部变量表 -------->局部变量、对象引用。线程私有。
- 5.程序计数器:线程切换后能恢复到正确的执行位置。线程私有。
- 6.常量池:方法区的一部分
名称 | 特征 | 作用 | 配置参数 | 异常 |
---|---|---|---|---|
程序计数器 | 占用内存小,生命周期与线程相同 | 大致为字节码行号指示器 | 无 | 无 |
虚拟机栈 | 生命周期与线程相同,使用连续内存空间 | 存储局部变量表、操作栈动态链接、方法出口等信息 | -Xss 设置栈的大小 | StackOverFlowError OutOfMemoryError |
堆 | 生命周期与虚拟机相同,可以不使用连续的内存地址 | 保存对象实例,所有对象实例(包括数组)都要在堆上分配 | -Xms: 最大堆,默认为物理内存的1/4 -Xsx:默认为物理内存的1/64 -Xmn:新生代大小 |
OutOfMemoryError |
方法区 | 生命周期与虚拟机相同,可以使用不连续内存 | 存储类信息、常量、静态变量、即时编译器编译后的代码等数据 | -XX:PermSize //永久代最小大小 -XX:MaxPermSize //最大大小 |
OutOfMemoryError |
运行时常量池 | 方法区的一部分,具有动态性 | 存放字面量及符号引用 | 无 | 无 |
1.1运行时数据区域存储的内容
1.2 程序计数器
1.什么时程序计数器
程序计数器(Program counter Register)是一块较小的内存空间,它可以看作时当前线程所执行的字节码行号的指示器。
2.程序计数器的作用
- 字节码解释器工作通过改变程序计数器的值来选取下一条需要执行的字节码指令。如分支、循环、跳转、异常处理、线程恢复等基础功能。
- 多线程情况下,程序计数器表示当前线程执行的位置,从而在线程切换的时候知道此线程上一次执行的位置在哪里。
3.程序计数器的特点
- 一块较小的内存空间
- 每个线程独立的程序计数器
- 生命周期随着线程创建和死亡。
- 如果线程执行的时Java方法,程序计数器记录的时当前正在执行的字节码指令地址。如果执行的时Native方法,程序计数器的值则为空(Undefined)。
- 此内存区域时Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
- 由于Java虚拟机多线程时通过线程轮流切换并分配处理器执行时间方式来时间,在任何一个确定的时刻。一个处理器(多核处理器来说是一个内核)都只会执行一条线程中的指令。
- 为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
1.3 虚拟机栈
1.什么是虚拟机栈
虚拟机栈描述的是Java方法执行的内存模型。每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,方法出口,动态链接等信息,每一个方法从调用直至执行完成的过程,就对应这一个栈帧从虚拟机中入栈到出栈的过程。
2.什么是局部变量表
存放了编译期可知的各种基本类型(boolean,byte,char,short,int long,float,double),对象引用(reference类型)和returnAddress类型(指向了一条字节码指令地址),其中64位长度long和double类型的数据会占用2个局部变量的空间,其余数据类型占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部变量表的大小空间。
3.Java虚拟机栈的特点
- 1.每个线程私有,生命周期和线程相同
- 2.每个方法在执行的同时,创建一个栈帧。
- 3.局部变量表所需的内存空间在编译期间完成分配,在运行期间不会改变局部变量表大小。
- 在Java虚拟机规范中,对这个区域规定了两种异常:
StackOverFlowError:如果线程请求的栈深度太深,超出Xss规定栈的最大空间,超出就会报错。
OutOfMemoryError:虚拟机栈可以动态扩展,如果扩展到无法申请足够内存空间。
Xss:每个线程堆栈大小 默认1M。
1.4 本地方法栈
1.本地方法栈与虚拟机栈发挥的作用非常相似,他们之间的区别:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机执行Native服务。
2.有的虚拟机直接就把本地方法栈和虚拟机栈合二为一 (比如Sun HotSpot虚拟机)
3.也会有StackOverFlowError和OutOfMemoryError异常。
1.5 堆
堆是JVM中内存占用最大,管理最复杂的一个区域,其唯一用途就是存放对象实例。所有的对象实例及数组都在堆上进行分配。1.7后,字符串常量池从永久代中剥离出来,放在堆中。
并不是所有对象都在堆上,由于栈上分配和标量替换,导致有些对象不在堆上。
堆大小通过 -Xms(最小值)和-Xmx(最大值)参数设置。
- -Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64 但小于1G。
- -Xmx为JVM可申请的最大内存,默认为物理内存的1/4 但小于1G
- 当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation来指定这个比例。
- 当空余堆内存大于70%时,JVM会减小heap大小到-Xms指定的大小,可通过-XX:MaxHeapFreeRation 来指定这个比例。
- 对于后端系统,为了避免在运行时频繁调整Heap大小,通常-Xms和-Xmx设置成一样的值。
由于收集器都是采用分代收集算法,堆被划分为新生代和老年代。
新生代:主要存储新创建和尚未进入老年代
老年代:存储经过多次minor GC后仍然存活的对象。
堆中没有足够的内存完成实例分配,并且堆也无法扩展时,将会出现OOM异常(内存溢出,内存泄漏)满足下面两个条件就会OOM。
1.JVM 98%的时间都花在内存回收。
2.每次回收的内存小于2%。
为什么要分代?
给堆内存分代是为了提高对象内存分配和垃圾回收的效率。
如果没有区域划分,生命周期很长的和新创建的对象放在一起,堆内存需要频繁进行回收,每次回收都遍历所有对象,花费的时间代价是巨大的,严重影响GC效率。
内存分代后,情况就不同了。
1.新的对象会在新生代中分配。
2.经过多次回收仍然存活的放在老年代中,静态属性和类信息等存放在永久代中。
3.新生代中的对象存活时间短,只需要在新生代中频繁进行GC
4.老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收。
5.永久代中回收效果差,一般不进行回收。
新生代
程序创建的对象都是从新生代分配内存。
新生代由Eden Space和两块相同大小的Survivor Space构成,默认比例为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区。
- 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的大小。
老年代
用于存放经过多次新生代GC依然存活的对象,老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
老年代所占的内存大小为-Xmx 减去 -Xmn
主要存储的有:缓存对象,新建的对象也有可能直接进入老年代。
主要有两种情况。
- 大对象,可通过启动参数设置:-XX:PretenureSizeThreshold=1024(单位字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
2.大的数组对象,且数组中无引用外部对象。
堆的特点:
1.Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例和数组,几乎所有的对象实例都在这里分配。
2.Java堆是垃圾收集器管理的主要区域。
3.Java堆还可以细分为:新生代和老年代;在细分一点有Eden空间、From Survivor空间、To Survivor空间等。
4.可以位于物理上不连续的空间,但是逻辑上要连续。
5.OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
6.新生代和老年代比例默认1:2 新生代Eden和From、To三个区域,比例默认为8:1:1
- -Xms 最小堆大小 默认为物理内存的1/64 但小于1G -Xmx 最大堆大小 默认为物理内存的1/4 但小于1G -Xmn新生代大小。
方法区是JVM的规范,而永久代是方法区的一种实现,并且只有HotSpot才有永久代,而对于其他类型的虚拟机并没有。在JDK1.8中,HotSpot已经没有永久代这个区间了,取而代之的是MetaSpace。
1.6 方法区
属于共享内存区域,存储已被虚拟机加载的类信息,常量、静态变量、即时编译后的代码等数据。
默认最小值为16MB,最大值为64MB,可以通过-XX:PermSize 和-XX:MaxPermSize 参数限制方法区的大小
运行时常量池:是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后方法方法区的运行时常量池中。
方法区空间不够的时候出现OOM。(主流框架中,通过字节码技术动态生成大量的Class)。
1.7 运行时常量池
属于方法区的一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String的intern())都可以将常量放入池中。
字面量:比较借金额Java语言的常量概念,如文本字符串,被声明为final的常量值等。
符号引用:属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
因为运行时常量池是方法区的一部分,那么当常量池无法再申请到内存时也会抛出OOM异常。
1.8 直接内存
什么是直接内存?
非JVM内存,操作系统的部分内存。
JDK1.4加入的NIO类,引入了一种基于通道(Channel)和缓存(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。可以避免在Java堆和Native堆中来回的数据耗时操作。
OutOfMemoryError:会受到本机内存限制,如果内存区域总和大于物理内存从而导致动态扩展时出现该异常。
NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理的内存类--DirectByteBuffer。DirectByteBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer扔在JVM堆上分配内存,其最大的内存收到最大堆限制,而DirectByteByffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
区别和应用场景
1.直接内存的读写操作比普通buffer快,但它的创建、销毁比普通的Buffer慢。
2.因此直接内存使用于需要大内存空间且频繁访问的场合,不适合用于频繁申请释放内存的场合。
2.JDK1.8内存模型- 运行时数据区域
2.1 JDK1.8 内存模型
2.2 JDK1.8 VS JDK1.7
- 1.8同1.7比,最大的差别就是:元空间取代了永久代。
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现,所以原来属于方法区的运行常量池就属于元空间了。
- 元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
- 在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区实现为永久代(运行时常量池和字符串常量池都在JVM)中
- 在JDK1.7中 字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆中,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代(运行时常量池在永久代,字符串常量池在堆中)
- 在JDK1.8 hotspot移除了永久代取而代之的是元空间,这时字符串常量池还在堆中,运行时常量池在元空间。元空间已经挪到JVM外。(运行时常量池在元空间,字符串常量池在堆中)
2.3 堆
JDK1.8中把存放元数据的永久代从堆内存移动到了本地内存中,JDK1.8内存结构就变成了如下:
实际上在JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是Native Heap。
但永久代仍存在于JDK1.7中,并没有完全移除。譬如:
符号引用(Symbols)转移到了native heap。
字面量(interned strings)转移到了java heap。
类的静态变量(class statics)转移到了java heap。
元空间本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间和永久代最大区别是 元空间在本地内存,永久代在JVM中。
元空间仅受本地内存限制,但可以同过-XX:metaSpaceSize调整。
取消永久代的原因:
1.字符串存在永久代中,容易出现性能问题和内存溢出。
2.类及方法的信息等比较难确定大小,因此对永久代大小指定比较困难。太小容易永久代溢出,太大容易老年代溢出。
3.永久代会为GC带来不必要的复杂度,并且回收效率低。
2.4 元空间
JDK8的HotSpot JVM现在使用的是本地内存来表示类的元数据,这个区域就叫做元空间。
1.元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
2.不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
3.在JDK1.8中,永久代已经不存在,存储的类信息,编译后代码等数据已经移动到了MetaSpace中。
4.原来静态变量(class statics)和字面量(Interned Strings)都被转移到了Java堆中。(JDK1.7开始)
5.永久代删除后,JVM胡烈PermSize和MaxPermSize这两个参数,再也看不到java.lang.OutOfMemoryError:PermGen error
的异常了。
6.元空间大小仅受本地内存限制,可以通过以下参数来指定元空间大小
- -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值
- -XX:MaxMetaspaceSize,最大空间,默认是没有限制的
- -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
- -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
2.5 方法区
所以对于方法区,Java8之后的变化:
1.移除永久代(PermGen),替换为元空间(MetaSpace)
2.永久代中的class metadata转移到本地内存(本地内存,而不是虚拟机)
3.永久代中的字面量(interned String)和类静态变量(class static variables)转移到了Java Heap.
3.OOM && SOF
OutOfMemoryError异常:除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemory异常的可能。
内存泄漏:指程序中动态分配内存给一下临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但是已无用。
内存溢出:指程序运行过程中无法申请到足够的内存而导致的。内存不够时,会先进行垃圾回收,回收后仍然空间不够。内存泄漏是内存溢出的一种诱因,不是唯一因素。
3.1 发生内存泄漏或溢出了怎么办
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess
Java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出。
1.-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现OOM时候Dump出内存映像便于分析。
2.使用内存映像分析工具(Jstack、Jmap、Jconsole)对映像进行分析。确定出是因为内存泄漏还是内存溢出导致的。
3.如果是内存泄漏,进一步通过工具查看泄漏对象GC Roots的引用链。于是就能找到引用信息,定位内存泄漏代码位置。
4.如果不是内存泄漏,就检查虚拟机参数(-Xmx和-Xms)设置的是否合理。并修改代码,把某些对象生命周期过长,持有状态时间过长的代码修改。
3.2 Java Heap溢出
在JVM规范中,堆中的内存是用来生成对象实例和数组的。
如果细分、堆内存还可以分为年轻代和年老代、年轻代包括一个Eden和两个survivor区。
当生成新对象时,内存申请过程如下:
1.JVM先尝试在eden区分配新建对象所需的内存。
2.如果内存大小足够,申请结束,否则启动young GC。
3.JVM启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Sruvivor区。
4.Survivor区被用来作为Eden及old区中间交换区域,当old区空间足够时,Survivor区的对象会被移到old区。否则会被保留在Survivor区。
5.当old区空间不够时,JVM会在old区进行full GC。
6.full GC后,若Survivor及old去仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新的对象创建内存区域,则出现OOM。
4.3 虚拟机栈和本地方法栈溢出
1.如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflow异常。
2.不断创建线程,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
3.这里需要注意:当Xss设置的大小越大,可分配的线程数就越少。
4.4 运行时常量池溢出(针对JDK1.7以前)
异常信息:java.lang.OutOfMemoryError:PermGen space
如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。如果池中已经包含一个等于此String的字符串,则返回常量池中的这个字符串,否则,将此字符串添加到常量池中,并且返回此字符串的引用。
4.5 方法区溢出(针对JDK1.7及以前)
异常信息:java.lang.OutOfMemoryError:PermGen space
方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述。
所以如果程序加载类过多,或者使用反射、cglib代理等动态代理生成类的技术,就可能导致该区发生内存溢出。
4.6 OutOfMemoryError:GC overhead limit exceeded
原因:执行垃圾回收的时间比例太大,有效运算量太小。默认情况下如果垃圾回收超过98%,并且GC回收的内存小于2%,JVM就会抛这个异常。
一般是应用程序在有限的内存上创建了大量的临时对象或者弱引用对象,从而导致该异常。
解决方法:
1.大对象在使用后指向null
2.增加参数 -XX:-UseGCOverheadLimit 关闭这个特性。
3.增加堆大小,-Xmx
4.7 StackOverflow 堆栈溢出
StackOverFlowError:当应用程序递归太深而发生堆栈溢出时,抛出该错误。栈一般默认为1M,一旦出现死循环或者大量的递归调用,在不断压栈的过程中,造成栈容量超过1M而导致溢出。
栈溢出原因:
1.递归调用
2.大量循环或者死循环
3.全局变量是否过多
4.数组,List、map数据过大
4.8 内存泄漏场景
- 1.使用静态的集合类:静态集合类生命周期和应用程序一样长,所以JVM结束前都不会释放。造成内存泄漏
- 2.单例模式可能会造成内存泄漏(长生命周期的对象持有短生命周期对象的引用):单例生命周期和应用程序羊场,如果单例中拥有另一个 对象的引用。那么就会内存泄漏。
- 3.数据库、网络、输入输出这些资源没有显式的关闭:如果对象正在使用资源,比如输入输出,Java无法判断这些对象是不是正在进行操作,所以不能回收。资源使用完后要调用close()方法关闭。
- 4.存在于集合中的对象,被修改了hashCode,会造成内存泄漏。
4.9 如何避免发生内存泄漏和溢出
1.尽早释放无用对象的引用。
2.使用StringBuffer或Build,避免使用String
3.尽量少用静态集合类。
4.避免在循环中创建大量对象。
5.开启大文件或从数据库一次拿太多数据容易造成溢出。