JVM 运行时数据区域
简要模型
jdk1.8前

jdk1.8后

程序计数器
记录正在执行的虚拟机字节码指令的地址,它可以看做是当前线程所执行的字节码的行号指示器 。
如果正在执行的是Native方法(本地方法)则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。
为什么不会内存溢出
OutOfMemoryError出现场景是:比如某个线程递归调用,随着调用层次的加深,可能会出现栈空间不足的情况,这时候如果可以动态扩增,jvm就会向申请更多的内存空间来扩充栈,当没有更多的内存空间得以申请的时候,就会发生OutOfMemoryError。
而程序技术器保存的是当前执行的字节码的偏移地址,当执行到下一条指令的时候,改变的只是程序计数器中保存的地址,并不需要申请新的内存来保存新的指令地址,因此,永远都不可能内存溢出。
为什么执行Native方法时为空
因为本地方法是 C++/C 写的,由系统调用,根本不会产生字节码文件,因此,程序计数器也就不会做任何记录
Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。
从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异 常)都算作方法结束。
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。
运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
GC堆
是被线程共享的一块内存区域,所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同的代采取不同的垃圾回收算法。可以将堆分成两块:
- 新生代(Young Generation)
- 老年代(Old Generation)
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
新生代
一般情况下,会将新创建的对象都会存放到新生代中(大对象除外)。
新生代中对象大多都是存活时间不长的,并且不大的对象。
新生代使用的是复制算法,为了方便使用复制算法,新生代又分出了一个Eden区,两个 Survivor区
Eden区
Eden区位于Java堆的年轻代,是新对象分配内存的地方,由于堆是所有线程共享的,因此在堆上分配内存需要加锁。而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC(young GC)。
Survival from to
Survival区与Eden区相同都在Java堆的年轻代。Survival区有两块,一块称为from区,另一块为to区,这两个区是相对的,在发生一次Minor GC后,from区就会和to区互换。在发生Minor GC时,Eden区和Survivalfrom区会把一些仍然存活的对象复制进Survival to区,并清除内存。Survival to区会把一些存活得足够旧的对象移至年老代。
流程:
当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden和from Suvivor,如果对象仍然存活,则复制到to Suvivor Space,如果to Suvivor已经满了,则会复制到老年代。同时,在扫描Suvivor Space时,如果对象已经经过了几次的扫描仍然存活,JVM认为其为一个持久化对象,则将其移到老年代。扫描完毕后,JVM将Eden和from Suvivor清空,然后交换from和to的角色(即下次垃圾回收时会扫描Eden Space和to Suvivor Space。这么做的好处是减少了内存碎片的产生。)
年轻代垃圾回收时,采用将存活对象复制到到空的Suvivor Space的方式来确保尽量不存在内存碎片,采用空间换时间的方式来加速内存中不再被持有的对象尽快能够得到回收。
这三个区域默认情况下是按照8:1:1分配,也可以手动配置。
老年代
在新生代没进行一次垃圾回收后,就会给存活的对象“加一岁”,当年龄达到一定数值后就会进入老年代(默认是15,可以调整-XX:MaxTenuringThreshold来设置)。
除此之外,比较大的对象(即需要分配一块较大的连续内存空间)也会进入老年代,可以-XX:PretenureSizeThreshold进行设置。
如-XX:PretenureSizeThreshold3M,那么大于3M的对象就会直接就进入老年代。
因此,老年代中存放的都是一些生命周期较长的对象或者特别大的对象。
老年代使用的是标记整理/清除算法。(具体使用那种看垃圾回收器)
当年老代容量满的时候,会触发一次Major GC(full GC),回收年老代和年轻代中不再被使用的对象资源。
永久代
其实就是jvm的方法区,在这里面会存储被虚拟机所加载的类信息(还有动态生成的类)的静态文件,因此这里比新生代和老年代更不容易回收。
元空间
从JDK8开始,就是用元空间替代了永久代,元空间并不在虚拟机中,而是使用本地内存。
那么,正常情况下元空间的大小仅受本地内存限制。
垃圾回收
垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
判断一个对象是否需要回收
引用计数
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class Test {
public Object instance = null;
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.instance = b;
b.instance = a;
a = null;
b = null;
doSomething();
}
}
在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
根可达
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
类型
Minor GC
当Eden区满时,触发Minor GC。
对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
Full GC
也叫 Major GC,对整个堆进行回收,包括新生代和老年代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。
垃圾回收算法
分代收集算法
它的核心思想是将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以在不同代采取不同的收集算法。
在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。
复制算法
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法回收新生代,但是并不是划分为大小相等的两块,而是一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象全部复制到另一块 Survivor 上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 就不够用了,此时需要依赖于老年代进行空间分配担保,也就是借用老年代的空间存储放不下的对象。
适用场合:
- 存活对象较少的情况下比较高效(复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。)
- 扫描了整个空间一次(标记存活对象并复制移动)
- 适用于年轻代(即新生代):基本上98%的对象是"朝生夕死"的,存活下来的会很少
缺点:
- 需要一块儿空的内存空间
- 需要复制移动对象
标记清除
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
在标记阶段首先通过根节点(GC Roots),标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。
适用场合:
- 存活对象较多的情况下比较高效
- 适用于年老代(即旧生代)
缺点:
- 容易产生内存碎片,再来一个比较大的对象时(典型情况:该对象的大小大于空闲表中的每一块儿大小但是小于其中两块儿的和),会提前触发垃圾回收
- 扫描了整个空间两次(第一次:标记存活对象;第二次:清除没有标记的对象)
标记整理
标记-整理算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。
首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
优点:既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
不足:需要移动大量对象,处理效率比较低。
JVM 参数选项
-Xms | 初始堆大小。如:-Xms256m |
---|---|
-Xmx | 最大堆大小。如:-Xmx512m |
-Xmn | 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% |
-Xss | JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。 |
-XX:NewRatio | 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3 |
-XX:SurvivorRatio | 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 |
-XX:PermSize | 永久代(方法区)的初始大小 |
-XX:MaxPermSize | 永久代(方法区)的最大值 |
-XX:+PrintGCDetails | 打印 GC 信息 |
-XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分 |
参考
https://www.pianshen.com/article/55121297501/
https://blog.youkuaiyun.com/seu_calvin/article/details/51404589
https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md
打印 GC 信息 |
| -XX:+HeapDumpOnOutOfMemoryError | 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分 |
参考
https://www.pianshen.com/article/55121297501/
https://blog.youkuaiyun.com/seu_calvin/article/details/51404589
https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md