JVM内存模型

一、什么是JVM

JVM 是 Java Virtual Machine(Java虚拟机)的缩写,是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机是整个 Java平台的基石,是 Java 技术用以实现硬件无关与操作系统无关 的关键部分,是 Java 语言生成出极小体积的编译代码的运行平台,是保障用户机器免于恶意代码损害的保护屏障。

二、JVM 内存模型(JDK1.8)

JVM内存模型主要是指运行时数据区的内存结构,根据Java虚拟机规范,JVM运行时数据区域分为五大数据区域。image.png

2.1 PC 寄存器

当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。JVM中唯一一块没有规定任何OutofMemoryError的区域。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。

在任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法(Current Method),如果这个方法不是 native 的,那 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值是 undefined。PC 寄存器的容量至少应当能保存一个 returnAddress 类型的数据或者一个与平台相关的本地指针的值。

2.2 方法区

方法区/永久代是被所有线程共享区域,用于存放已被虚拟机加载的类信息、常量、静态变量等数据。永久代的垃圾回收和老年代的垃圾回收是绑定的,一旦其中一个区域被占满,这两个区都要进行垃圾回收。

在JDK1.7之前运行时常量池逻辑包含字符串常量池,存放在方法区,此时hotspot虚拟机对方法区的实现为永久代

在JDK1.7字符串常量池被从方法区拿到了堆中,这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区,也就是hotspot中的永久代

在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之,这时候字符串常量池还在堆, 运行时常量池还在方法区,只不过方法区的实现从永久代变成了元空间(Metaspace)

2.2.1 移除永久代的影响

永久代在JDK8中被删除,这项改动是很有必要的,因为对永久代进行调优是很困难的。永久代中的元数据可能会随着每一次Full GC发生而进行移动。并且为永久代设置空间大小也是很难确定的,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等。

默认情况下,元空间的最大可分配空间就是系统可用内存空间。因此,我们就不会遇到永久代存在时的内存溢出错误,也不会出现泄漏的数据移到交换区这样的事情。最终用户可以为元空间设置一个可用空间最大值,如果不进行设置,JVM会自动根据类的元数据大小动态增加元空间的容量。

注意:永久代的移除并不代表自定义的类加载器泄露问题就解决了。还必须监控内存消耗情况,因为一旦发生泄漏,会占用大量的本地内存。

2.3 虚拟机栈

每个方法执行都会创建一个栈帧,用于存放局部变量表,操作栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。

通常说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型(boolean、byte、char、short、int、float、long、double)及对象引用(reference 类型)和指令地址(returnAddress 类型)。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。

常见的的两种异常StackOverFlowError和OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。

2.4 本地方法栈

本地方法栈与虚拟机栈的作用十分类似,不过本地方法是为native方法服务的。部分虚拟机(比如Sun HotSpot虚拟机)直接将本地方法栈与虚拟机栈合二为一。与虚拟机栈一样,本地方法栈也会抛出StactOverflowError与OutOfMemoryError异常。

2.5 堆区

对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。

堆区是垃圾回收的主要区域,通常情况下分为两个区块-年轻代和老年代。年轻代又分为Eden区(存放新创建对象),From survivor区和To survivor区(两个survivor区保存gc后幸存下的对象)。默认情况下各自占比 8:1:1。

Java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者被其它线程所引用。

#在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例

#JVM运行时堆的大小
-Xms,堆的最小值
-Xmx,堆空间的最大值

#新生代堆空间大小调整
-XX:NewSize新生代的最小值
-XX:MaxNewSize,新生代的最大值
-XX:NewRatio,设置新生代与老年代在堆空间的大小
-XX:SurvivorRatio,新生代中Eden所占区域的大小

#永久代大小调整
-XX:MaxPermSize 4.其他
-XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收

OutOfMemoryError报错及解决方法

java.lang.OutOfMemoryError:java heap space
这种是Java堆内存不够,一个原因是内存真不够,另一个原因是程序中有死循环。
解决方案:
--如果是Java堆内存不够的话,可以通过调整JVM下面的配置来解决:-Xms、-Xmx

java.lang.OutOfMemoryError:GC overhead limit exceeded
这是JDK6新增错误类型,当GC为释放很小空间占用大量时间时抛出;一般是因为堆太小,导致异常的原因,没有足够的内存。
解决方案:
--查看系统是否有使用大内存的代码或死循环;
--通过添加JVM配置,来限制使用内存:-XX:-UseGCOverheadLimit

java.lang.OutOfMemoryError: PermGen space
这一部分用于存放Class和Meta的信息,Class在被Load的时候被放入PermGen space区域。所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。
解决方案:
--这种是永久代内存不够,可通过调整JVM的配置: -XX:MaxPermSize、-XXermSize

java.lang.OutOfMemoryError: Direct buffer memory
可能原因是本身资源不够或者申请的太多内存。
解决方案:
--如果不是内存泄漏的话,可以使用参数-XX:MaxDirectMemorySize参数,或者-XX:MaxDirectMemorySize

java.lang.OutOfMemoryError: unable to create new native thread
可能原因是系统内存耗尽,无法为新线程分配内存或者创建线程数超过了操作系统的限制。
解决方案:
--排查应用是否创建了过多的线程。通过jstack确定应用创建了多少线程
--调整操作系统线程数阈值。操作系统会限制进程允许创建的线程数,使用ulimit -u命令查看限制。某些服务器上此阈值设置的过小,比如1024。一旦应用创建超过1024个线程,就会遇到java.lang.OutOfMemoryError: unable to create new native thread问题。如果是这种情况,可以调大操作系统线程数阈值。
--增加机器内存。如果上述两项未能排除问题,可能是正常增长的业务确实需要更多内存来创建更多线程。如果是这种情况,增加机器内存。
--减小堆内存。一个老司机也经常忽略的非常重要的知识点:线程不在堆内存上创建,线程在堆内存之外的内存上创建。所以如果分配了堆内存之后只剩下很少的可用内存,依然可能遇到java.lang.OutOfMemoryError: unable to create new native thread。考虑如下场景:系统总内存6G,堆内存分配了5G,永久代512M。在这种情况下,JVM占用了5.5G内存,系统进程、其他用户进程和线程将共用剩下的0.5G内存,很有可能没有足够的可用内存创建新的线程。如果是这种情况,考虑减小堆内存。
--减小线程栈大小。线程会占用内存,如果每个线程都占用更多内存,整体上将消耗更多的内存。每个线程默认占用内存大小取决于JVM实现。可以利用-Xss参数限制线程内存大小,降低总内存消耗。例如,JVM默认每个线程占用1M内存,应用有500个线程,那么将消耗500M内存空间。如果实际上256K内存足够线程正常运行,配置-Xss256k,那么500个线程将只需要消耗125M内存。(注意,如果-Xss设置的过低,将会产生java.lang.StackOverflowError错误)。

java.lang.StackOverflowError
这也内存溢出错误的一种,即线程栈的溢出,要么是方法调用层次过多(比如存在无限递归调用),要么是线程栈太小。
解决方案:
--可以通过优化程序设计,减少方法调用层次;调整-Xss参数增加线程栈大小。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码的搬运工-小刘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值