• java虚拟机入门(二)-探索内存世界


        上节简单介绍了一下jvm的内存布局以及简单概念,那么对于虚拟机来说,它是怎么一步步的让我们能执行方法的呢:

        1.首先,jvm启动时,跟个小领导一样会根据配置参数(没有配置的话jvm会有默认值)向大领导操作系统申请内存

        2.jvm小领导这时候已经有资源了,要向下级分配资源,jvm是个清廉的领导,一点都没贪,根据配置将堆,栈,方法区内存分配好。

        这时候jvm内存模型已经都有了,这些堆栈方法区等干活的拿到资金后,开始做下一步的准备工作,

        3.jvm将类class信息加载入方法区,包含一些静态变量,代码块和常量信息(上节也说了,其实运行时常量池在逻辑上还是算方法区的,身在堆营心在方)。

        4.万事具备,让我们走起来吧,这时候就可以开始执行方法了,方法压栈,堆中生成对象,开始java不归路

    在jvm的角度这就算是系统跑起来的全部流程了,对于我们程序员来说,第四步才算是开始,我们会无限重复压栈出栈,对象生成,垃圾回收,而如何在有限的空间里,做更多的事情,才是一个程序员该考虑的问题,当然,我们要想优化,首先必须要知道他是怎么玩的。

        上节我们已经知道堆是线程共享的,栈是线程私有的,栈在线程运行时会分配一块栈帧,这块是线程独占的,而堆里面的对象都是线程共享的,只要有引用就可以随意访问和修改(final对象也不例外,final只是规定了这个对象通过这个引用无法改变,但是如果final引用的对象被其他普通引用到,依旧是可以改的)。java基础还行的应该都知道,栈上的运行速度远高于堆的,原因很多,如:

          1.堆内存是在运行时动态分配的,所以需要考虑并发安全问题(后面会讲到通过cas和分配缓冲方式),而堆在线程创建时都会分配一块tlab缓存,不会冲突,同时分配的tlab内存不够用时,就需要再次申请分配一块tlab。

          2.栈不需要释放缓存,垃圾回收都不需要,因此会快很多

          3.栈通过JIT即时编译优化,亲儿子有cpu指令加持就是牛

          4.栈可以用到cpu的高速缓存

        其实总结下来就两点:cpu加持,内存简单。既然他这么牛,那还用啥堆啊,全部放在栈里面他不香吗。其实我认为栈之所以快的一部分原因也是因为对象少,cpu爸爸还能照顾得过来,而且无论什么东西,只要多了自然而然效率会下降。如果像堆中,动不动搞个个把g的对象,咋的也玩不转。况且,很多对象是跨线程共享的对象,这就得涉及到垃圾回收,这样玩的话,还是咱们认识的青涩纯洁的小栈(这里绝对没有碰瓷肖战的意思)了吗,当然这些都是我的猜想,要理解为啥,咱还是得先看看堆是咋设计的,堆有啥是栈不可替代的呢:

        被划分为新生代和老年代(G1也是如此,只不过物理内存没有划分那么开),新生代又被划分成Eden区和Survior区,Survior区又分为from区和to区(其实这俩没有区别,只是形象的表示是从一块区域复制到另一块区域)。

    那么堆为什么要这么设计呢,这就要牵扯到垃圾回收了,由于堆内存共享的,很多变量被其他对象引用(栈是线程私有的,因此用完直接可以把这块内存清掉就行了),因此,既然我们不能像栈那样潇洒,那也不能任他自由生长,因此必须要对不用的对象做回收处理,在介绍垃圾回收之前我们还是先来了解一下我们常见的几个内存溢出的场景吧:

        栈溢出:

              栈的大小可以通过 -Xss设置,默认为1m,很多同学觉得这个值太小了,其实这个1m是设置的每个线程分配1m,jvm不支持设置整个栈的大小。一般来说,栈的内存溢出主要有两种情况:

              1. java.lang.StackOverflowError :这种很容易实现无限递归就可以了,我们先试一下:

       public static void main(String[] args) {
            get();
        }
    
        static void get(){
            get();
        }

    执行之后很快就会出现:Exception in thread "main" java.lang.StackOverflowError

    为什么我明明没有任何对象生成,却依然内存溢出了呢,原因还是要理解栈的内存结构,每次调用方法都要将信息入栈,退出方法时出栈,因此在无限调用时只进不出,默认1m内存自然很快就占满了。一般来说,出现这种情况的时候我们就要考虑代码中是否有死递归的情况发生了。

              2.OutOfMemoryError: 这种就很复杂,由于栈无法限制整体内存大小,因此想要占满栈必须占满物理内存区域,可是这样电脑也卡的不行不行的了,尝试之前我得先保存一下了。

     说实话这个场景太难复现了,尝试了很多次,要么电脑卡死重启,反正就是出不来,贴上代码大家试一下,或者有大佬看到可以指点一番:

    public static void main(String[] args) {
            for (int i=0;i<1000000000;i++){
                new get().start();
            }
        }
    
        static class get extends Thread{
            @Override
            @SneakyThrows
            public void run() {
                Thread.sleep(20000L);
            }
        }

    其实原理就是每个线程都会分配一块固定的内存区域,因此,当同时运行线程数量很多时,就会占用很多内存,导致出现oom。

        堆溢出:

               堆内存溢出很简单,由于堆内存是可以限制的只需要设置 -Xms,-Xmx的参数大小再添加一个大对象就可以复现(对象都是在堆中分配),我们先上代码:

    public static void main(String[] args)
       {
           String[] strings = new String[100*1000*1000];
       }

    只要设置-Xmx100m以下,就会出现java.lang.OutOfMemoryError: Java heap space,堆内存溢出嘛,这个很好理解,其实在真正生产环境我们不可能会有这样的大对象能直接塞满堆得,真出现了这种情况,赶快检查一下数据库操作的代码,因为没准过不了多久就会出现删库跑路的新闻,哈哈。但是原理都是一样的->没有垃圾回收的对象太多,堆放不下了。

             方法区溢出:

                   方法区内存主要包含运行时常量池,class信息,那我们在什么情况下会出现内存溢出的场景呢,首先运行时常量池在jdk1.8以后物理内存被挪到堆内存中去了(现在互联网公司基本上都是1.8以上的版本,太低的版本就不演示了,知道就行了),那么在1.8以后我们主要考虑的还是class信息过多,有的同学会问了,class信息大小不是在系统启动之初就确定了吗,你还能在项目跑的时候加一个类进去,答案是可以,不过当然不是运行的好好地,我写个类编译完然后塞到jar包中,相信动态代理很多人都知道,但是原理可能不太清楚,其实动态代理做的就是在运行的时候,根据不同场景,动态的生成class,那么我们就来试试吧:

        

    public static void main(String[] args) {
            while (true) {
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(MethodOOM.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable {
                        return arg3.invokeSuper(arg0, arg2);
                    }
                });
                enhancer.create();
            }
        }
    
        public static class MethodOOM {
    
        }

    网上copy了一下动态代理的代码,加了个死循环,无限生成class,再把方法区内存区域设小一点,很快就会出现java.lang.OutOfMemoryError: Metaspace。

          直接内存溢出:  

        直接内存上节也说了,其实并不属于jvm管理,但是当我们在使用nio如常用的netty,java自带的unsafe都是可以操作到直接内存的,我们可以通过设置 MaxDirectMemorySize参数限制直接内存大小,一般来说直接内存很难排查(毕竟不归jvm管),但是正因为如此,当我们在出现大量oom,但是dump文件很小,很难看出问题时,就可以考虑排查直接内存的影响了。

    总结:        

    想当年被这些玩意坑过很多次,当时也不知道这些参数到底代表什么,只是出现这个问题就百度,网上很多只给了答案,但是每个系统占用内存是不一样的,所以其实是没有标准答案的,因此后来就研究了很久关于这些jvm如何优化,本来很高深的东西学着学着好像很简单,但是再深入,又会觉得未知的越来越多,或许正是这些矛盾让我们更加享受去钻研这些看似平时用不到的东西吧。

  • 相关阅读:
    存储过程分页
    连接数据库
    绑定数据
    有关最近做的项目中用到的日期控件
    循环累加 由for _foreach
    打印gridview 中的所有内容
    list<>操作
    创建表两个主键外键~~~
    连接数据库sql server 2005
    WPF控件编程
  • 原文地址:https://www.cnblogs.com/gmt-hao/p/13653218.html
Copyright © 2020-2023  润新知