Java虚拟机:JVM内存模型和OOM
Posted Java程序员成长之路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java虚拟机:JVM内存模型和OOM相关的知识,希望对你有一定的参考价值。
说在开头的话:
最近作者因为新入某厂,为了熟悉新环境,新业务,因此相对来说稍微有点忙,估计有注意到的大家也发现了作者更新文章的时间大多是在周末,2而上周因为作者好朋友的婚礼,所以也没时间来继续更新了,在这里说声抱歉啦,这周补更,而且是干货哦~
记得17年初刚开始写文章的时候,在作者的微信公众平台(growth_of_java)写了一篇关于JVM内存模型简述的文章,当时整体很浅,也只是简单介绍下,这次,作者再好好把自己的经验和大家一起分享下吧,多多欢迎大家踢馆哈~
JVM内存模型
相信有做过简单jvm调优的同学肯定知道几个参数:-Xms -Xmx -Xmn -Xss -XX:MaxPermSize -XX:MaxMetaspaceSize ,那么,作者就以一张图再配上这几个参数来向大家描述下分别在JDK7与JDK8之间JVM内存模型本身以及区别
-Xmn EdenSpace,Survivor1,Survivor2三区总和(Young区)的大小
-Xms 最小heap大小
-Xmx 最大heap大小
-Xss 线程私有栈大小
-XX:MaxPermSize 最大永久代(方法区)大小
-XX:MaxMetaspaceSize 最大元空间大小
从上图可以看出,JDK7和JDK8都内存模型中都存在堆和栈两部分,其中7中额外存在一块永久代,存放相应的数据;8中去除了7中的永久代,重新定义了元空间来存放相应的数据.(这里的栈不仅仅只是线程栈,还有本地方法栈)
上图表示的是JDK7和JDK8中对应的永久代以及元空间具体存放的数据内容,其实两者之间存放的数据是差不多的,但是两者之间有一个最本质的区别,那就是永久代在逻辑上是属于堆的一部分,但是为了与堆区分,因此也叫“非堆”,因此,方法区本身就是在虚拟机内部的;而元空间,其实是存在于本地内存的,其最大值也受系统内存大小的影响。
下面,我们来看一段代码
public class HelloWorld {
private static Logger logger = LoggerFactory.getLogger(HelloWorld.class);
public static void main(String[] args) {
new Thread(()->{new HelloWorld().sayHello("Hello World!");
try {
new CountDownLatch(1).await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
public void sayHello(String message){
SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.YYYY");
String today = formatter.format(new Date());
logger.info(today + ": " + message);
}
}
而这段代码在内存中的存放如下
那么,为什么要把永久代替换成元空间这样的实现呢?
其实,如果你有这样一个疑问,你就可以从自己日常开发中去思考下,在当时使用JDK7的时候常常会遇到什么问题,尤其是在做Web开发的时候,答案很明显:java.lang.OutOfMemoryError:PermGen 相信这个异常大家都很熟悉,用过JDK7的人都遇到过,而且特别频繁,但是当你将7换掉,使用8的时候,虽然对应的,会有java.lang.OutOfMemoryError:Metaspace,但是,这个错误出现的概率大大得降低了。因此,这其实就是为什么永久代被移除的原因之一
其次,由于JRockit VM没有永久代的概念,为了方便Hotspot JVM与其融合,自然而然的,没有必要再有永久代了,另外,根据官方描述,在实际开发工作中,想对永久代调优其实是比较困难的,不像堆或者是栈。
OOM - java.lang.OutOfMemoryError: xxx
相信这个错误大家在日常开发中会常常遇到,那么这里小编也向大家详细介绍下这个错误的由来的情况,以便大家在遇到问题的时候可以有排查的方向
其实,从名称中可以看出,OOM整体分为两类,堆内OOM以及非堆OOM
堆内OOM又分为Java heap space 以及 GC overhead limit;非堆OOM也分为Metaspace以及Unable to create new native thread
Java heap space 简单说来就是,当程序运行时堆内存达到设置的Xmx或者超出系统可用内存时,即会出现这个错误,但是具体产生的原因,从根本上来说有两种:内存使用量增大、内存泄漏
内存使用量增大 在业务大量增加,而导致内存需求超出业务预期,达到Xmx的值时
内存泄漏 在堆内,常见的内存泄漏其实会是由于逻辑代码上的失误而导致的Java对象泄漏
GC overhead limit exceeded 其根本原因是当GC线程花费了大量的时间来进行垃圾回收,但是却并没有起到什么卵用,此时即会出现这个异常,而默认为当应用程序花费来98%的时间来做GC却回收不到2%的堆内存
Metaspace 作为非堆内存中十分重要的一块,它在OOM的产生情况的表现上也是十分不足,作者结合工作中遇到的一些情况,大致进行了一下简单的分类
内存碎片化 由于Metaspace是通过Chunk List来管理,同时不断被回收(Enable ClassUnloading的情况下),因此自然会面临内存碎片化的问题
频繁加载js,groovy脚本 由于Java允许自定义classloader,因此在频繁加载js、groovy脚本时,会不断触发defineClass操作,而一旦业务逻辑有缺陷,就会造成metaspace猛增,从而导致OOM
MethodHandle 同上,由于自定义classloader或者是动态加载js、groovy脚本的时候,会通过构造MethodHandle同时每次都会产生一个新的MemberName,很容易导致metaspace猛增
由于不恰当得使用反射 当通过Method.invoke来调用反射方法时,JDK底层一班是先通过NativeMethodAccessorImpl来实现,但是当调用次数超过-Dsun.reflect.inflationThreshold的值时,会自动切换为GeneratedMethodAccessorXXX来实现,而为了反射的高效调用,JDK会对每个Method的反射调用都构造一个GeneratedMethodAccessorXXX类,并且每个GeneratedMethodAccessorXXX类都对应一个ClassLoader实例,当在对线程的环境下,很明显,这种现象将被恶化,此时很容易就会造成metaspace达到阈值造成OOM
Unable to create new native thread 当出现这个错误的时候,其实已经说明,应用程序已经达到其可以启动的线程数量的极限了
以上是关于Java虚拟机:JVM内存模型和OOM的主要内容,如果未能解决你的问题,请参考以下文章