JavaJava 杂谈java学习之路

深入JVM(一)java内存区域与内存溢出异常

2019-08-01  本文已影响109人  唯有努力不欺人丶

java与c++之间有一堵由动态分配和垃圾收集技术所围成的高墙,墙内的想出去,墙外的人却想进来。

深入了解JVM虚拟机

重申下个人观点:我说的都是别人讲过的东西,老生常谈,然后会加一下自己的理解和看法,甚至有的时候有些东西也是自己的猜测。可能会有错的,欢迎指出,有异议的也欢迎交流。然后我之所以写出来一来是我觉得单独看一遍书其实理解的不深,自己打一遍,而且为了让文章言之有物我还要自己推敲琢磨,这样对我自己的理解本身是一种好处。另外就是如果读者看到了,觉得有帮助,那也算是一件好事。毕竟我是打在笔记本里自己看和打在简书里大家看,对我而言是差不多的!还有!我也是在学习阶段,我也是小白,刚刚也说了也会有错误。但是我不觉得一定要特别特别厉害才有资格写文章(最近有人问我整天写文章能挣钱咋的,所以针对这种看法我做一下解释。)

内存到底谁来管理好?

因为这本书都是对比JAVA和C++来讲解的,所以我也多少说两句。其实我不太有发言权,因为我目前为止只接触过java,不过各种资料看了不少,大概明白c++的机制是内存完全由程序员管理,也就是从创建到销毁都要维护。而java省事多了,在虚拟机自动内存管理机制的帮助下,一般都不需要去考虑内存。
但是!又到了举例子的时候了:打个比方,如果一个公司老板很不信任手下,啥事都要管,都要问。这样可能会弄得老板自己很累啊,员工也觉得麻烦啊,但是起码老板是很有权力和对情况的掌握的!如果有谁有点啥问题,作风不好啊,用公款大保健了啊,很容易就被发现了,然后老板惩罚或者换人,反正出不了什么大事。但是如果一个公司的老板很信任手下,啥事都让经理,副经理啥的处理。如果你眼光好,没啥问题,指不定因为员工知恩图报,你又清闲又盈利呢!但是假如你信任的是个白眼狼,好么,可能公司都赔没了但你却最后一个才知道。你不能单纯的说啥都管老板好或者不好,也不能说甩手老板好或者不好,各有利弊。同样你不能说C++的自己管理内存好不好,也不能断言java的自动内存管理机制好不好。

java运行时数据区域

java虚拟机会在执行java程序的时候,把它管理的内存划分成不同的数据区。这些区域各有用途,各有创建和销毁的时间。有的区域随着虚拟机进程创建,有的用户的行为出发的创建。(不夸张的说话这个图用了半个小时,最后还是用的微信图片编辑)


虚拟机运行时数据区

其实这个也没啥,应该学过java的或者有一丢丢了解的就能说出来,就是堆栈方法区。只不过书上把栈说的更细致一点,又本地方法栈还有虚拟机栈。另外有个程序计数器。
然后方法区和堆是线程共享的,我特意选个绿色背景。共享的颜色嘛。

程序计数器

程序计数器是一块比较小的内存空间,。它可以看作是当前线程所执行的字节码文件的行号指示器。在虚拟机的概念模型里(仅仅是概念模型,各种虚拟机实际上可能会用更高效的方式去实现),字节码解释工作时就是通过改变这个计数器的值来选取下一条指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于这个程序计数器来完成的。

其实我个人感觉,这个应该是有点类似于一个指针的功能。不过书的原文里没这么讲。这个属于猜测。但是我们可以逐字的去读。字节码文件的行号指示器改变这个计数器的值来选取下一条指令分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖于这个程序计数器来完成。我感觉,咱们用的编译器,我个人习惯用eclipse,他在代码的执行的时候,怎么跳转,执行到哪行代码,分支循环之类的,我不知道底层实现的原理,但是感觉应该就是有一个类似于程序计数器这样的东西吧。反正我是这么理解的,有不同意见的欢迎交流,我要是理解错了帮忙指出来。
然后java虚拟机的多线程是轮流切换并分配处理器执行时间的方式实现的。也就是任意时刻一个处理器(多核就是一个内核)只会处理一条线程中的指令。为了防止你执行一半时间到了,下次再回来不知道你执行到哪里了,所以要给你一个单独记录,这个就是程序计数器。所以程序计数器要线程之间个不影响,线程私有的内存。
这个更好理解了,感觉没啥解释的必要都。自己跑到哪了不记住,下次回来找不到要么少跑几行命令,要么重复跑几行命令。那还得了?
最后说明的一点:如果线程执行的是java方法,则这个计数器记录的是虚拟机字节码指令的地址,但如果执行的是本地方法,则计数器值为空(undefined)。

java虚拟机栈

与程序计数器一样,这个虚拟机栈也是线程私有的。它的生命周期与线程相同。咱们总听说的内存分堆栈,这个栈就是虚拟机栈,或者说虚拟机栈中的局部变量表部分。
其实虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧。用来存储局部变量表。操作数栈,动态链接,方法出口等(我其实也就知道第一个局部变量表)。每一个方法从调用到执行完成,就对应着一个栈帧从入栈到出栈的过程。
咱们继续说局部变量表吧。局部变量表存放的是编译器可知的各种基本数据类型,对象的引用,和returnAddress类型(这个我也第一次看到,不过看英文大概能理解是啥玩意,指向的是字节码指令的地址。结合上文我们程序计数器就是指针,指向字节码指令地址)。
书中这里还说到了占用空间问题,我也跟着说说。long和double占用两个局部变量空间,别的数据类型都是一个。局部变量表所需要的内存空间在编译器就能确定。当进入一个方法,需要在栈帧中分配多大局部变量空间是完全确定的。方法运行期间也不会改变局部变量表大小。
然后这有两个异常说一下:StackOverflowError(栈溢出)。这个官方的解释是线程请求的栈深度大于虚拟机所允许的深度。听着高大上云里雾里,但是我实际中不小心用了死循环就报这个错。我大概理解为jvm处理的时候栈里先执行这个方法,分一块地方。因为里面递归,还得执行这个方法,又分一块地方,一直分一直分栈里没地方了,然后就报这个错了。告诉你栈溢出了。
还有一个异常:OutOfMemoryError(内存不足错误)。这个打个比方,人家jvm一共1G内存,分给虚拟机栈500M内存(有的可以动态扩展,有的固定长度。不同虚拟机配置不一样)。但是别管咋地,你一下子跟虚拟机说你要10个G内存。那虚拟机自己卖了也不够给你的啊,所以就告诉你一声OutOfMemoryError,内存不足了,给不了你了。

本地方法栈

跟着书走,接下来说本地方法栈。与虚拟机栈差不多,区别就是虚拟机栈是为执行java方法提供服务的,本地方法栈是为了Native方法(本地方法)服务的。也会抛出那两个异常,就不多说了。

对于大多数应用来说,java堆是虚拟机所管理的内存中的最大的一块。java堆是被所有线程所共享的区域。在虚拟机启动时创建。这个区域就是放对象实例的。几乎所有的对象都在这里分配内存。但是!!!重点是几乎所有,也就是不那么绝对的所有的对象都分配在堆上。
java堆是垃圾收集器管理的主要区域。因此也叫GC堆(下一篇文章就是讲垃圾回收的)。从内存回收的角度,因为垃圾收集是分代的,所以java堆也可以细分:
新生代和老年代。再细致一点Eden,Form Survivor,To Form Survivor等(如果你现在不懂看完下一篇垃圾收集的就懂了)。
然后堆内存不足的话,也会报OutOfMemoryError异常。

方法区

方法区和堆一样,也是线程共享的。它存储的是被虚拟机加载的类信息,常量,静态变量。也就是编译器编译后的代码等数据。
据说java虚拟机规范把方法区描述为堆的一个逻辑部分。但是它还叫Non-heap(非堆)。所以说它不属于堆。也有人习惯把方法区叫做”永久代“。其实也不是,那有什么永久的。java虚拟机规范允许方法区的不进行垃圾回收。因为一般情况方法区里的很少需要垃圾收集,但也不是可以完全不收集的!
然后这个方法区无法满足内存分配,也会抛出OutOfMemoryError异常。

OutOfMemoryError异常

内存不足,这个除了程序计数器剩下的都可能会发生这个异常。然后这里简单的讲解一下出现的环境(书里就举了几个例子。我估计还有别的情况也会异常,自己研究吧)
堆内存溢出:
一个不断创建对象的死循环,然后直接跑的我笔记本要炸了,过了一会儿报错了。啧啧

public static void main(String[] args) {        
    
    List<Object> list = new ArrayList<Object>();
    
    while(true) {
        list.add(new Object());
    }
}

栈内存溢出:
这里有个问题,栈是有两个异常的,而且有重叠部分、当栈空间不足无法分配,到底是内存小还是使用的栈空间大你不知道啊。所以说,反正书里作者多次尝试都是StackOverflowError这个异常。因为不断创建线程可能会导致操作系统假死,所以我这里就跑一下死递归得了。

public static void main(String[] args) {                            
        new demo().stack();
    }   
    public void stack() {
        stack();
    }

方法区常量池溢出:
我笔记本跑了两分钟多才出异常,我差点以为是没起来~~啧啧,太吓人了。

public static void main(String[] args) {        
        
        List<Object> list = new ArrayList<Object>();
        System.out.print("跑了没?");
        int i = 0;
        while(true) {
            list.add(String.valueOf(i++).intern());
        }
    }

反正大概这章就简单的介绍了下内存的区域划分,和各个区域的特性,作用啥的。然后我画的丑萌丑萌的图是重点。堆,方法区两个绿色的线程共享。剩下的私有的,线程隔离。然后这个程序计数器和后文的引用计数器别混了。就是相当于一个走到哪里了的书签或记录。

今天就到这吧,然后全文手打,如果帮到你了麻烦点个喜欢点个关注支持一下。最近今天在看jvm,如果有问题或者你也在学习可以一起交流下。欢迎评论私信。

上一篇下一篇

猜你喜欢

热点阅读