当前位置: 首页 > 科技观察

详解Java内存管理:栈、堆、引用类型_0

时间:2023-03-23 10:28:28 科技观察

【.com速译】小伙伴们,在使用Java编程的时候,你了解过调用内存的工作原理吗?总的来说,Java作为一个优秀的Silent垃圾收集器,具有自动管理内存的功能,可以在后台工作,清理不用的对象,释放内存。话虽如此,如果你的程序设计不到位,Java的垃圾收集器和内存管理功能可能不会自动生效。可见了解Java中内存的实际原理是至关重要的。它不仅可以帮助你编写高性能的应用程序,还可以尽量避免因OutOfMemoryError导致的程序崩溃;或帮助您在程序运行不畅时快速找到内存泄漏的原因。接下来,我们先来看看Java语言中的内存组织结构:如上图所示,内存通常分为两部分:栈和堆。请记住,这张图片中的内存类型大小与实际内存大小不成比例。即:相对于栈,堆是一块更大的内存。堆栈(Stack)堆栈内存不仅负责保存堆对象(heapobjects)的引用,还负责保存各种值的类型,即存储的是值本身,而不是对对象中对象的引用堆。在Java中,我们称之为原始类型(primitivetypes)。另外,栈上的变量有一定的可见性,我们称之为作用域。通常,只能使用活动范围内的对象。例如:假设我们没有任何全局作用域变量(或字段),只有局部变量,那么如果编译器要执行一个方法体,它只能从栈中访问方法体中的对象。而且由于它超出了范围,它无法访问其他局部变量。一旦该方法已执行并返回,它就会弹出堆栈顶部并更改活动范围。可能你注意到了,由于Java的栈内存是按照线程分配的,所以上图中会有多个栈内存。而且,程序每次创建和启动一个线程,都有自己的栈内存,不需要也不可能访问另一个线程的栈内存。堆(Heap)的内存存放的是实际的对象,这些对象会被栈的变量引用。让我们看一下下面这行代码:StringBuilderbuilder=newStringBuilder();关键字new负责确保堆获得足够的可用空间。它在内存中创建一个StringBuilder类型的对象,并传递对“builder”的引用,将其压入堆栈。由于每个正在运行的JVM进程只有一个堆内存,因此无论系统上当前有多少线程正在运行,它们都共享指定的内存部分。事实上,堆的真实结构与上图不同,它会根据垃圾回收的过程分为几个部分。是否需要预先定义堆栈和堆的最大大小将完全取决于程序运行的计算机。在接下来的讨论中,我们将着眼于配置JVM以显式指定正在运行的应用程序的大小。引用类型如果你仔细观察上面的图片,你可能会注意到来自堆的箭头,代表对象引用,实际上是不同的类型。这是因为在Java编程语言中我们有不同类型的引用,即:强引用、弱引用、软引用和幻引用。引用类型的区别在于堆上的对象可以在不同的条件下被不同的垃圾收集器引用。下面我们一一讨论。1、强引用这是开发者最流行也是最常用的引用类型。在上面的StringBuilder例子中,我们实际上对堆上的对象进行了强引用。堆上的对象不会被垃圾回收,而是有强引用指向它,或者通过强引用链来获取对象。2.弱引用弱引用可以通过以下方式创建:WeakReferencereference=newWeakReference<>(newStringBuilder());弱引用的最佳使用场景之一是缓存解决方案。想象一下,你检索了一些数据,想把它存储在内存中,以便下次可以直接响应。当然,您不确定何时或是否有对该数据的请求。然后,可以对它使用一个弱引用来防止堆上的对象被垃圾回收器回收,这样当对象被回收时,返回一个空值。可以看出,WeakHashMap/***TheentriesinthishashtableextendWeakReference,usingitsmainref*fieldasthekey.*/privatestaticclassEntryextendsWeakReferenceimplementsMap.Entry{Vvalue;一旦WeakHashMap中的键被垃圾回收,整个条目将从映射中删除。3.软引用这种类型的引用可以用于对内存非常敏感的程序。例如,如果应用程序内存不足,引用只会被垃圾回收。也就是说,除非万不得已,否则垃圾回收器是不会将软引用对应的对象处理掉的。而且Java的相关文档中有提到:在虚拟机抛出OutOfMemoryError之前,所有的软引用对象都已经被清除了。与弱引用类似,我们可以创建软引用,如下所示:SoftReferencereference=newSoftReference<>(newStringBuilder());4、虚引用虚引用可以用来进行后期的清理操作,毕竟我们可以确定对象已经不存在了。此类引用的.get()方法将始终返回null。幻象引用必须与引用队列(ReferenceQueue)一起使用。也就是说,当垃圾回收器准备回收一个对象时,如果发现它还有一个虚引用,就会在回收该对象的内存之前,将这个虚引用添加到与其关联的引用队列中。如何引用字符串Java中的字符串(String)类型有些特殊,它是不可变的。这意味着每次程序对一个字符串执行操作时,它实际上在堆上创建了另一个对象。至于字符串,由于Java管理着一个内存中的字符串池,所以Java尽可能地存储和重用字符串。例如:StringlocalPrefix="297";//1Stringprefix="297";//2if(prefix==localPrefix){System.out.println("Stringsareequal");}else{System.out.println("Stringsaredifferent");}上面的代码运行后,会打印:Stringsareequal可见,原来比较两个String类型的引用后,这些引用实际上指向了堆上的同一个对象。但是,这不适用于那些计算字符串。例如:我们把上面代码的//1行改成:StringlocalPrefix=newInteger(297).toString();//1那么输出就变成了:Stringsaredifferent(字符串不同)可见,在这种情况下,有是堆上的两个不同的对象。如果我们认为计算出的字符串会被频繁使用,可以在计算出的字符串末尾添加.intern()方法,强制JVM将其加入到字符串池中。下面的代码再次修改//1行:StringlocalPrefix=newInteger(297).toString().intern();//1那么输出就变成了:Stringsareequal(字符串相等)垃圾收集器就是上面说的,根据保存到堆中的堆栈上的变量引用的对象类型,在某个时间点,该对象将成为垃圾收集器的“合格对象”。如上所示,所有红色对象都有资格被垃圾收集器收集。您可能会注意到堆上有一个对象对其他对象具有强引用(例如,引用它的列表或具有两种引用类型的字段的对象)。由于它在栈上失去了引用,程序无法再访问它,所以它也变成了垃圾。在继续之前,让我们明确三点:这个过程是由Java自动触发的,由Java决定何时以及是否启动这个过程。当垃圾收集器运行时,应用程序中的所有线程都被暂停,因此这个过程是昂贵的。该过程并不像垃圾收集和释放内存那么简单。由于这是一个非常复杂的过程,可能会影响程序的性能,所以我们可以使用所谓的“标记和清除(MarkandSweep)”过程,即:让Java分析堆栈中的变量并“标记”所有需要保持活动状态的对象,然后清理所有未使用的对象。显然,被标记为垃圾的对象越多,需要保持存活的对象就越少,处理过程也会越快。为了提高效率,我们可以使用JavaJDK自带的工具——JvisualVM来可视化内存使用情况等有用的信息。当然,你需要安装一个叫做VisualGC的插件来查看内存的实际结构。如上图所示,我们创建一个对象并将其分配到Eden(1)空间。由于伊甸园空间不大,很快就会被填满。此时,垃圾收集器在伊甸园空间运行,并将每个对象标记为活动的。一旦一个对象在垃圾收集过程中幸存下来,它就会被移动到所谓的保留空间——S0(2)。当垃圾收集器第二次在Eden空间运行时,它会将所有剩余的对象移动到S1(3)空间。同样,当前在S0(2)上的所有对象也被移动到S1(3)空间。如果一个对象在经过n轮垃圾回收后仍然保留,则认为它是持久的并移入Old(4)空间。至此,在garbagecollectorgraph(6)中,你会看到每次运行后各种对象被转移到保留空间,同时Eden空间也被重新生成。Metaspace(5)可用于存储由JVM加载的各种类的元数据。上图实际上是一个Java8应用程序。在Java8之前的版本中,内存结构会略有不同。元空间实际上称为PermGen空间。比如在Java6中,这个空间还存放了字符串池的内存。因此,如果Java6应用程序中的字符串过多,可能会崩溃。垃圾收集器(GC)的种类实际上,JVM有以下三种垃圾收集器供开发者选择。默认情况下,会根据实际环境中的底层硬件选择Java。1.SerialGC——单线程收集器。它通常适用于数据量小的小型应用程序。您可以通过指定命令行选项来启用它:-XX:+UseSerialGC。2.ParallelGC——吞吐量收集器。就是使用多个线程进行垃圾回收的过程。您可以通过显式指定选项来启用它:-XX:+UseParallelGC。3.ConcurrentGC–如上所述,当垃圾收集过程运行时,所有线程都会被挂起。并发GC的很多操作(不是全部)都和应用的业务有并发关系。在具有多个处理核心的机器上,应用程序线程可以在并发收集期间使用处理器,因此并发垃圾收集器线程不会停止应用程序。效果当然是停顿会减少,但相应地,可供应用程序使用的处理器资源也会减少,并且速度可能会变慢,尤其是在应用程序正在最大化所有处理核心的情况下。通常有两种类型的并发GC可以选择:3.1Garbage-first-它在满足垃圾收集暂停时间目标的同时实现高吞吐量。您可以通过以下方式启用它:-XX:+UseG1GC。3.2ConcurrentMarkSweep-该收集器适用于寻求更短的垃圾收集暂停时间并能够与垃圾收集共享处理器资源的应用程序。您可以通过以下方式启用它:-XX:+UseConcMarkSweepGC。但是,从JDK9开始,不再推荐这种GC类型。提示和技巧要尽量减少内存使用,请尽可能限制变量的范围。请记住,每次弹出堆栈顶部的范围时,该范围内的引用都会丢失并导致对象被判断为符合垃圾收集条件。将过时引用标记为null以使此类引用对象符合垃圾回收条件。由于终结器会减慢垃圾收集过程,因此最好使用幻像引用。不要在可以使用弱引用或软引用的地方使用强引用。最常见的内存陷阱是??在缓存场景中,即使不再需要数据也会保留在内存中。JVisualVM还具有在特定时间点进行堆转储的能力,因此您可以分析每个类占用了多少内存。根据您的应用程序需要配置JVM。运行应用程序时,可以显式指定JVM堆的大小并分配合理的初始和最大内存量。您可以参考以下规范指南:Initialheapsize-Xms512m–将初始堆大小设置为512MB。最大堆大小-Xmx1024m–将最大堆大小设置为1024MB。线程堆栈大小-Xss1m–将线程堆栈大小设置为1MB。nascent_size-Xmn256m–将初始大小设置为256MB。如果您的Java应用程序因OutOfMemoryError崩溃,您可以使用–XX:HeapDumpOnOutOfMemory参数运行该进程。下次发生此类错误时,它将创建一个堆转储文件,以便您可以收集有关内存泄漏的信息。请使用-verbose:gc选项来获取垃圾收集输出。小结综上所述,如果你了解了内存是如何组织的,你不仅可以从合理使用内存资源的角度写出好的优化代码,还可以通过优化配置来调整运行的JVM。此外,使用正确的工具,您可以轻松修复各种程序中的内存泄漏错误。原标题:Java内存管理,作者:ConstantinMarian