JVM内存模型图:
上图均来自于牛客网。
JVM的内存空间分为了5部分:
①程序计数寄存器
②java虚拟机栈
③本地方法栈
④堆
⑤方法区
1.1定义
程序计数器是一块较小的内存空间,可看作当前线程正在执行的字节码的行号指示器。执行情况有2种:
①执行java方法。计数器记录的就是当前线程正在执行的字节码指令的地址
②本地方法。那么程序计数器值为undefined
1.2作用
作用有二:
①字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行,选择,循环,异常处理
②在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行的位置。
1.3特点
一块较小的内存空间;
线程私有,每条线程都有一个独立的程序计数器;
是唯一一个不会出现OOM的内存区域;
生命周期随着线程的创建而创建,随着线程结束而死亡。
2.1 定义
相对于基于寄存器的运行环境来说,JVM是基于栈结构的运行环境。栈结构移植性,可控性都强。是java方法执行的内存区域,是线程私有的。
栈中元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程,在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧,执行的方法也称为当前方法。栈帧是方法运行的基本结构。
在执行引擎时,所有指令只能针对当前栈进行操作。
StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中。
虚拟机栈通过压/出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定。
栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等
总结:java虚拟机栈为每个即将运行的java方法创建栈帧,用于存储方法在运行时的所需信息:
本地方法栈实现功能与虚拟机方法栈基本相同,异常也是。
区别在于,虚拟机为java方法服务,本地方法栈为native方法服务。在jvm内存布局中,本地方法栈也是线程私有的。但是虚拟机主“内”,本地方法栈主“外”。
堆是OOM故障的最主要发源地,他几乎存储所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用。通常情况下,它占用的空间是所有区域中最大的。
堆内存空间既可以固定大小,也可运行时动态调整。通过如下参数设定初始值和最大值,比如:
-Xms256M. -Xmx1024M
其中-X表示他是JVM运行参数
-XX:MaxTenuringThreshold
参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升为老年代。
若Survivor区无法放下,或者超大对象的阈值超过上限,则尝试在老年代中进行分配;
如果老年代也无法放下,则会触发Full Garbage Collection(Full GC);
如果依然无法放下,则抛OOM.
堆出现OOM的概率是所有内存耗尽异常中最高的
出错时的堆内信息对解决问题非常有帮助,所以给JVM设置运行参数
XX:+HeapDumpOnOutOfMemoryError
让JVM遇到OOM异常时能输出堆内信息
ps:在不同的JVM实现及不同的回收机制中,堆内存的划分方式是不一样的
特点
java虚拟机所需要管理的内存中最大的一块
堆内存物理上不一定要连续,只需要逻辑上连续即可,就像磁盘空间一样。
堆的大小既可以固定也可以扩展,但主流的虚拟机堆得大小是可以扩展的(通过-Xmx和-Xms控制),因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError.
线程共享
整个java虚拟机只有一个堆,所有的线程都访问同一个堆。
它是被所有线程共享的一块内存区域,在虚拟机启动时创建。而程序计数器,java虚拟机栈,本地方法栈都是一个线程对于一个。
拓展
jvm垃圾收集器有哪些,以及优劣势比较。
1 串行收集器
串行收集器是最简单的,他设计为在单核的环境下工作,你几乎不会使用到它。它在工作的时候会暂时整个应用的运行,因此在所有服务器环境下都不可能被使用。
使用方法:-XX:+UseSerialGC
2 并行收集器
这是JVM默认收集器,跟它显示的一样,它的最大优点是使用多个线程来扫描和压缩堆。缺点是在minor和fullGC的时候都会暂停应用的运行。并行收集器最合适用在可以容易程序停滞的环境使用,它占用较低的cpu因而能提高应用的吞吐。
使用方法:-XX:+UseParallelGC
3 CMS收集器
CMS是Concurrent-Mark-Sweep的缩写,并发的标记与清除。
这个算法使用多个线程并发地扫描堆,标记不使用的对象,然后清除它们回收内存。在两种情况下会使应用暂停(STW):
①当初次开始标记根对象时initial mark;
②当在并行收集时应用又改变了堆的状态时,需要它从头再确认一次标记了正确的对象finalremark。
这个收集器最大的问题是在年轻代与老年代收集时会出现的一种竞争情况,称为提升失败promotion failure。对象从年轻代复制到老年代称为提升promotion,但有时候老年代需要清理出足够空间来放这些对象,这需要一定的时间,它收集的速度可能赶不上不断产生的要提升的年轻代对象的速度,这时就需要做STW的收集。STW正是CMS想避免的问题。为了避免这个问题,需要增加老年代的空间大小或增加更多的线程来做老年代的收集以赶上从年轻代复制对象的速度。
除了上文所说的内容之外,CMS最大的问题就是内存空间碎片化的问题。CMS只有在触发FullGC的情况下才会对堆空间进行compact。如果线上应用长时间运行,碎片化会非常严重,会很容易造成promotion failed。为了解决这个问题线上很多应用通过定期重启或者手工触发FullGC来触发碎片整理。
对比并行收集器它的一个坏处是需要占用比较多的CPU。对于大多数长期运行的服务器应用来说,这通过是值得的,因为它不会导致应用长时间的停滞。但是他不是JVM的默认的收集器。
4 G1收集器
如果你的堆内存大于4G的话,那么G1会是要考虑使用的收集器。它是为了更好支持大于4G堆内存在JDK7 u4引入的。G1收集器把堆分为多个区域,大小从1MB到32MB,并使用多个后台线程来扫描这些区域,优先会扫描最多垃圾的区域,这就是它名称的由来,垃圾优先Garbage First。
如果在后台线程完成扫描之前堆空间耗光的话,才会进行STW收集。它另外一个优点是它在处理的同时会整理压缩堆空间,相比CMS只会在完全STW收集的时候才会这么做。
使用过大的堆内存在过去几年是存在争议的,很多开发者从单个JVM分解成使用多个JVM的微服务和基于组件的架构。其他一些因素像分离程序组件,简化部署和避免重新加载类到内存的考虑也促进了这样的分离。
除了这些因素,最大的因素当然是避免在STW收集时JVM用户线程停滞时间过长,如果你使用了很大的堆内存的话就可能出现这种情况。
5.1 定义
java虚拟机规范中定义方法区是堆的一个逻辑部分,但是别名Non-Heap(非堆),以与java堆区分。方法区存放已经被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据。
5.2 特点
线程共享
方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的,整个虚拟机只有一个方法区
永久代
方法区中的醒醒一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为永久代
内存回收效率低
java虚拟机规范对方法区的要求比较宽松,可以不实现垃圾收集。方法区中的信息一般需要长期存在,回收一边内存之后可能只有少量信息无效。对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载
和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。当方法区内存空间无法满足内存分配需求时,将抛出OutOfMemoryError异常.
5.3.1 定义
运行时常量池是方法区的一部分
方法区中存放三种数据:类信息,常量,静态变量,即时编译器编译后的代码。其中常量存储在运行时常量池中。.java文件被编译之后生成的.class文件中除了包含:类的版本,字段,方法,接口等信息外,还有一项就是常量池。
常量池中存放编译时期产生的各种字面量和符号引用,.class文件中的常量池中的所有的内容在类被加载后存放到方法区的运行时常量池中。
PS:int age = 21;//age是一个变量,可以被赋值;21就是一个字面值常量,不能被赋值;
int final pai = 3.14;//pai就是一个符号常量,一旦被赋值之后就不能被修改。
运行时常量池相对于class文件常量池的另外一个特性是具备动态性,java语言并不要求常量一定只有编译器才产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。
在近三个JDK版本(6,7,8)中,运行时常量池的所处区域一直在不断变化
jdk6时它是方法区的一部分,7他放到了堆内存中,8之后出现了元空间,他又回到了方法区。
我们一般在一个类中通过public static final 来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。当这个类被java虚拟机加载后,class文件中的常量就存放在运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern方法就能在运行期间向常量池添加字符串常量。当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器进行回收。
5.3.2特性
class文件中的常量池具有动态性。
java并不要求常量只能在编译时候产生,java允许在运行期间将新的常量放入方法区的运行时常量池中。String类中的intern()方法就是采用了运行时常量池的动态性。当调用intern方法时,如果池已包含一个等于此String对象的字符串,则返回池中的字符串,否则,将此String 对象添加到池中,并返回此String对象的引用。
5.3.3 可能抛出的异常
运行时常量池是方法区的一部分,所以会受到方法区内存的限制,因此当常量池无法在申请到内存时就会抛出OutOfMemoryError异常。
直接内存不是虚拟机运行时数据区的一部分,也不是jvm规范中定义的内存区域,但在jvm的实际运行过程中会频繁地使用这个区域,而且也会抛出OOM
在jdk1.4中加入了NIO类,引入了一种基于管道和缓冲区的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆里的DirectByteBuffer对象作为这块内存的引用来操作堆外内存中的数据。能够在一些场景中显著提升性能,因为避免了java堆和native堆中来回复制数据。
在JDK8,元空间的前身Perm区已被淘汰,在JDK7及之前版本中,只有Hotspot才有Perm区(永久代),他在启动时固定大小,很难进行调优,并且FullGC会移动类元信息。
在某些场景下,如果动态加载类过多,容易产生Perm区的OOM
比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多的类,经常出现致命错误:Exception in thread ‘dubbo client x.x connector' java.lang.OutOfMemoryError: PermGenspac
为了解决该问题,需要设定运行参数:
-XX:MaxPermSize= l280m
如果部署到新机器上,往往会因为JVM参数没有修改导致故障再现。不熟悉此应用的人排查问题时往往苦不堪言,除此之外,永久代在GC过程中还存储诸多问题。
故而在JDK8中使用了元空间代替了永久代,元空间在本地内存中分配,即只有本地内存空间足够,他就不会像永久代中java.lang.OutOfMemoryError: PermGen space
同样的,对永久代的设置参数PermSize和MaxPermSize也会失效。
在JDK8及以上版本中,设定MaxPermSize参数,JVM在启动时并不会报错,但是会提示:Java HotSpot 64Bit Server VM warning:ignoring option MaxPermSize=2560m; support was removed in 8.0
默认情况下,“元空间”的大小可以动态调整,或者使用新参数MaxMetaspaceSize来限制本地内存分配给类元数据的大小。
在JDK8里,Perm去所有内容变更位置:
①字符串常量移至堆内存
②其他内容包括类元信息,字段,静态属性,方法,常量等都移动至元空间。
元空间特色
①正常关闭:当最后一个非守护线程结束或调用了System.exi或通过其他特定于平台的方式,比如ctrl+c
②强制关闭:调用Runtime.halt方式,或在操作系统中直接kill(发送single信号)掉JVM进程。
③异常关闭:运行中遇到RUNtimeException异常等
在某些情况下,我们需要在JVM关闭时做一下扫尾的工作,比如删除临时文件,停止日志服务。为此JVM提供了关闭钩子来做这些事件。
Runtime类封装java应用运行时的环境,每个java应用程序都有一个Runtime类实例,使用程序能与其运行环境相连。
关闭钩子本质上是一个线程(也称为hock线程),可以通过Runtime的addshutdownhock (Thread hock)向主jvm注册一个关闭钩子。hock线程在jvm正常关闭时执行,强制关闭不执行。
对于在jvm中注册的多个关闭钩子,他们会并发执行,jvm并不能保证他们的执行顺序。
转载于:https://www.nowcoder.com/discuss/151138?type=1
部分修改内容参考:https://www.nowcoder.com/discuss/151138?type=1