今天咱们一块儿来学习JVM的内存分配,主要目的是为咱们Android内存优化打下基础。java
一直在想以什么样的方式来呈现这个知识点才能让咱们易于理解,最终决定使用方法为:图解+源代码分析。git
欢迎访问个人我的博客:senduo's blog程序员
咱们深知,一个Java程序员在不少时候根本不用操心内存的释放,而是依靠JVM去管理,之前写C++代码的时候,却要时刻记着new的空间要及时释放掉,否则程序很容易出现内存溢出的状况。由于,Java在这方面确实方便了许多,让咱们有更多精力去考虑业务方面的实现。可是,这并不意味着咱们就能肆无忌惮的使用内存,由于:数组
1.JVM并不会及时的去清理内存 2.咱们没法经过代码去控制JVM去清理内存
这就要求咱们平时在开发过程当中,要了解JVM的垃圾回收机制,合理安排内存。缓存
那么怎么样才能合理安排内存呢?那么就须要咱们了解JVM的内存分配机制,然后才能真正控制好,让程序运行在咱们鼓掌之中。性能优化
平时咱们对于Java内存都有一个比较粗略的概念,就是分堆和栈,但实际上仍是复杂得多,如下给出完整内存模型:性能
相对应区域的内容为:学习
这一个区域我归纳了如下几个要点:优化
1.这一区域不会出现OOM(Out Of Memory)错误的状况 2.属于线程私有,由于每个线程都有本身的一个程序计数器,来表示当前线程执行的字节码行号 3.标识Java方法的字节码地址,而不是Native方法 4.处于CPU上,咱们没法直接操做这块区域
这个区域也是咱们平时口中说的堆栈的栈,关于这个块区域有以下要点:spa
1.属于线程私有,与线程的生命周期相同 2.每个java方法被执行的时候,这个区域会生成一个栈帧 4.栈帧中存放的局部变量有8种基本数据类型,以及引用类型(对象的内存地址) 5.java方法的运行过程就是栈帧在虚拟机栈中入栈和出栈的过程 6.当线程请求的栈的深度超出了虚拟机栈容许的深度时,会抛出StackOverFlow的错误 7.当Java虚拟机动态扩展到没法申请足够内存时会抛出OutOfMemory的错误
这个区域,属于线程私有,顾名思义,区别于虚拟机栈,这里是用来处理Native方法(Java本地方法)的,而虚拟机栈是处理Java方法的。对于Native方法,Object中就有很多的Native的方法,hashCode,wait等,这些方法的执行不少时候都是借助于操做系统。
这一区域也有可能抛出StackOverFlowError 和 OutOfMemoryError
咱们平时说得最多,关注得最多的一个区域,就是他了。咱们后期进行的性能优化主要针对这部份内存,GC的主战场,这个地方存放的几乎全部的对象实例和数组数据。这里我大概进行了以下归纳:
1.Java堆属于线程共享区域,全部的线程共享这一块内存区域 2.从内存回收角度,Java堆可被分为新生代和老年代,这样分可以更快的回收内存 3.从内存分配角度,Java堆可划分出线程私有的分配缓存区(Thread Local Allocation Buffer,TLAB),这样可以更快的分配内存 4.当Java虚拟机动态扩展到没法申请足够内存时会抛出OutOfMemory的错误
方法区主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。GC在该区域出现的比较少。归纳以下:
1.方法区属于线程共享区域 2.习惯性加他永久代 3.垃圾回收不多光顾这个区域,不过也是须要回收的,主要针对常量池回收,类型卸载 4.常量池用于存放编译期生成的各类字节码和符号引用,常量池具备必定的动态性, 里面能够存放编译期生成的常量 5.运行期间的常量也能够添加进入常量池中,好比string的intern()方法。
运行时常量池也是方法区的一部分,用于存放编译器生成的各类字面量和符号引用。单独拿出来讲明一下,是由于咱们平时使用String比价多,涉及到这一块的知识,但这一块区域不会抛出OutOfMemoryError
首先写了一个main方法,来作演示,代码以下:
package senduo.com.memory.allocate; /** * ***************************************************************** * * 文件做者:ouyangshengduo * * 建立时间:2017/8/11 * * 文件描述:内存分配调用过程演示代码 * * 修改历史:2017/8/11 9:39************************************* **/ public class MemoryAllocateDemo { public static void main(String[] args){ //JVM自动寻找main方法 /** * 执行第一句代码,建立一个Test实例test,在栈中分配一块内存,存放一个指向堆区实例对象的指针 */ Test test = new Test(); /** * 执行第二句代码,声明定义一个int型变量(8种基本数据类型),在栈区直接分配一块内存存储这个变量的值 */ int date = 9; /** * 执行第三句代码,建立一个BirthDate实例bd1,在栈中分配一块内存,存放一个指向堆区实例对象的指针 */ BirthDate bd1 = new BirthDate(13,6,1991); /** * 执行第四句代码,建立一个BirthDate实例bd2,在栈中分配一块内存,存放一个指向堆区实例对象的指针 */ BirthDate bd2 = new BirthDate(30,4,1991); /** * 执行第五句代码,方法test1入栈帧,执行完出栈 */ test.test1(date); /** * 执行第六句代码,方法test2入栈帧,执行完出栈 */ test.test2(bd1); /** * 执行第七句代码,方法test3入栈帧,执行完出栈 */ test.test3(bd2); } }
1.JVM自动寻找main方法,执行第一句代码,建立一个Test类的实例test, 在栈中分配一块内存,存放一个指向堆区对象的指针110925。 2.建立一个int型的变量date,因为是基本类型,直接在栈中存放date对应的值9。 3.建立两个BirthDate类的实例bd一、bd2,在栈中分别存放了对应的指针指向各自的对象 ,他们在实例化时调用了有参数的构造方法,所以对象中有自定义初始值。
图解以下:
1.test1方法入栈帧,以date为参数 2.value为局部变量,把value放在栈中,而且把date的值赋值给value 3.把123456赋值给value局部变量 4.test1方法执行完,value内存被释放,test1方法出栈
1.test2方法入栈帧,以实例bd1为参数 2.birthDate为局部变量,把birthDate放在栈中,把bd1的引用的值赋值给birthDate, 也就是bd1与birthDate的地址都是指向同一个堆区的实例 3.在堆区new了一个对象,而且把这个堆区的指针保存在栈区中birthDate对应的内存空 间,这个时候,bd1与birthDate指向了不一样的堆区,那么birthDate的改变,并不会对 bd1形成影响 4.test2方法执行完,栈中的birthDate空间被释放,test2方法出栈,但堆区的内存空间 则要等待系统自动回收
1.test3方法入栈帧,以实例bd2为参数 2.birthDate为局部变量,把birthDate放在栈中,把bd2的引用的值赋值给birthDate, 也就是bd2与birthDate的地址都是指向同一个堆区的实例 3.调用birthDate的setDay方法,由于birthDate与bd2指向的是同一个对象,也就是bd2调用了setDay方法,因此,也会bd2形成影响 4.test3方法执行完,栈中的birthDate空间被释放,test3方法出栈
跟着上面四个步骤,走一遍,会发现其实也不会那么复杂,掌握思想就能摸到门路了,咱们平时注意区分一下基本数据类型的变量和引用数据类型变量,如下进行了几点归纳:
1.局部变量中的基本数据类型的值直接存栈中 2.局部变量中的引用数据类型在栈中存的是引用类型的指针(地址) 3.栈中的数据与堆中的数据内存回收并非同步的,栈中的只要方法运行完,就会直接 销毁局部变量,但堆中的对象不必定当即销毁 4.类的成员变量在不一样对象中各不相同,都有本身的存储空间(成员变量在堆中的对象中 )。而类的方法倒是该类的全部对象共享的,只有一套,对象使用方法的时候方法才被 压入栈,方法不使用则不占用内存
终于把JVM内存分配的分享写完了,一路写下来,确实对内存分配又深刻了解了一次。期间参考了如下博客:
经过对JVM内存模型的认识后,下一章将进行JVM垃圾回收机制的探索。