JVM之运行时数据区(二)方法区与堆

上篇博客讲到程序计数器与虚拟机栈,这次我们来聊一聊方法区与堆(Heap),还有本地方法栈。

本地方法栈

简单的说本地方法栈我们没有必要了解太多,本地方法栈(Native Method Stack)的作用域虚拟机栈所发挥的作用类似,不过通过名字我们也能推测出,本地方法栈是用来执行Native方法的。它也会抛出stackOverflowerror和OutOfMemoryError。

堆Heap

我们先来看下这幅图先来了解下:

java中的堆是java虚拟机中所管理的内存中最大的一块。从上面的图片中我们可以按到。java Heap是被线程共享的。在java虚拟机规范中说明:所有对象实例以及数组都要在堆上进行分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换技术将会导致一些微妙的变化发生,所有对象都分配在堆上也不在那么绝对了,也就是说堆是用来存放对象的空间,几乎所有的对象都存储在堆中

我们也通过我所画的图形颜色可以看出java中的堆分为新生代与老年代,而新生代有分为Eden空间,From Survivor空间、To Survivor空间,也就是上图中的Eden、s0、s1。

我们在这里先简单介绍下几个概念,至于新生代,老年代以及Eden 与survivor之间的比例如何划分,还有对象如何存放的,我们会在后面的垃圾回收器中会讲到内存的分配策略。

  • 新生代:英文为Young Generation也叫年轻代,通过字面意思,在根据新生代是在堆中我们可以理解为新创建的对象存放的内存空间,但是注意的是由于新生代也有空间的,并不能保证最有的对象会分配到新生代中,一些大对象(需要大量连续内存空间的java对象,例如很长的字符串以及数组:new Byte[100*1024*1024])。
  • 老年代:除了上面所说的大对象放在老年代里,还有一部分是“年龄大”的对象,由于对象的生命周期不一致,有的很早就夭折了,有的比较坚强存活时间长,虚拟机就把这些”老货“安置到老年代中,具体如何进行区分是“老年”与“新生”,我们在后面再进行介绍。
  • Eden:是新生代的一部分,其实在进行划分细的话,新创建的对象时放在新生代中的Eden空间的。
  • Survivor:翻译中文为“幸存”的意思,java虚拟机在开始的时候会将From Survivor 与Eden进行存放对象,在空间满的时候会进行GC(回收),虚拟机会把存活的对象放在To Survivor空间中(如果存放的下的话),然后清空From Survivor+Eden,然后使用To Survivor 与Eden ,当下次进行GC时,虚拟机将Eden+To Survivor中存活的对象放入From Survivor,以此类推。
  • 永久代: 指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。不过在jdk1.8之后被移除。
  • Metaspace( 元空间):从JDK 8开始,Java开始使用元空间取代永久代,元空间并不在虚拟机中,而是直接使用本地内存。

方法区

方法区(Method Area)用来存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。从之前的图中我们也可以看到它与java 对一样也是线程共享的,也可以叫做非堆(Non_Heap)。

有点需要说明的是部分人将方法区成为“永久代”,实际上两者本质上不同,在HotSpot虚拟机上,把GC分代收集扩展至方法区,这样就可以使用java虚拟机来进行管理这部分内存。

到了这个地方我们需要进行分析下,在jdk8中将“永久代”给移除了,那么我们犯迷糊“永久代”,“方法区”到底是什么概念以及jdk中的元数据又是什么?

这里我们先来一个个分析

  • jdk7之前:方法区位于永久代(PermGen), 永久代和堆相互隔离,永久代的大小在启动JVM虚拟机的时可以设定一个固定值,不可变。
  • jdk7中:存储在永久代的部分数据就已经转移到Java Heap或者Native Memory(本地内存,也称为C-Heap,是JVM自身进程使用的,空间不足时不会触发GC),但是永久代依然存在,没有完全移除。 JDK 7的HotSpot VM是把Symbol的存储从PermGen移动到了native memory,并且把静态变量从instanceKlass末尾(位于PermGen内)移动到了java.lang.Class对象的末尾(位于普通Java heap内)。“常量池”如果说的是SymbolTable(符号引用) / StringTable(字符串常量),这俩table自身原本就一直在native memory里,是它们所引用的东西在哪里更有意义。JDK7是把SymbolTable引用的Symbol移动到了native memory,而StringTable引用的java.lang.String实例则从PermGen移动到了普通Java heap(譬如符号引用(Symbols)转移到了native memory;字符串常量池(interned strings)转移到了Java heap;类的静态变量(class statics)转移到了Java heap)。
  • jdk8中:取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但是与堆共享物理内存,逻辑上可认为在堆中。

为什么要移除永久代?

  1. 字符串存在永久代中,容易出现性能问题和内存溢出;
  2. 永久代大小不容易确定,PermSize指定太小容易造成永久代OOM;
  3. 永久代会为GC带来不必要的复杂度,并且回收率偏低;
  4. oracle将HotSpot与JRockit合二为一;

我们这里简单测试下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package jvm;

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());
}
}
}

运行的结果如下,抛出的为Java heap溢出,我们可以判断出字符串在1.8中不是存在永久代而是java堆中。

1
2
3
4
5
6
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at jvm.StringOomMock.main(StringOomMock.java:11)

下面我们进行测试Metaspace溢出,记得设置参数“-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=20M”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package jvm;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

/**
* Metaspace溢出
* @author wsylp
*
*/
public class MetaspaceOOMTest {
public static void main(String[] args) {
try {
//准备url
URL url = new File("C:\\Users\\wsylp\\Desktop\\study\\性能优化\\code").toURI().toURL();
URL[] urls = {url};
//获取有关类型加载的JMX接口
//用于缓存类加载器
List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
while (true) {
//加载类型并缓存类加载器实例
ClassLoader classLoader = new URLClassLoader(urls);
classLoaders.add(classLoader);
classLoader.loadClass("HelloWorld2");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at jvm.MetaspaceOOMTest.main(MetaspaceOOMTest.java:24)

我们可以看出JDK8中永久代已经被移除了,换成了元空间(Metaspace)。

本文标题:JVM之运行时数据区(二)方法区与堆

文章作者:wsylp

发布时间:2019年01月13日 - 20:01

最后更新:2020年01月02日 - 10:01

原始链接:http://wsylp.github.io/2019/01/13/JVM之运行时数据区(二)方法区与堆/

许可协议: 本文为 wsylp 版权所有 转载请保留原文链接及作者。

-------------本文结束感谢阅读-------------