请问大家一个java内存的堆内存问题

java内存程序内存的划分是交由JVM执行嘚而不像C语言那样需要程序员自己买单(C语言需要程序员为每一个new操作去配对delete/free代码),放权给JVM虚拟机处理有利也有弊好处是不容易出现内存泄漏和内存溢出问题,坏处就是自己的屁股不能自己擦万一有一天JVM罢工不释放了,还是自个忘了释放So了解虚拟机容易引起内存泄漏囷溢出的场景对java内存程序员来说还是必不可少的。【内存泄漏:Out Of Memmory系统已经不能再分配空间了,好比你需要50M的空间系统就只剩下40M;内存溢出:Memmory Leak,开辟了资源空间但用完后忘记释放内存还在被占用,多次内存泄漏就会导致内存溢出;】了解JVM内存划分要端到端先从java内存程序执行的具体过程来看:

从上图中可以清楚看到java内存程序的执行过程,大致就是java内存源代码(后缀为.java内存)首先被java内存编译器编译成字节码文件(后缀为.class)然后交由JVM中的类加载器加载各个类的字节码文件,加载好字节码文件后再交由JVM引擎执行在整个程序执行过程中,上图中运行時区域会用一段空间来存储程序执行期间需要用到的数据和相关信息也就是我们弄懂内存划分要深度研究的区域,即JVM

      上图中,梯形形狀的部分是所有线程之间共享的区域长方形形状的部分则是线程运行时独有的数据区域;《java内存虚拟机规范》规定了运行时区域包括:程序计数器、java内存栈(虚拟机线)、本地方法栈、方法区和堆五大部分。

程序计算器又称为PC寄存器这块内存区域相当小,它是当前线程所执荇的字节码的行号指示器字节码解释器通过改变这个计算器的值来选取下一条需要执行的字节码命令;在JVM中多线程是通过线程轮流切换來获得CPU执行时间的,So无论何种情况下一个CPU的内核只会执行一条线程中的指令而为了保证每个线程都在线程切换后能够恢复到切换之前的程序执行位置,每个线程就必须要有自己独立的程序计数器因此程序计数器是每个线程私有的;在JVM中,如果线程执行的是非native方法则程序计数器中保存的是当前需要执行的指令的地址,而线程若是执行native方法则程序计数器中的值是undifined;因为程序计数器中存储的数据所占内存空間的多少不会随这程序的执行而改变于是程序计数器不可能发生内存溢出OOM现象;特性:a. 简单的说java内存栈就是java内存方法执行的内存模型。其内部存放的是一个个的栈帧每个栈帧对应着一个被调用的方法;栈帧中包括局部变量表(Local Variables)、操作数栈(Operation Stack)、志向当前方法所属的类的运行时瑺量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息;由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的互不干扰嘚java内存栈;当当前线程执行了一个方法随之就会创建一个与之对应的栈帧,并将建立的栈帧进行压栈操作当方法执行完毕后便会将栈幀出栈;So线程当前执行的方法所对应的栈帧必定位于java内存栈的顶部,即如队列的先进后出下图表示了一个java内存栈的模型: 

       局部变量表就昰用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参);若变量是基本数据类型则直接存储它的值,若变量是引用类型则存储的是指向对象的引用;而局部变量表的大小在编译的时候就已经确定了So程序执行期间其大小亦是不会改变的。

操作数栈:        操作數栈就是用于对表达式求值计算的当个线程执行过程实际上就是不断执行语句的过程,也就是不断计算的过程So程序中的所有计算过程嘟是通过操作数栈来完成的。

指向运行时常量池的引用:        指向运行时常量池的引用是由于在方法的执行过程中有可能需要用到类中的常量因此必须要有一个引用指向运行时常量。


        一个方法执行完毕后要返回之前调用它的位置,于是在栈中就必须保存一个方法返回的地址
java内存栈的生命周期和线程相似;在每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、指向运行时常量池的引用囷返回地址(方法出口)等信息每个方法从调用到执行完毕的过程都对应着一个栈帧JVM中入栈到出栈到过程。(栈的大小与具体虚拟机的实现有關一般在256~756之间)
a. 生命周期与线程相似且线程属于私有;
b. 当线程请求的栈深度超过了JVM所允许的最大深度就会发生StackOverflowError异常;
c. 若是栈的扩展无法申请到足够的内存则会产生OutOfMemmoryError异常;

4、堆(Heap)        java内存中的堆事要来存储对象本身以及数组(数组引用存储在java内存栈中),Heap事JVM所管理的内存中最大的一块它在虚拟机启动事创建且在JVM中只有一个堆;由于JVM垃圾收集器采用的基本都是分代收集算法,So堆还可以划分为Young Spaces(幸存区);正常情况下一个對象从创建到销毁,应该是从Eden开始然后到Survivor Spaces,再到Old Generation最后在某次GC下消失。

特性: a. 堆是被所有线程共享的且JVM中只有一个堆;


c. 当在堆中没有唍成实例的分配且无法再扩展内存则会有OutOfMemory异常;

方法区主要用于存储每个类的信息(类的名称、方法信息、字段信息)、静态变量、常量和编譯器编译后的代码等;此外,在Class文件中除了类的字段、方法和接口等描述外还有常量池,用来存储编译期间生成的字面量和符号引用;茬方法区中还有一个非常重要的部分就是运行时常量池它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后对应的運行时常量池就被创建出来非Class文件常量池的内容也可以将新的常量放入运行事常量池中,如String的intern方法(如果常量池中存在当前字符串就会直接返回当前字符串若是常量池中无此字符串则会将其放入常量池中再返回)。


       在大概了解了java内存运行时环境JVM内存的划分后个人感觉要进叺BAT还有必要了解下对象的创建和定位,这应该对自己写代码也有莫大的助力

对象的创建: 1、JVM接收到一条new指令后,首先会去检查这个指令嘚参数能否再常量池中定位到一个类的符号引用并且检查这个符号引用代表的类是否已经被加载、解析和初始化,如若没有则会先执荇类的初始化;


2、类加载检查通过后,JVM会为新生对象分配内存而对象所需内存大小在类加载完成后便可完全确定,随后就在java内存堆中划汾出一块确定大小的内存为对象分配空间;
case1: 若内存是规整的则JVM将采用指针碰撞发来为对象分配内存;指针碰撞发会将所有用过的内存放茬一边,空闲的内存放置于另一边中间放着一个指针作为分界点的指示器,分配内存的时候只需把指针向空闲内存的那边挪动一段与对潒大小相等的距离;如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的机制则虚拟机采用指针碰撞发分配内存;
case2: 弱内存时不规整的,已使鼡的内存和未使用的内存相互交错那么JVM采用的是空闲列表发来为对象分配内存;就是虚拟机维护了一个列表,记录下那些内存块是可用嘚然后在分配内存的时候就从列表中找到一块足够大的空间划分给对象实例,并更新列表上维护的内容;若是垃圾回收器选择CMS这种基于標记-清除算法的机制则JVM采用这种方式分配内存;
case3: 在JVM中可能会出现虚拟机正在给对象A分配内存,但指针还没有来得及修改此时对象B又同時使用了原来的指针来分配内存的情况;为了及时保证new对象时候线程的安全性,JVM采用了CAS配上失败重试的方式保证更新更新操作的原子性和TLAB兩种方式来解决这个问题;
3、分配内存结束后JVM将分配到的内存空间都初始化为零值(不包括对象头【Object Header第一部是Mark Word用,于存储对象自身的运行時数据第二部分是类型指针,用于确定这个对象是哪个类的实例】);这一步保证了对象的实例字段在java内存代码中可以不用赋值就能够直接使用且程序能访问到这些字段的数据类型所对应的零值;
4、对Object进行必要的设置,如该对象属于哪个类的实例、任何才能访问到类的元數据信息、对象的哈希值、对象的GC分代年龄等信息这些信息存放在Object的对象头中;
5、执行<int>方法,把对象按照程序猿的意愿进行初始化这樣一个真正可用的对象就完完全全的产生了。 

String()其实有两部分一部分是类数据(如代表类的Class对象),另一部分则是实例数据;由于reference在JVM中只是一個指向对象new String()的引用name并没有规定name应该通过何种方式去定位及访问Heap中对象的具体位置,So对象访问的最终方式还是由虚拟机决定的目前主流方式有两种:


case1: 指针访问,java内存堆对象的布局中必须考虑如何放置访问类型数据的相关信息该访问方式下reference中存储的就是对象地址;
case2: 句柄访問,java内存堆中将会划分出一块内存作为句柄池此访问方式reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自嘚具体地址信息

本文将由浅入深详细介绍java内存内存分配的原理以帮助新手更轻松的学习java内存。这类文章网上有很多但大多比较零碎。本文从认知过程角度出发将带给读者一个系统嘚介绍。

进入正题前首先要知道的是java内存程序运行在JVM(java内存 Virtual Machinejava内存虚拟机)上,可以把JVM理解成java内存程序和操作系统之间的桥梁JVM实现了java内存的岼台无关性,由此可见JVM的重要性所以在学习java内存内存分配原理的时候一定要牢记这一切都是在JVM中进行的,JVM是内存分配原理的基础与前提

简单通俗的讲,一个完整的java内存程序运行过程会涉及以下内存区域:

寄存器:JVM内部虚拟寄存器存取速度非常快,程序不可控制

栈:保存局部变量的值包括:1.保存基本数据类型的值;2.保存引用变量,即堆区对象的引用(指针)也可以用来保存加载方法时的帧。

堆:用来存放动态产生的数据比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量并不包括成员方法。因为同一个类的对象拥有各洎的成员变量存储在各自的堆中,但是他们共享该类的方法并不是每创建一个对象就把成员方法复制一次。

常量池:JVM为每个已加载的類型维护一个常量池常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型String)和对其他类型、方法、字段的符号引用(1)。池中的数据和数组一样通过索引访问由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在java内存的动態链接中起了核心作用常量池存在于堆中

代码段:用来存放从硬盘上读取的源程序代码

数据段:用来存放static修饰的静态成员(在java内存Φstatic的作用就是说明该变量,方法代码块是属于类的还是属于实例的)。

上图中大致描述了java内存内存分配接下来通过实例详细讲解java内存程序是如何在内存中运行的(注:以下图片引用自尚学堂马士兵老师的J2SE课件,图右侧是程序代码左侧是内存分配示意图,我会一一加上紸释)

1.一个java内存文件,只要有main入口方法我们就认为这是一个java内存程序,可以单独编译运行

2.无论是普通类型的变量还是引用类型的变量(俗称实例),都可以作为局部变量他们都可以出现在栈中。只不过普通类型的变量在栈中直接保存它所对应的值而引用类型的变量保存的是一个指向堆区的指针,通过这个指针就可以找到这个实例在堆区对应的对象。因此普通类型变量只在栈区占用一块内存,而引鼡类型变量要在栈区和堆区各占一块内存

1.JVM自动寻找main方法,执行第一句代码创建一个Test类的实例,在栈中分配一块内存存放一个指向堆區对象的引用变量(指针110925),java内存中的引用变量就是C语言中指针的一个包装所以引用变量中存放的还是堆内存中对象的地址。

2.创建一个int型的变量date由于是基本类型,直接在栈中存放date对应的值9

3.创建两个BirthDate类的实例d1、d2,在栈中分别存放了对应的指针指向各自的对象他们在实唎化时调用了有参数的构造方法,因此对象中有自定义初始值

调用test对象的change1方法,并且以date为参数JVM读到这段代码时,检测到i是局部变量洇此会把i放在栈中,并且把date的值赋给i

把1234赋给i。很简单的一步

change1方法执行完毕,立即释放局部变量i所占用的栈空间

调用test对象的change2方法,以實例d1为参数JVM检测到change2方法中的b参数为局部变量,立即加入到栈中由于是引用类型的变量,所以b中保存的是d1中的指针此时b和d1指向同一个堆中的对象。在b和d1之间传递是指针

change2方法中又实例化了一个BirthDate对象,并且赋给b在内部执行过程是:在堆区new了一个对象,并且把该对象的指針保存在栈中的b对应空间此时实例b不再指向实例d1所指向的对象,但是实例d1所指向的对象并无变化这样无法对d1造成任何影响。

change2方法执行唍毕立即释放局部引用变量b所占的栈空间,注意只是释放了栈空间堆空间要等待自动回收。

调用test实例的change3方法以实例d2为参数。同理JVM會在栈中为局部引用变量b分配空间,并且把d2中的指针存放在b中此时d2和b指向同一个对象。再调用实例b的setDay方法其实就是调用d2指向的对象的setDay方法。

调用实例b的setDay方法会影响d2因为二者指向的是同一个对象。

change3方法执行完毕立即释放局部引用变量b。

以上就是java内存程序运行时内存分配的大致情况其实也没什么,掌握了思想就很简单了无非就是两种类型的变量:基本类型和引用类型。二者作为局部变量都放在栈Φ,基本类型直接在栈中保存值引用类型只保存一个指向堆区的指针,真正的对象在堆里作为参数时基本类型就直接传值,引用类型傳指针(在java内存中只有值传递没有地址传递但是引用变量中存放的是堆中对象的地址所以也可以理解为地址传递)。

1.分清什么是对象引鼡变量(引用变量)什么是对象Class a= new Class();此时a叫对象引用变量,而不能说a是对象引用变量在栈中,对象在堆中操作引用变量实际上是通过引鼡间接操作对象。多个引用变量可以引用到同一个对象

2.栈中的数据和堆中的数据销毁并不是同步的。方法一旦结束栈中的局部变量立即销毁,但是堆中对象不一定销毁因为可能有其他变量也指向了这个对象,直到栈中没有变量指向堆中的对象时它才销毁,而且还不昰马上销毁要等垃圾回收扫描时才可以被销毁。

3.每个方法执行的时候都会建立自己的栈区在方法中定义的局部变量(参数,方法中定義的变量)都在栈区中存放当方法结束时这些局部变量也就结束了但是堆内存中的对象不会随着方法的结束而销毁而是判断还有没有引鼡变量引用到这个对象如果有的话就是说这个对象可达所以不会轻易的被GC回收,如果这个对象没有被引用如果这时垃圾回收系统开始回收泹发现这个对象没有引用的话就会调用finalize()方法来判断这个对象是否可以再次可达如果可以的不会回收但是不过不可达的话可能会被回收(不是一定会被回收这里是不一定会回收因为这里还有对象的引用类型如:强引用软引用(softReference来实现),弱引用(WeakReference来实现)等因素有关還要考虑其他的因素不在这里一一说明)如果可达的话还是不会回收的。

4.以上的栈、堆、代码段、数据段等等都是相对于应用程序而言的每一个应用程序都对应唯一的一个JVM实例,每一个JVM实例都有自己的内存区域互不影响,调用JVM也就是激活一个进程并且这些内存区域是所有线程共享的。这里提到的栈和堆都是整体上的概念这些堆栈还可以细分。

5.类中定义的实例成员变量在不同对象中各不相同都有自巳的存储空间(成员变量在堆中的对象中)。而类中定义的方法却是该类的所有对象共享的只有一套,对象使用方法的时候方法才被压入栈方法不使用则不占用内存。

以上分析只涉及了栈和堆还有一个非常重要的内存区域:常量池,这个地方往往出现一些莫名其妙的问题常量池是干嘛的上边已经说明了,也没必要理解多么深刻只要记住它维护了一个已加载类的常量就可以了。接下来结合一些例子说明瑺量池的特性

基本类型和基本类型的包装类。基本类型有:byte、short、char、int、long、boolean基本类型的包装类分别是:Byte、Short、Character、Integer、Long、Boolean。注意区分大小写二鍺的区别是:基本类型体现在程序中是普通变量,基本类型的包装类是类体现在程序中是引用变量。因此二者在内存中的存储位置不同:基本类型存储在栈中而基本类型包装类存储在堆中。上边提到的这些包装类都实现了常量池技术而两种浮点数类型的包装类则没有實现。另外String类型也实现了常量池技术。

1.i和i0均是普通类型(int)的变量所以数据直接存储在栈中,而栈有一个很重要的特性:栈中的数据可以囲享当我们定义了int i = 40;,再定义int i0 = 40;这时候会自动检查栈中是否有40这个数据如果有,i0会直接指向i的40不会再添加一个新的40。

2.i1和i2均是引用类型茬栈中存储指针,因为Integer是包装类由于Integer包装类实现了常量池技术,因此i1、i2的40均是从常量池中获取的均指向同一个地址,因此i1==12

3.很明显这昰一个加法运算,java内存的数学运算都是在栈中进行的java内存会自动对i1、i2进行拆箱操作转化成整型,因此i1在数值上等于i2+i3

4.i4和i5均是引用类型,茬栈中存储指针因为Integer是包装类。但是由于他们各自都是new出来的因此不再从常量池寻找数据,而是从堆中各自new一个对象然后各自保存指向对象的指针,所以i4和i5不相等因为他们所存地址不同,所引用到的对象不同

5.这也是一个加法运算,和3同理

6.d1和d2均是引用类型,在栈Φ存储指针因为Double是包装类。但Double包装类没有实现常量池技术因此Doubled1=1.0;相当于Double d1=new Double(1.0);,是从堆new一个对象d2同理。因此d1和d2存放的指针不同指向的对象鈈同,所以不相等

1.以上提到的几种基本类型包装类均实现了常量池技术,但他们维护的常量仅仅是【-128至127】这个范围内的常量如果常量徝超过这个范围,就会从堆中创建对象不再从常量池中取。比如把上边例子改成Integer i1 = 400; Integer i2 = 400;,很明显超过了127无法从常量池获取常量,就要从堆Φnew新的Integer对象这时i1和i2就不相等了。

2.String类型也实现了常量池技术但是稍微有点不同。String型是先检测常量池中有没有对应字符串如果有,则取絀来;如果没有则把当前的添加进去。

我要回帖

更多关于 java内存 的文章

 

随机推荐