对于学过C++
的开发者而言,他们对内存的分配与回收确定不陌生,由于他们要对每个对象负责(从建立到结束
)。可是对于Java程序员来讲,就不须要考虑那么多,由于虚拟机的内存管理机制
能够帮助咱们自动的管理内存
,咱们再也不须要为每个new操做去写配对的delete/free代码 。html
既然虚拟机都这么方便了,那么咱们为何还要学内存管理呢,这不是自讨苦吃么,事实上,虚拟机的自动内存管理确实能帮助咱们减小内存泄漏和内存溢出
的状况;可是也正由于咱们把内存的控制权交给了虚拟机,一旦出现内存泄漏和内存溢出的问题,若是咱们不了解虚拟机是怎么使用的内存的,那么咱们该怎么解决呢?因此,咱们很是有必要学习内存管理,学习它不是为了本身控制管理内存,而是在出现问题的可以有效的定位并予以解决。java
因此,让咱们一块儿来学习吧。程序员
Java虚拟机将Java程序执行的区域称为运行时数据区
,根据各自功能不一样将运行时数据区划分为若干个不一样的区域,具体分为两大块,线程共享
部分和线程私有
部分。线程共享部分能够分为堆
、方法区
(jdk1.8后这块区域被称为元空间
);线程私有部分能够分为虚拟机栈
、本地方法栈
和程序计数器
。算法
上述的这些区域都有各自的用途,下面让咱们一个个来学习学习他。缓存
程序计数器
是一块较小的内存空间,它能够看作是当前线程所执行的字节码的行号指示器
,字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支
、跳转
、循环
、异常处理
、线程恢复
等基础功能都须要依赖这个计数器来完成。安全
这是《深刻理解Java虚拟机》书籍对程序计数器的介绍,事实上,在此基础应该补充上,程序计数器是线程私有,在执行Java方法时有值,可是在执行native方法时,程序计数器值为空。markdown
有没有看懵,懵了也不要紧,下面咱们抽出程序计数器的特色,并介绍每一个特色的来源及做用。多线程
首先,为何线程私有呢,咱们都了解Java虚拟机的多线程是经过轮流切换
、分配处理器
的执行时间的方式来实现的,也就是说,在同一时刻一个处理器内核只会执行一条线程,处理器切换时并不会记录上一个线程执行到那个位置,因此为了线程切换后依然可以恢复到原位,每条线程都须要有各自的独立的程序计数器
,计数器之间互不影响,独立存储。并发
这个特色列出来好像有点白痴,咱们在上面都已说了它是行号计数器,那确定是有值啊,那么咱们还要单独列出来呢,咱们单独列出来一方面是为了与执行native方法比较,另外一发面我是想解释下线程执行字节码时,这个行号指示器究竟是个啥?好吧,通俗来说,JAVA代码经javac编译后的得字节码在未通过JIT(实时编译器)编译前,其执行方式是经过“字节码解释器”进行解释执行的,其工做原理就是为解释器读取装载入内存的字节码,按照顺序读取字节码指令,这个过程就是行号指示器在不断变化的过程。当读取一条指令后,就讲该指令“翻译”成固定的操做,并根据这些操做进行分支、循环、跳转等流程。jvm
当执行native本地方法
时,程序计数器是空的,这是由于native方法是java调用本地的C/C++库
,能够近似的认为native方法至关于C/C++暴露给java的一个接口
,java经过调用这个接口从而调用到C/C++方法。因为该方法是经过C/C++而不是java进行实现。那么天然没法产生相应的字节码,而且C/C++执行时的内存分配是由本身语言决定的,而不是由JVM决定的。
NOTE:学到这里,相信你对程序计数器已经了解的的差很少了,可是你可能还存在这样的疑惑,程序计数器占用的内存那么小,会不会抛出内存溢出错误OutOfMemorryError
,别担忧,不会出现错误的,既然程序计数器存储的是字节码文件的行号,那么程序字节码执行的范围确定是已知的,在虚拟机将字节码文件加载进内存时就已经分配一个绝对不可能的溢出的内存,为啥会提早知道,由于字节码文件包含的有相关信息,若是想要更加具体的了解,能够看看个人上一篇文章,认识Class文件结构
好了,学习完程序计数器,咱们接下来学习线程私有的另外一部份内容:Java虚拟机栈
。
虚拟机栈描述的是Java方法执行的内存模型
,每一个方法被执行的时候,Java虚拟机都会同步建立一个栈帧
用于存储局部变量表
、操做数栈
、动态连接
、方法出口
等信息。每个方法被调用直至执行完毕的时候,就对应着一个栈帧从入栈到出栈的过程。
看到上面这么长的定义可能有点懵逼,栈帧是个啥,里面存的都是些啥玩意,我学它干啥,搞得挺痛苦的。莫慌,咱们一个个解释,看完个人解释后绝对让你喊出“真香”。
首先,既然虚拟机栈描述的是Java方法的内存模型,那咱们就认为他是存储Java方法集合
的内存,而栈帧就能够认为集合中的一个方法,方法间的调用就对用着栈帧的调用,当执行一个方法,就将该方法的栈帧压入栈顶,方法执行完就退出栈,也即从方法集合中去掉。
抽象?来一张图看看
虚拟机栈里存储的是一个个栈帧,栈帧里面包含啥啊?下面,咱们下先看一张图来直观感觉下
局部变量表
是一组变量值存储空间,用于存放方法参数
和方法内部定义
的局部变量。在Java程序编译为Class文件时就在方法的code属性的max_locals数据项中肯定了该方法所须要分配的局部变量表的最大容量。局部变量能够存放基本数据类型
(boolean、byte、char、short、int、float、long、double)和对象引用类型
(reference)。
局部变量是以变量槽
(Slot)为单位,每一个槽的容量为32位
,因此对于小于32位的类型占用一个变量槽,64位长度的long和double类型的数据会占用两个变量槽。
JVM会为局部变量表中的每个slot都分配一个访问索引
,经过这个索引就能够成功的访问到局部变量表中的指定局部变量值。当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照声明顺序被复制到局部变量表中的每个slot上。
Note:栈帧中的局部变量表中的槽位是能够重复利用的,若是一个局部变量过了其做用域,那么在其做用域以后申明的新的局部变量就颇有可能会复用过时局部变量的槽位,从而达到节省资源的目的。
本地方法栈
也是线程私有的部分,本地方法栈与虚拟机栈方法做用类似,其区别仅是虚拟机栈为Java方法服务,而本地方法栈则是为虚拟机使用到的本地方法服务。
Java堆
是虚拟机的内存中的最大一块,它能被全部的线程共享,在虚拟机启动时建立,咱们在Java代码编写的对象实例就存在这快内存区域。
堆究竟是个什么样的结构呢?它由那几部份组成呢,每部分都各自有什么做用呢?
别慌,咱们一个一个来。
咱们都知道对象的存活是有周期的,若是一个对象没有被引用,那么就能够认为该对象能够被清除掉了,就是咱们认为的垃圾。因为每一个对象存活的时间不一样,为了减小GC线程扫描垃圾时间及频率,咱们能够将存活时间较长的对象单独放一个区域。所以,堆的布局也就肯定下来了。总的来讲,堆被划分红两部分:新生代和老年代。
新生代和老年代比值为1:2
,这个比例并非惟一的,咱们能够能够经过参数 –XX:NewRatio
按照具体的场景来指定。
若是再细粒度的划分,新生代又能够分为Eden区
和Survivor区
,而Survivor区
又能够分为FromSurvivor
和ToSurvivor
,默认比值为8:1:1
这时问题又来了,为何要将Survivor分为两块相等大小的空间啊?好问题,我先说答案,这两分为两部分主要是为了解决内存碎片化
的问题,若是内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC。不懂GC的先暂时这样理解,在下一篇文章垃圾回收算法时,我会重点讲解。
知道堆的内存结构布局后,咱们聊一聊对象是如何在堆中建立的。
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已被加载、解析和初始化过,若是没有,先执行相应的类加载过程,接下来为新生对象分配内存。为对象分配空间的任务等同于把一块肯定大小的内存从堆中划分出来。划分方式按照堆内存是否规整分为两种。
能够采用指针碰撞
方式解决,即全部被使用过的内存都放到一边,空闲的内存被放到另外一边,中间放着一个指针做为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。
能够采用空闲列表
的方式解决,空闲和使用的内存相互交错,JVM必须维护一个列表,记录哪些内存块是可用的,分配时候找到一块足够大的分配给对象实例。
咱们还有一个问题值得考虑的是,若是在并发状况下,对象的建立是否安全呢,会不会出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。废话,确定会出现这样的状况,能够有两种办法解决:①能够对分配内存空间的动做进行同步处理,这其实是虚拟机采用CAS
加上重试机制
保证更新操做的原子性。②把内存分配的动做按照线程划分在不一样的空间中进行,即每一个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)
,哪一个线程要分配内存,就在哪一个线程的TLAB上分配,只有TLAB用完并分配心得TLAB时,才须要同步锁定。
方法区
与Java堆同样,是各个线程共享的内存区域,在jdk1.8后,这部份内存被放置在元空间中,是一种逻辑内存
部分。它是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息
、常量
、静态变量
、即时编译器编译后的代码缓存数据,这些信息是由类加载时从类文件中提取出来的。
除此以外,方法区还具有如下特色:
由于jvm在运行应用时要大量使用存储在方法区中的类型信息,因此在类型信息的表示上,设计者除了要尽量提升应用的运行效率外,还要考虑空间问题。根据不一样的需求,jvm的实现者能够在时间和空间上追求一种平衡,具体体如今方法区的大小没必要是固定的,根据应用的须要动态调整。一样方法区也没必要是连续的。方法区能够在堆(甚至是虚拟机本身的堆)中分配。jvm能够容许用户和程序指定方法区的初始大小,最小和最大尺寸。
由于方法区是被全部线程共享的,因此必须考虑数据的线程安全。假如两个线程都在试图找lava的类,在lava类尚未被加载的状况下,只应该有一个线程去加载,而另外一个线程等待。
这篇文章详细讲解了Java虚拟机内存的各部分区域,这部份内容很是重要,接下来的文章:类加载机制、内存分配和垃圾回收算法都是以这篇为基础的。
好了,今天的文章就到这里了,我是Simon郎
,一个想要天天博学一点点的少年郎,若是这篇文章对你有帮助,欢迎在看
、点赞
、转发
哦!
参考文献
[1]https://www.cnblogs.com/yanl55555/p/12616356.html
[2]深刻理解JVM虚拟机.周志华