对JVM内存的理解

1.JVM内存大致分为五个区域

1)栈

栈内存,线程私有,生命周期与线程相同。每个java方法在执行的时候都会创建一个栈帧,用来创建这个方法的操作数栈,局部变量表,方法正常完成和异常完成信息,动态连接等信息。每个方法的开始执行到结束过程都对应了它的栈帧在栈中的入栈和出栈的过程。一般而言,的栈指的就是栈帧里存储局部变量表的内存部分。

局部变量表:是一组变量值的存储空间,它用于存储方法,参数,以及方法内定义的局部变量。

局部变量表所需要的内存空间在代码编译期就已经分配完成,当进入一个方法时这个方法在栈帧中需要分配多大的局部变量表空间是完全确定的,在方法运行期间不会改变局部变量表大小。(*是否是局部变量未初始化时,不可使用的原因?)

2)堆

堆内存,线程共享,是JVM管理的内存最大的一块内存区域,在虚拟机启动时创建。因为堆里面存储的对象是线程共享,所以多线程的时候也需要同步机制。值得一提的是,成员变量,不是静态变量不独立于类的实例而存在,可包含基本类型和引用类型成员变量,都是存放在堆的对象里,和对象同时生成和销毁。所以说基本数据类型存放于栈中是不准确的。

java虚拟机规范对这块的描述是:所有对象实例及数组都要在堆上分配内存,但随着JIT编译器的发展和逃逸分析技术的成熟,这个说法也不是那么绝对,但是大多数情况都是这样的。

即时编译器:可以把把Java的字节码,包括需要被解释的指令的程序)转换成可以直接发送给处理器的指令的程序)

逃逸分析:通过逃逸分析来决定某些实例或者变量是否要在堆中进行分配,如果开启了逃逸分析,即可将这些变量直接在栈上进行分配,而非堆上进行分配。这些变量的指针可以被全局所引用,或者被其它线程所引用。

堆是所有线程共享的,它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代。

根据虚拟机规范,Java堆可以存在物理上不连续的内存空间,就像磁盘空间只要逻辑是连续的即可。它的内存大小可以设为固定大小,也可以扩展。

当前主流的虚拟机如HotPot都能按扩展实现(通过设置 -Xmx和-Xms),如果堆中没有内存内存完成实例分配,而且堆无法扩展将报OOM错误(OutOfMemoryError)

3)方法区

方法区,线程共享,为了区分堆,又被称为非堆。用于存储已被虚拟机加载的类信息,静态变量,静态块,静态方法,成员方法,常量,即时编译器编译后的代码等数据等。在老版jdk,方法区也被称为永久代。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

jdk8真正开始废弃永久代,而使用元空间(Metaspace)

4)程序计数器

程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。

 这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。

5)本地方法栈

本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码。

6)运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。Java 虚拟机对Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java 虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了保存Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中①。运行时常量池相对于Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只能在编译期产生,也就是并非预置入Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String 类的intern() 方法。既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError 异常

2.代码执行时,内存变化

下面是内存表示图: 
            这里写图片描述

Java 字符串常量存放在堆内存还是JAVA方法区?

JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。JDK1.8开始,取消了Java方法区,取而代之的是位于直接内存的元空间(metaSpace)

预备知识:

1.一个Java文件,只要有main入口方法,我们就认为这是一个Java程序,可以单独编译运行。 
2.无论是普通类型的变量还是引用类型的变量(俗称实例),都可以作为局部变量,他们都可以出现在栈中。只不过普通类型的变量在栈中直接保存它所对应的值,而引用类型的变量保存的是一个指向堆区的指针,通过这个指针,就可以找到这个实例在堆区对应的对象。因此,普通类型变量只在栈区占用一块内存,而引用类型变量要在栈区和堆区各占一块内存。 
3.在方法的参数传递中,基本数据类型,String类是按值传递,即拷贝了一个副本!引用数据类型是按引用传递,即把栈中的地址传入!

示例: 
这里写图片描述
1.JVM自动寻找main方法,执行第一句代码,创建一个Test类的实例,在栈中分配一块内存,存放一个指向堆区对象的指针110925。 
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程序的运行原理图

参考链接:https://www.cnblogs.com/lipeineng/p/8358601.html

               https://blog.csdn.net/liupeng900605/article/details/7826573?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1

https://www.cnblogs.com/weibanggang/p/11119410.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章