一. JVM内存模型
根据JVM规范,JVM内存分为五个部分:虚拟机堆栈、堆、方法区、程序计数器和本地方法堆栈。
- 虚拟机堆栈:每个线程都有一个私有堆栈,该堆栈在创建线程时创建。堆栈内部是一种称为“堆栈帧”的东西。每个方法将创建一个堆栈帧。堆栈帧存储局部变量表(基本数据类型和对象引用)、操作数堆栈、方法退出和其他信息。堆栈的大小可以固定,也可以动态扩展。当堆栈调用深度大于JVM允许的范围时,将抛出stackoverflowerr错误,但此深度范围不是一个常量值。我们可以按照以下步骤测试此结果:
堆栈溢出测试源代码:
package com.javakk.test.memory;
public class StackErrorMock {
private static int index = 1;
public void call(){
index++;
call();
}
public static void main(String[] args) {
StackErrorMock mock = new StackErrorMock();
try {
mock.call();
}catch (Throwable e){
System.out.println("Stack deep : "+index);
e.printStackTrace();
}
}
}
代码段1
运行三次后,可以看到每个堆栈的深度不同,输出如下
至于红框中的值是如何产生的,您需要深入研究JVM的源代码来探索它。这里不再赘述。
除了上述错误外,虚拟机堆栈还有另一个错误,即当应用程序没有空间时,将抛出OutOfMemoryError。这里有一个小细节需要注意,catch catches Throwable
而不是Exception。因为StackOverflowError
和OutOfMemoryError
都是Exception的子类。
2. 局部方法栈:
这部分主要涉及到虚拟机使用的本机方法。一般来说,Java应用程序程序员不需要关心这部分内容。
3. PC寄存器:
PC寄存器,也称为程序计数器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。如果执行当前的JVM方法,则当前执行的指令的地址将保存在此寄存器中;如果执行本机方法,则PC寄存器为空。
4. 堆
堆内存是JVM所有线程的共享部分,在虚拟机启动时创建。所有对象和数组都在堆上分配。这个空间可以被GC回收。当没有空间请求时,抛出OutOfMemoryError
。下面我们简单地模拟堆内存溢出情况:
package com.javakk.test.memory;
import java.util.ArrayList;
import java.util.List;
public class HeapOomMock {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<byte[]>();
int i = 0;
boolean flag = true;
while (flag){
try {
i++;
List.add(new byte[1024 * 1024]);//Add a 1M size array object each time
}catch (Throwable e){
e.printStackTrace();
flag = false;
System.out.println("count="+i);//The number of times the record was run
}
}
}
}
代码段2
运行以上代码,输出如下:
注意这里我指定的堆内存大小是16M,所以这里显示count=14(这个数字不固定),至于为什么是14还是其他的数字,需要根据GC日志来判断,具体原因会在文章中向大家解释。
5. 方法区域:
方法区域也由所有线程共享。主要用于存储类信息、常量池、方法数据、方法代码等。方法区域在逻辑上是堆的一部分,但为了区别于堆,通常称为“非堆”。下面将更详细地讨论方法区域中的内存溢出问题。
二. 永久代(PermGen)
大多数Java程序员应该看到java.lang.OutOfMemoryError:PermGen space
“异常。这里的“PermGen空间”指的是方法区域。然而,方法面积和“PermGen空间”本质上是不同的。前者是JVM规范,后者是JVM规范的实现,只有HotSpot有“PermGen space”,而其他类型的虚拟机,如JRockit(Oracle),J9(IBM)没有“PermGen space”。由于方法区主要存储类的相关信息,因此对于动态生成的类,更容易发生永久内存溢出。最典型的情况是,在大量jsp页面的情况下,很容易出现永久内存溢出。我们现在通过动态生成类来模拟“PermGen space”的内存溢出:
package com.javakk.test.memory;
public class Test {
}
代码片段3:
package com.javakk.test.memory;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
public class PermGenOomMock{
public static void main(String[] args) {
URL url = null;
List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>();
try {
url = new File("/tmp").toURI().toURL();
URL[] urls = {url};
while (true){
ClassLoader loader = new URLClassLoader(urls);
classLoaderList.add(loader);
loader.loadClass("com.paddx.test.memory.Test");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行后的结果:
本例中使用的JDK版本是1.7,指定的PermGen区域是8M,每次生成不同的URLClassLoader对象时都会加载测试类,从而产生不同的类对象,这样我们就可以看到熟悉的java.lang.OutOfMemoryError:PermGen space
“异常。这里之所以使用jdk1.7,是因为在jdk1.8中,HotSpot没有“PermGen-space”间隔,而是一种叫做Metaspace的东西。让我们看看元空间和PermGen空间之间的区别。
三. 元空间(Metaspace)
事实上,永久代的删除始于JDK1.7。在JDK1.7中,永久生成中存储的一些数据已被传输到Java堆或本机堆。但是,永久性的一代仍然存在于JDK1.7中,并且没有被完全删除。例如,符号已传输到本机堆;内部字符串已传输到java堆;类的静态变量已传输到java。堆。我们可以通过一个程序来比较JDK1.6和JDK1.7、JDK1.8的区别,以字符串常量为例:
package com.javakk.test.memory;
import java.util.ArrayList;
import java.util.List;
public class StringOomMock {
static String base = "string";
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i=0;i< Integer.MAX_VALUE;i++){
String str = base + base;
base = str;
list.add(str.intern());
}
}
}
这个程序连续生成指数为2的新字符串,这样可以更快地消耗内存。我们分别运行JDK 1.6、JDK 1.7和JDK 1.8:
JDK 1.6 的结果:
JDK 1.7 的结果:
JDK 1.8 的结果:
从上面的结果可以看出,在JDK1.6下,会出现“PermGen Space”的内存溢出,在JDK1.7和JDK1.8中会出现堆内存溢出,JDK1.8中的PermSize和MaxPermGen没有影响。因此,您可以大致验证jdk1.7和1.8是否将字符串常量从永久生成转移到堆,并且jdk1.8中没有永久生成。现在让我们看看元空间是什么。
元空间的性质与永久生成类似,是JVM规范中方法区域的实现。但元空间和永久生成的最大区别在于,元空间不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制,但可以使用以下参数指定元空间的大小:
-XX: MetaspaceSize
,初始空间大小。当达到此值时,它将触发垃圾回收以进行类型卸载。同时,GC会调整该值:如果释放了大量空间,则会适当降低该值;如果释放了少量空间,则在不超过MaxMetaspaceSize时适当增加该值。
-XX: MaxMetaspaceSize
,最大空间,默认为无限制。
除了上面指定的两个选项外,还有两个与GC相关的属性:
-XX: MinMetaspaceFreeRatio
,GC后,元空间剩余空间的最小百分比,减少为分配的空间导致的垃圾回收
-XX: MaxMetaspaceFreeRatio
,GC后,元空间剩余空间的最大百分比,减少为垃圾回收所造成的空间释放
现在让我们在jdk8下重新运行代码段4,但这次我们不再指定PermSize和MaxPermSize。相反,请指定MetaSpaceSize和MaxMetaSpaceSize的大小。输出如下:
从输出可以看出,这次不再是永久代的溢出,而是元空间的溢出。
关于metaspace内存溢出排查细节也可以参考之前的文章:https://javakk.com/160.html
四. 总结
通过以上分析,您应该对JVM的内存分区有一个大致的了解,同时也了解jdk8中永久元空间的转换。但每个人都应该有一个问题,为什么要转换?因此,最后,我想总结以下原因:
- 字符串存在于永久代中,容易出现性能问题和内存溢出。
- 类和方法信息很难确定其大小,因此永久代的大小很难指定,太小容易发生永久代溢出,太大则有可能导致老年代溢出。
- 永久代给GC垃圾回收带来了不必要的复杂性,回收效率低。
- 甲骨文可能会将HotSpot和JRockit结合在一起。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/490.html
暂无评论