《深入理解 Java 虚拟机 1》Java 内存区域与内存分配策略
第二章 Java 内存区域与内存溢出异常
=======================
我们知道在 C++语言里,如果想使用一个对象,需要对其进行 new 操作,如果不用这个对象了,需要对其进行 delete 操作,一旦开发人员忘记写 delete 语句,就会造成内存泄漏。
而 java 就很聪明,它将手动改为自动,把内存的控制权交给了虚拟机,下面我们就来探究一下 JVM 是怎么进行自动内存管理的。
手动内存管理分为两部分:给对象分配内存和回收分配给对象的内存。
一、运行时数据区域
线程公有
在运行时数据区中,方法区和堆是属于线程公有的,也就是两块区域是循环利用的,所以要对其进行垃圾回收。
线程私有
程序计数器、虚拟机栈、本地方法栈是属于线程私有的,与其线程“同生共死”,属于一次性的,不需要进行垃圾回收。
1、程序计数器
程序计数器中存放的是当前线程所执行的字节码的行号。jvm 工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
2、Java 虚拟机栈
Java 虚拟机栈是线程私有的,它的声明周期与线程相同。
虚拟机栈里面存储的是栈帧,栈帧里面存储的是局部变量表,操作数栈,动态链接,方法出口等信息。
栈中的栈帧
每个方法从调用到执行的过程就是一个栈帧在虚拟机栈中入栈到出栈的过程。
栈帧中的局部变量表
存放的是编译期可知的各种基本数据类型,对象引用类型。所以其所需要的内存空间在编译期间就能完成分配,在运行期间不会改变其大小。
在分配基本数据类型所占的空间时,除了 64 位的 long 和 double 类型的数据会占用 2 个局部变量空间,其余的数据类型只占用 1 个。
3、本地方法栈
本地方法栈和虚拟机栈的作用是相同的,只不过虚拟机栈执行的是 java 方法,本地方法栈执行的是 Native 方法。
java 方法就是开发人员写的 java 代码,Native 方法就是一个 java 调用非 java 代码的接口。
4、Java 堆
如果说栈解决的是程序运行问题,即程序如何处理数据;则堆解决的是数据存储问题,即数据怎么放,放在哪。
此内存区域的唯一目的是存放对象实例,Java 堆是垃圾收集器管理的主要区域。
特定:堆是虚拟机内存中最大的一块,大概占内存的三分之二,堆可处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
5、方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也可以看作是 Java 堆的一部分。
这部分区域可以不选择垃圾回收,这区域的内存回收主要针对常量池的回收和对类型的卸载。
这部分可能会导致未完全回收而导致内存泄漏。
二、java8 内存模型-永久代(PermGen)和元空间(Metaspace)
1、PermGen(永久代)
绝大部分程序员都见过"java.lang.OutOfMemoryError:?PermGen?space?"这个异常。这里“PermGen space”其实指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是 JVM 规范,而后者是 JVM 规范的一种实现,并且只有 HotSpot 才有“PermGen space”,而对于其他类型的虚拟机,如 JRockit(Oracle)、J9(IBM) 并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。我们现在通过动态生成类来模拟“PermGen?space”的内存溢出:
package com.paddx.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 对象来加载 Test 类,从而生成不同的类对象,这样就能看到我们熟悉的?"java.lang.OutOfMemoryError:?PermGen?space?" 异常了。这里之所以采用 JDK 1.7,是因为在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。下面我们就来看看 Metaspace 与 PermGen space 的区别。
2、Metaspace(元空间)
其实,移除永久代的工作从 JDK1.7 就开始了。JDK1.7 中,存储在永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap。但永久代仍存在于 JDK1.7 中,并没完全移除,譬如符号引用(Symbols)转移到了 native heap;字面量(interned strings)转移到了 java heap;类的静态变量(class statics)转移到了 java heap。我们可以通过一段程序来比较 JDK 1.6 与
JDK 1.7 及 JDK 1.8 的区别,以字符串常量为例:
package com.paddx.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 的运行结果:
从上述结果可以看出,JDK 1.6 下,会出现“PermGen Space”的内存溢出,而在 JDK 1.7 和 JDK 1.8 中,会出现堆内存溢出,并且 JDK 1.8 中 PermSize 和 MaxPermGen 已经无效。因此,可以大致验证 JDK 1.7 和 1.8 将字符串常量由永久代转移到堆中,并且 JDK 1.8 中已经不存在永久代的结论。现在我们看看元空间到底是一个什么东西?
元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时 GC 会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 MaxMetaspaceSize 时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在 GC 之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
现在我们在 JDK 8 下重新运行一下代码段 4,不过这次不再指定 PermSize 和 MaxPermSize。而是指定 MetaSpaceSize 和 MaxMetaSpaceSize 的大小。输出结果如下:
从输出结果,我们可以看出,这次不再出现永久代溢出,而是出现了元空间的溢出。
第三章 垃圾收集器与内存分配策略
====================
一、内存分配
这部分我们说一下对象在 java 堆中是如何分配、布局、访问以及内存分配的原则。
1、对象的创建
我们用 new 来创建对象,来看看系统运行到 new 时,虚拟机在干什么。此时的类就像一块肉,他要经过层层安检,才能到达人类的饭桌。
(1)查看在常量池中是否有对应的符号引用。【在方法区中进行】
(2)查看此类是否被加载、解析和初始化过。【在方法区中进行】
(3)领取新生对象的内存。有两种方式:指针碰撞和空闲列表。【在堆中进行】
(4)将分配到的内存空间初始化为零。
(5)对对象进行必要的设置,比如其实哪个类的实例,对象的哈希码之类的。这些信息存放在对象的对象头中。
(6)如果 java 代码对对象进行了赋值,则会走到第六步,执行<init>方法。此方法的作用就是对对象进行初始化。
2、对象的内存布局
对象在内存中的存储布局分为三个部分:对象头+实例数据+对其补充
对象头
对象头里面有两部分信息:
(1)运行时数据,包括哈希码、GC 分代年龄、锁状态标志灯。
(2)类型指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
实例数据中存放的是代码中定义的各种类型的字段内容。
对其填充
对齐填充起的是占位符的作用,不是必然存在的,其只要保证对象的大小是 8 字节的整数倍即可。
3、对象的访问定位
建立完对象后,我们就可以使用对象了。通过句柄和直接指针两种方式。
句柄
评论