首页 > 图灵资讯 > 技术篇>正文

Java 内存分配全面浅析

2023-05-09 10:14:02

  本文将简要介绍Java内存分配的原理,帮助新手更容易地学习Java。这种文章网上有很多,但大部分都是零碎的。本文将从认知过程的角度给读者带来系统的介绍。

  Java程序在进入主题之前,首先要知道的是Java程序在JVMM运行(Java Virtual Machine,Java虚拟机可以将JVM理解为Java程序与操作系统之间的桥梁。JVM实现了Java平台的无关性,显示了JVM的重要性。因此,在学习Java内存分配原理时,一定要记住这一切都是在JVM中进行的,JVM是内存分配原理的基础和前提。

  简单地说,一个完整的Java程序操作过程将涉及以下内存区域:

  l寄存器:JVM内部虚拟寄存器,访问速度非常快,程序无法控制。

  l栈:保存局部变量的值包括:1。用于保存基本数据类型的值;2。保存类型的例子,即堆叠对象的引用(指针)。也可用于保存加载方法中的帧。

  l堆:用于存储动态数据,如new对象。请注意,创建的对象只包含自己的成员变量,而不包括成员方法。因为同一类别的对象有自己的成员变量并存储在自己的堆中,但他们共享这种方法并不是每个对象都复制一次。

  L常量池:JVM为每种加载类型维护一个常量池,常量池是该类型使用的常量的有序集合。包括直接常量(基本类型,String)引用其他类型、方法和字段的符号(1)。通过索引访问池中的数据和数组一样。常量池在Java的动态链接中起着核心作用,因为它包含了一种类型对其他类型、方法和字段的所有符号引用。堆中存在常量池。

  l代码段:用于存储从硬盘上读取的源程序代码。

  l数据段:用于存储static定义的静态成员。

  以下是内存表示图:

Java 内存分配全面浅析_Java

  上图大致描述了Java内存分配。接下来,通过实例详细说明Java程序是如何在内存中运行的(注:以下图片引用尚尚学校马士兵老师的J2SE课件,图片右侧为程序代码,左侧为内存分配示意图,我将逐一添加注释)。

  预备知识:

  1.Java文件,只要有main入口方法,我们就认为这是Java程序,可以单独编译。

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

  示例:

Java 内存分配全面浅析_java_02

  1.JVM自动寻找main方法,执行第一句代码,创建Test类的例子,在堆栈中分配一个内存,并存储一个指向堆叠对象的指针110925。

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

  3.创建两个BirthDate实例d1、d2,将相应的指针存储在堆栈中,指向各自的对象。他们在实例中调用了参数结构方法,因此对象具有自定义的初始值。

Java 内存分配全面浅析_Java_03

  调用test对象的change1方法,并以date为参数。JVM读取这个代码时,发现i是局部变量,所以他会把i放在栈里,把date的值给i。

Java 内存分配全面浅析_Java_04

  给i1234。一个非常简单的步骤。

Java 内存分配全面浅析_JVM_05

  实施change1方法后,立即释放局部变量i占用的栈空间。

Java 内存分配全面浅析_java_06

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

Java 内存分配全面浅析_java_07

  在change2方法中,另一个birthdate对象被实例化,并被赋予b。内部执行过程是:堆区new中有一个对象,并将对象的指针保存在堆栈中的b对应空间。此时,实例b不再指向实例d1指向的对象,但实例d1指向的对象没有变化,不会对d1产生任何影响。

  change2方法实施后,立即释放局部引用变量b占用的栈空间,注意只释放栈空间,等待自动回收。

Java 内存分配全面浅析_java_09

  以实例d2为参数,调用test实例的change3方法。同样,JVM会在栈中引用变量b分配空间,并将d2中的指针存储在b中。此时,d2和b指向相同的对象。再次调用实例b的setday方法实际上是调用d2指向对象的setday方法。

Java 内存分配全面浅析_内存分配_10

  调用实例b的setday方法会影响d2,因为它们指向相同的对象。

Java 内存分配全面浅析_常量池_11

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

  以上是Java程序运行过程中内存分配的一般情况。事实上,没什么。掌握这个想法很容易。它只不过是两种类型的变量:基本类型和参考类型。作为局部变量,两者都被放置在堆栈中。基本类型直接存储在堆栈中。参考类型只保存一个指向堆区的指针,真正的对象存储在堆中。作为参数,基本类型直接传输,参考类型指针。

  小结:

  1.区分什么是实例,什么是对象。Class a= new Class();这时候a叫实例,不能说a是对象。实例在栈中,对象在堆中,操作实例实际上是通过实例的指针间接操作对象。多个例子可以指向同一个对象。

  2.堆栈中的数据与堆栈中的数据销毁不同步。一旦方法结束,堆栈中的局部变量将立即被销毁,但堆栈中的物体不一定被销毁。因为可能还有其他变量指向物体,直到堆栈中没有变量指向物体,它才被销毁,而不是立即被销毁,只有在垃圾回收和扫描时才能被销毁。

  3.与应用程序相比,上述堆栈、堆叠、代码段、数据段等。每个应用程序都对应于唯一的JVM实例。每个JVM实例都有自己的内存区域,不相互影响。这些内存区域共享所有线程。这里提到的堆栈和堆栈是整体概念,也可以细分。

  4.成员变量在不同的对象中有所不同,并且有自己的存储空间(成员变量在堆中的对象中)。但是,这种方法是所有这类对象共享的。只有一套。当对象使用方法时,该方法被压入堆栈。如果该方法不使用,则不占用内存。

  以上分析只涉及堆栈和堆栈,还有一个非常重要的内存区域:常量池,这个地方经常出现一些令人费解的问题。上面已经解释了常量池是什么,没有必要深入理解,只要记住它保持了一个加载常量。接下来,结合一些例子来解释常量池的特性。

  预备知识:

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

  实例:

  [java] view plain copy1. public class test { 2. public static void main(String[] args) { 3. objPoolTest(); 4. } 5. 6. public static void objPoolTest() { 7. int i = 40; 8. int i0 = 40; 9. 40; 10. 40; 11. 0; 12. new Integer(40); 13. new Integer(40); 14. new Integer(0); 15. 1.0; 16. 1.0; 17. 18. "i=i0\t" + (i == i0)); 19. "i1=i2\t" + (i1 == i2)); 20. i1=i2+i3t" + (i1 == i2 + i3)); 21. "i4=i5\t" + (i4 == i5)); 22. i4=i5+i6\t" + (i4 == i5 + i6)); 23. "d1=d2\t" + (d1=d2); 24. 25. System.out.println(); 26. } 27. }

  结果:

  [java] view plain copy 1. i=i0 true 2. i1=i2 true 3. i1=i2+i3 true 4. i4=i5 false 5. i4=i5+i6 true 6. d1=d2 false

  结果分析:

  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,它们不再从常量池中寻找数据,而是从堆中找到一个对象,然后保存指向对象的指针。因此,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类型是检测常量池中是否有相应的字符串,如果有,则取出;如果没有,则添加当前添加。

  所有涉及内存原理的领域一般都是广泛而深刻的。不要听一个家庭的话,多读书文章。我只是在这里分析,里面有很多技巧,让读者去探索和思考。我希望这篇文章能对你有所帮助!

  脚注:

  (1)符号引用,顾名思义,是一个符号,当使用符号引用时,将分析符号。如果您熟悉linux或unix系统,您可以将该符号引用作为文件的软链接。当使用该软连接时,它将真正分析并扩展它以找到实际的文件

  对于符号引用,类加载层面讨论较多,源代码级别只是一种形式讨论。

  当一个类被加载时,该类使用的其他类别的符号引用将保存在常量池中。当实际代码执行时,当第一次遇到某个类别时,JVM将引用常量池中的这些符号,并将其转换为直接引用。这样,当下次遇到相同类型时,JVM将不再分析,并直接使用已分析的直接引用。

  除了上述类加载过程中的符号引用说法外,源代码级别是根据引用的分析过程来区分代码中的某些数据是属于符号引用还是直接引用,例如,System.out.println("test" +"abc");///这里的效果相当于直接引用,假设Strings = "abc"; System.out.println("test" + s);///这里发生的效果相当于符号引用,即分析s,相当于s是“abc符号链接,也就是说,在编译时,class文件并没有直接显示s,而是在实际代码执行时将s视为符号。

上一篇 Java垃圾回收机制
下一篇 Google推荐的图片加载库Glide介绍

文章素材均来源于网络,如有侵权,请联系管理员删除。