Java虚拟机(二):JVM内存模型和OOM

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 当出现这个错误的时候,其实已经说明,应用程序已经达到其可以启动的线程数量的极限了

猜你喜欢

转载自blog.csdn.net/YINZONGCHAO/article/details/106516738