jvm工作原理
您可能会认为,如果您使用Java编程,那么您需要了解什么关于内存如何工作的知识?Java有自动内存管理,一个安静的垃圾收集器,安静地工作在后台清理未使用的对象并释放一些内存。
因此,作为一个Java程序员,您不需要用诸如销毁对象之类的问题来困扰自己,因为它们已经不再使用了。然而,即使这个过程在Java中是自动的,它也不能保证任何事情。由于不知道垃圾收集器和Java内存是如何设计的,所以即使您不再使用垃圾收集器,也可以有不符合垃圾收集的对象。
因此,了解内存如何在Java中工作是很重要的,因为它为您提供了编写高性能和优化的应用程序的优势,这些应用程序永远不会使用OutOfMemoryError崩溃。另一方面,当你发现自己处于困境时,你会很快发现内存泄漏。
首先,让我们来看看在Java中内存是如何组织的:
一般来说,内存被分成两大部分:堆栈和堆。请记住,在这幅图中,内存类型的大小与现实中的内存大小并不成比例。与堆栈相比,堆是巨大的内存。
堆栈
堆栈内存负责保存对堆对象的引用,以及存储值类型(Java中也称为基本类型),它保存值本身,而不是从堆中引用对象。
此外,栈上的变量具有一定的可见性,也称为范围。只使用活动范围中的对象。例如,假设我们没有全局范围变量(字段)和局部变量,如果编译器执行一个方法的主体,它只能访问方法主体内的堆栈中的对象。它不能访问其他局部变量,因为这些变量超出了范围。一旦方法完成并返回,堆栈的顶部就会弹出,并且活动范围会发生变化。
也许你注意到在上面的图片中,显示了多个堆栈记忆。这是因为Java中的堆栈内存是按线程分配的。因此,每次创建和启动线程时,它都有自己的堆栈内存——无法访问另一个线程的堆栈内存。
堆
这部分内存将实际对象存储在内存中。这些是由堆栈中的变量引用的。例如,让我们分析一下下面一行代码中发生了什么:
StringBuilder builder = new StringBuilder();
new关键字负责确保堆上有足够的空闲空间,在内存中创建StringBuilder类型的对象,并通过“builder”引用来引用它,该引用将位于堆栈上。
每个运行的JVM进程只存在一个堆内存。因此,不管运行了多少线程,这都是内存的共享部分。实际上,堆结构与上面图中所示的有点不同。堆本身被分成几个部分,便于垃圾收集的过程。
最大堆栈和堆大小不是预定义的——这取决于运行的机器。但是,在本文的后面部分,我们将研究一些JVM配置,这些配置将允许我们为运行的应用程序显式地指定它们的大小。
引用类型
如果仔细查看内存结构图,您可能会注意到,表示从堆中引用对象的箭头实际上是不同类型的。这是因为,在Java编程语言中,我们有不同类型的引用:强、弱、软和虚引用。引用类型之间的区别在于,它们引用的堆上的对象符合不同标准下的垃圾收集。让我们仔细看看每一个。
1。强引用
只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();
//可直接通过obj取得对应的对象 如obj.equels(new Object());
而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。所以要最后置空 obj=null;
2。弱引用
简单地说,从堆中对对象的弱引用最有可能在下一次垃圾收集过程之后继续存在。
第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
弱引用创建如下:
WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());
弱引用的一个很好的用例是缓存场景。假设您检索了一些数据,并且希望它也存储在内存中——同样的数据可以再次被请求。另一方面,您不确定何时、或是否将再次请求此数据。因此,您可以对它保持一个弱引用,并且在垃圾收集器运行时,它可能会破坏您在堆上的对象。因此,在一段时间之后,如果您想要检索您所引用的对象,您可能会突然返回一个空值。缓存场景的一个很好的实现是集合WeakHashMap。如果我们在Java API中打开WeakHashMap类,我们会看到它的条目实际上扩展了弱引用类,并使用它的ref字段作为映射的键:
/**
* The entries in this hash table extend WeakReference, using its main ref
* field as the key.
*/
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());
非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;
软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
4 虚引用
用于安排死后清理操作,因为我们知道对象不再是活的。仅使用引用队列,因为此类引用的.get()方法总是返回null。这些类型的引用被认为比终结器更好。
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
pf.isEnQueued();//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。
虚引用主要用于检测对象是否已经从内存中删除。
如何引用字符串
Java中的字符串类型有一点不同。字符串是不可变的,这意味着每当你用一个字符串做某件事时,另一个对象实际上是在堆上创建的。对于字符串,Java在meory中管理一个字符串池。这意味着在可能的情况下,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");
}
运行时,打印输出如下:
字符串是相等的
因此,在比较了字符串类型的两个引用之后,它们实际上指向堆上的相同对象。但是,这对于计算的字符串无效。让我们假设在上面的代码行//1中有以下更改。
String localPrefix = new Integer(297).toString(); //1
输出:
字符串是不同的
在这种情况下,我们实际上看到了堆上有两个不同的对象。如果我们认为计算的字符串经常被使用,我们可以通过在计算字符串的末尾添加.intern()方法来强制JVM将其添加到字符串池中:
String localPrefix = new Integer(297).toString().intern(); //1
添加上述更改将创建以下输出:
字符串是相等的
垃圾收集过程
正如前面所讨论的,根据引用的类型,从堆栈中一个变量从堆中保存一个对象,在某个时间点,该对象将成为垃圾收集器的合格对象。
例如,所有红色的对象都有资格被垃圾收集器收集。您可能会注意到,堆上有一个对象,它对其他在堆上的对象有很强的引用(例如,可以是一个列表,它引用了它的条目,或者一个有两个引用类型字段的对象)。但是,由于堆栈的引用丢失了,所以不能再访问它,所以它也是垃圾。
为了深入了解细节,让我们先提几点:
这个过程是由Java自动触发的,它取决于Java何时以及是否开始这个过程。
这实际上是一个昂贵的过程。当垃圾收集器运行时,应用程序中的所有线程都暂停(取决于GC类型,稍后将讨论)。
这实际上是一个比垃圾收集和释放内存更复杂的过程。
即使Java决定何时运行垃圾收集器,您也可以显式地调用System.gc(),并期望垃圾收集器在执行这一行代码时运行,对吗?
这是一个错误的假设。
你只需要让Java运行垃圾收集器,但是,不管是否要这样做,它都是。无论如何,不建议使用显式调用System.gc()。
由于这是一个相当复杂的过程,而且它可能会影响您的性能,所以它是以一种巧妙的方式实现的。一个所谓的“标记和扫描”过程就是用来做这个的。Java从堆栈中分析变量,并“标记”所有需要保存的对象。然后,清除所有未使用的对象。
实际上,Java不收集任何垃圾。事实上,垃圾越多,被标记的对象越少,进程就越快。为了使这个更加优化,堆内存实际上由多个部分组成。我们可以使用JVisualVM来可视化内存使用和其他有用的东西,这是Java JDK附带的一个工具。唯一需要做的就是安装一个名为visualgc的插件,它允许您查看内存是如何构造的。让我们放大一点,把大图片分解一下:
当一个对象被创建时,它被分配到Eden(1)空间。因为伊甸园的空间不是那么大,它很快就会满了。垃圾收集器运行在Eden空间上,并将对象标记为alive。
一旦一个对象在垃圾收集过程中幸存下来,它就会被转移到一个所谓的幸存者空间S0(2)。垃圾收集器第二次运行在Eden空间上,它将所有幸存的对象移动到S1(3)空间中。此外,当前S0(2)上的所有内容都被移动到S1(3)空间中。
如果一个对象能够存活到X轮的垃圾收集(X依赖于JVM实现,在我的例子中是8),它很可能会永远存在,并且会被移动到旧的(4)空间中。
到目前为止,如果查看垃圾收集器图(6),每次运行时,您都可以看到对象切换到幸存者空间,而Eden空间获得了空间。诸如此类。旧的一代也可以被垃圾收集,但是由于它与Eden空间相比,它是内存中更大的一部分,所以它不会经常发生。Metaspace(5)用于存储关于JVM中加载类的元数据。
所展示的图片实际上是一个Java 8应用程序。在Java 8之前,内存的结构有点不同。metaspace被称为PermGen。空间。例如,在Java 6中,这个空间还存储了字符串池的内存。因此,如果在Java 6应用程序中有太多的字符串,它可能会崩溃。
垃圾收集器类型
实际上,JVM有三种类型的垃圾收集器,程序员可以选择应该使用哪一种。默认情况下,Java选择基于底层硬件的垃圾收集器类型。
1。串行GC -单个线程收集器。主要适用于小数据应用程序。可以通过指定命令行选项来启用:-XX:+UseSerialGC ?
2。并行GC -甚至从命名上来说,串行和并行的区别在于并行GC使用多个线程来执行垃圾收集过程。这种GC类型也称为吞吐量收集器。它可以通过显式指定选项来启用:-XX:+UseParallelGC。
3所示。大多数并发GC——如果您还记得,本文前面提到过,垃圾收集过程实际上非常昂贵,当它运行时,所有线程都暂停了。但是,我们有这个大多数并发的GC类型,它声明它与应用程序并行工作。然而,有一个原因,为什么它是“主要”并发。它不能与应用程序同时工作100%。有一段时间线程暂停。不过,暂停的时间尽可能短,以实现最佳的GC性能。实际上,有两种主要并发GCs:
3.1垃圾优先-高吞吐量与合理的应用暂停时间。启用选项:-XX:+UseG1GC。
3.2并发标记扫描-应用暂停时间保持在最小值。它可以通过指定选项来使用:-XX:+UseConcMarkSweepGC。在JDK 9中,该GC类型被弃用。
提示和技巧
为了最小化内存占用,尽量限制变量的范围。请记住,每次弹出堆栈的顶部范围时,都会丢失该范围的引用,这会使对象有资格进行垃圾收集。
显式引用空过时的引用。这将使对象成为垃圾收集的合格对象。
避免终结器。他们减慢了进程,他们不能保证任何事情。对于清理工作来说,更喜欢幽灵的引用。
在弱引用或软引用时不要使用强引用。最常见的内存陷阱是缓存场景,当数据被保存在内存中,即使它可能不需要。
JVisualVM还具有在某个点生成堆转储的功能,因此您可以在每个类中分析它占用了多少内存。
根据应用程序需求配置JVM。在运行应用程序时,显式地为JVM指定堆大小。内存分配过程也很昂贵,因此为堆分配一个合理的初始和最大内存数量。如果您知道从一开始就使用一个小的初始堆大小是没有意义的,那么JVM将扩展这个内存空间。使用以下选项指定内存选项:
初始堆大小- xms512m -将初始堆大小设置为512mb。
最大堆大小- xmx1024m -将最大堆大小设置为1024兆字节。
线程堆栈大小- xss128m -将线程堆栈大小设置为128兆字节。
年轻一代的大小- xmn256m -将年轻一代的大小设置为256兆字节。
如果Java应用程序使用OutOfMemoryError崩溃,并且需要一些额外的信息来检测泄漏,那么使用-XX:HeapDumpOnOutOfMemory参数来运行这个过程,当下一次错误发生时,它将创建一个堆转储文件。
使用-verbose:gc选项来获得垃圾收集输出。每次发生垃圾收集时,都会生成一个输出。
结论
了解内存是如何组织的,可以使您在内存资源方面编写良好和优化的代码。您可以通过提供最适合您的运行应用程序的不同配置来优化运行的JVM。如果使用正确的工具,发现和修复内存泄漏是很容易的事情。