Java内存你们都知道,但你知道要怎么管理Java内存吗?

2021年11月22日 阅读数:8
这篇文章主要向大家介绍Java内存你们都知道,但你知道要怎么管理Java内存吗?,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

前言java

深刻研究Java内存管理,将加强你对堆如何工做、引用类型和垃圾回收的认识。程序员

你可能会思考,若是你使用Java编程,关于内存如何工做你须要了解哪些哪些信息?Java能够进行自动内存管理,并且有一个很好的、安静的垃圾回收器,它在后台工做,清理那些未使用的对象并释放一些内存。算法

Java内存你们都知道,但你知道要怎么管理Java内存吗?_java内存管理

所以,做为一名Java程序员,你不须要再为销毁无用对象这样的问题而烦恼了。可是,虽然这个过程在Java中是自动的,它也不能保证任何事情。因为不知道垃圾回收器和Java内存是如何设计的,有些对象即便你再也不使用了,却也不符合垃圾回收的条件。编程

所以,了解Java中内存实际是如何工做的很是重要,由于它为你编写高性能和优化的应用程序提供了帮助,这些应用程序永远不会因内存不足而崩溃。另外一方面,当你发现本身处于糟糕的境地时,你将可以很快发现内存的漏洞。缓存

首先,让咱们看看内存在Java中一般是如何组织的:并发

Java内存你们都知道,但你知道要怎么管理Java内存吗?_java内存管理_02


一般,内存分为两大部分:堆栈和堆。请记住,内存类型在上图中的大小与实际内存大小不成比例。与堆栈相比,堆是一个巨大数量的内存。app

堆栈编程语言

堆栈内存负责保存对堆对象的引用和存储值类型(在Java中也称为基元类型),值类型保存值自己而不保存对堆中对象的引用。ide

此外,堆栈上的变量具备必定的可见性,也称为做用域。只有活跃做用域内的对象才能被使用。例如,假设咱们没有任何全局做用域变量(字段),只有局部变量,若是编译器执行方法的主体,它只能访问方法主体内堆栈中的对象。它不能访问其它局部变量,由于这些变量超出了做用域。一旦方法完成并返回,堆栈顶部就会溢出,活跃做用域也会发生变化。工具

或许你注意到了在上图中显示的多个堆栈内存,这是由于Java中的堆栈内存是按线程分配的。所以,每次一个线程被建立和启动时,它都有本身的堆栈内存,而且不能访问另外一个线程的堆栈内存。

堆内存将实际对象存储在内存中。这些对象被堆栈中的变量引用。例如,让咱们分析下面一行代码发生了什么:

StringBuilder builder = new StringBuilder();

“new”关键字负责确保堆上有足够的可用空间,在内存中建立一个StringBuilder类型的对象,并经过堆栈中的“builder”引用它。

每一个正在运行的JVM进程只有一个堆内存。所以,不管运行多少线程,这都是内存中的一个共享部分。实际上,堆结构与上图中显示的略有不一样。堆自己被分红几个部分,这有助于垃圾回收进程。

最大堆栈和堆大小都没有预约义 - 这取决于正在运行的计算机。 然而,在后文中,咱们将研究一些JVM配置,这些配置容许咱们为正在运行的应用程序明确设定它们的大小

Java内存你们都知道,但你知道要怎么管理Java内存吗?_java_03

引用类型

若是仔细观察内存结构图片,你或许会注意到,表明对堆中对象引用的箭头的样式实际是不一样的。这是由于,在Java编程语言中,咱们有不一样类型的引用:强引用、弱引用、软引用和虚引用。引用类型之间的区别在于它们所引用堆上的对象在不一样的条件下能够被做为垃圾回收。让咱们来仔细认识一下每一种引用类型。

1. 强引用>>>

这种引用类型是咱们都习惯而且最受欢迎的引用类型。在上面的StringBuilder示例中,咱们实际上使用了对堆中对象的强引用。当有一个强引用指向堆上的对象时,或者经过一系列强引用能够强访问该对象,则该对象不会被做为垃圾回收。

2. 弱引用>>

简单来讲,在下一个垃圾回收进程以后,对堆中对象的弱引用极可能不会继续存在了。弱引用的建立示例以下:

WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());

弱引用的一个很好的用例是缓存方案。假设你检索了一些数据,而且还但愿将其存储在内存中—这样一样的数据能够被再次请求。另外一方面,你不肯定什么时候或者是否会再次请求这些数据。所以,你能够保留对它的弱引用,万一垃圾回收器运行,它可能会破坏堆中的对象。所以,过了一下子,若是你想要检索你引用的对象,你可能会忽然获得一个空的返回值。缓存方案的一个很好的使用是回收WeakHashMap。若是咱们在Java API中打开WeakHashMap类,咱们会看到它的条目实际上扩展了WeakReference类,并使用它的引用字段做为映射的关键字:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> 
{ 
V value;
......
}

一旦WeakHashMap中的一个关键字被进行了垃圾回收,整个条目就会从映射中移除。

3. 软引用>>>

这种引用类型用于对内存更敏感的方案,由于只有当应用程序内存不足时,所引用的对象才会被做为垃圾回收。所以,只要没有迫切须要释放出一些内存空间,垃圾回收器就不会去回收软引用的对象。Java保证在抛出OutOfMemoryError以前清除全部软引用的对象。Javadocs代表:“在虚拟机抛出OutOfMemoryError以前,全部对可软访问对象的软引用都会确保被清除。” 与弱引用相似,软引用的建立示例以下:

SoftReference<StringBuilder> reference = new SoftReference<>(new StringBuilder());
......
}

4. 虚引用>>>

用于算法检查后的清理操做,由于咱们知道有些对象不须要再存在。仅与引用队列一块儿使用,由于此类引用的.get()方法将始终返回空值。这些引用类型被认为是优于终结器的。

如何引用字符串

Java中对字符串类型的处理略有不一样。字符串是不可变的,这意味着每次使用字符串执行操做时,实际上都会在堆上建立另外一个对象。对于字符串,Java在内存中进行字符串池管理。这意味着Java会尽量地存储和重用字符串。对于字符串文字,更是这样。例如:

String localPrefix = "297"; //1
String prefix = "297";      //2
if (prefix == localPrefix)
{
    System.out.println("Strings are equal" );
}
else
{
    System.out.println("Strings are different");
}

运行时,将输出如下内容:

Strings are equal

所以,能够看出在比较了字符串类型的两个引用以后,它们实际上指向了堆中的相同对象。可是,这对于被计算的字符串无效。假设咱们对上述代码的//1行进行如下更改

 
 
String localPrefix = new Integer(297).toString(); //1
 
 


输出:

Strings are different

在这种状况下,咱们实际上看到堆上有两个不一样的对象。若是咱们考虑到计算出的字符串会被常用,咱们能够强制JVM经过在计算的字符串末尾添加.intern()方法将计算的字符串添加到字符串池当中:

 
 
String localPrefix = new Integer(297).toString().intern(); //1
 
 


进行上述更改后输出以下:

Strings are equal

垃圾回收进程

正如前面所讨论的,根据堆栈中的变量对堆中对象的引用类型,在某个肯定的时间点,该对象符合垃圾回收器的条件。

Java内存你们都知道,但你知道要怎么管理Java内存吗?_java_04

比方说,全部红色的对象都符合被垃圾回收器的条件。 你可能会注意到堆上有一个对象,它对同一堆上的其它对象进行了强引用(例如,多是引用了本身项的列表,或者是具备两个引用类型字段的对象)。可是,因为堆栈中的引用丢失,这个对象就没法再被访问,所以它也成了垃圾。

为了更深刻地了解细节,咱们先提出如下几点:

1.这个过程是由Java自动触发的,什么时候启动以及是否启动此过程取决于Java。

2.实际上这个进程是昂贵的。当垃圾回收器运行时,应用程序中的全部线程都会暂停(取决于GC类型,稍后将对此进行讨论)。

3.这其实是一个比垃圾回收和释放内存更复杂的进程。

尽管由Java决定什么时候运行垃圾回收器,你也能够直接调用System.gc( )并指望垃圾回收器在执行这行代码时运行,对吧?

这是一个错误的假设。

你只须要让Java运行垃圾回收器,可是是否运行垃圾回收器仍然取决于Java。不管如何,不建议直接调用System.gc( )。

因为这是一个很是复杂的过程,而且它可能会影响你程序的表现,它须要以一个智能的方式实现。 一个被称做“标记和扫描”的进程来完成此任务。Java分析堆栈中的变量并“标记”全部保持活跃的对象,而后清除全部不会使用的对象。

实际上,Java并无回收任何垃圾。事实上,垃圾越多,标记为活跃的对象就越少,进程也就越快。为了使这个进程更加优化,堆内存实际由多个部分组成。咱们能够经过JVisualVM(Java JDK附带的工具)可视化内存使用状况和其它一些有用的东西。您惟一须要作的就是安装一个名为Visual GC的插件,它容许您查看内存的实际结构。让咱们放大一点,分解大局:

Java内存你们都知道,但你知道要怎么管理Java内存吗?_java内存管理_05

当一个对象被建立时,它被分配到Eden(1)区。由于Eden区的空间没有那么大,它很快就满了。垃圾回收器在Eden区运行,并标记出活跃的对象。

一旦一个对象在一次垃圾回收进程中存活,它就会被移动到所谓的幸存者区S0(2)中。 垃圾器第回收二次在Eden区上运行时,它会将全部幸存的对象移动到S1(3)区中。此外,当前在S0(2)区上的全部内容都将被移动到S1(3)区中。

若是一个对象在X轮垃圾回收中存活了下来(取决于JVM的实现,在个人例子中是8轮),那么它极可能会永远存活下来,并被移入到Old(4)区。

结合目前为止所说的一切,若是你看一下图中标号(6)的垃圾回收器,它每次运行时,你均可以看到对象切换到幸存者空间,而且Eden区的空间增大了。如此反复。老一代也能够被做为垃圾回收,但因为它在内存中空间是比Eden区更大的部分,所以这种状况不会常常发生。Metaspace(5)用于在JVM中存储已加载类的元数据。

所呈现的图片其实是一个Java 8的应用程序。在Java 8以前的版本,内存的结构有点不一样。元空间实际上称为PermGen. 区。例如,在Java 6中,此空间还为字符串池存储了内存。所以,若是Java 6应用程序中有太多字符串,则它可能会崩溃。欢迎你们关注个人公种浩【程序员追风】,文章都会在里面更新,整理的资料也会放在里面。

Java内存你们都知道,但你知道要怎么管理Java内存吗?_java_06

垃圾回收器类型

实际上,JVM有三种类型的垃圾回收器,程序员能够选择应该使用哪一种垃圾回收器。默认状况下,Java根据底层硬件选择要使用的垃圾回收器类型。

1.串行垃圾回收器 - 一个单线程回收器。 主要适用于数据使用量较小的小型应用程序。 能够经过指定命令行选项来启用:-XX:+ UseSerialGC

2.并行垃圾回收器 - 从命名能够看出,串行垃圾回收器和并行垃圾回收器之间的区别在于并行垃圾回收器使用多个线程来执行垃圾回收进行。并行垃圾回收器也被称做吞吐量回收器。能够经过直接指定选项来启用它:-XX:+ UseParallelGC

3.主要并发标记垃圾回收器 - 若是你还记得,在本文前面提到垃圾回收过程实际上至关昂贵,而且当它运行时,全部线程都被暂停。可是,咱们有这种大多数并发GC类型,它声明它与应用程序并发工做。可是,它有“大多数”并发的缘由。它不能100%同时应用于应用程序。线程暂停一段时间。尽管如此,暂停时间尽量短,以实现最佳的GC性能。实际上,有两种类型的大多数并发GC:

3.1垃圾优先 - 应用程序合理暂停时间内的高吞吐量。 经过如下选项启用:-XX:+ UseG1GC

3.2并发标记扫描 - 应用程序暂停时间保持最短。能够经过指定选项来启用:-XX:+ UseConcMarkSweepGC。从JDK 9开始,这个垃圾回收器类型不推荐使用。。

提示和技巧

1.为了最小化内存的占用,请尽量限制变量的做用域。请记住,每次堆栈中的顶级做用域溢出时,来自该做用域的引用都会丢失,这可能会致使相应的对象被做为垃圾回收。

2.直接对空的、废弃对象的引用,这会致使被引用的对象被做为垃圾回收。

3.避免成为终结者。 它们放慢了进程,不保证任何事情, 更喜欢进行对虚引用的清理工做。

4.当弱引用或软引用适用时,请不要使用强引用。最多见的内存缺陷是缓存方案,即便数据可能不须要,也会被保存在内存中。

5.JVisualVM还具备在某一点时间点进行堆转储的功能,所以你能够分析每一类所占用的内存量。

6.根据你的应用程序需求来配置JVM。运行应用程序时,明确指定JVM的堆大小。内存分配进程是宝贵的,所以要为堆分配一个合理的初始最大内存空间。若是你知道一开始使用较小的初始堆空间是没有意义的,JVM将扩展这个内存空间。 根据如下命令来明确内存空间:

(1)初始堆大小 -Xms512m 将初始堆大小设置为512 mb。

(2)最大堆大小 -Xmx1024m 将最大堆大小设置为1024 mb。

(3)线程堆栈大小 -Xss128m 将线程堆栈大小设置为128mb。

(4)新生代堆大小 -Xmn256m 将新生代堆大小设置为256mb。

7.若是Java应用程序崩溃并出现OutOfMemoryError,你须要一些额外的信息来检测漏洞,运行如下进程:-XX:HeapDumpOnOutOfMemory,它将在下次发生此错误时建立堆转储文件。

8.使用-verbose:gc选项获取垃圾回收输出。 每次进行垃圾回收时,都会生成一个输出

总结

从内存资源的角度看,了解内存是如何组织的,会为你编写良好、优化的代码提供优点。这样作的好处是,你能够经过提供最适合你所运行应用程序的不一样配置,来优化你正在运行的JVM。若是使用正确的工具,发现和修复内存漏洞只是一件容易的事情。

最后

欢迎你们一块儿交流,喜欢文章记得点个赞哟,感谢支持!