1 JVM内存布局图
整体上来看,JVM的内存分为堆区和非堆区,而非堆区又包括了方法区、JVM栈、本地方法栈、程序计数器等。
2 JVM运行时数据区划分
2.1 JVM堆
其主要作用是用于为几乎所有的对象实例和数组实例的实例化提供内存空间。说通俗点,所有采用new关键字产生的对象的空间都在此分配。 如:Map<String,String> map = new HashMap<>();
这个map所引用的对象即位于JVM的堆中。 JVM heap的特点是:
- 内存不一定连续分配,只要逻辑上是连续的即可;
- 内存的大小可以通过JVM的参数来控制:如 -Xms = 1024M -Xmx = 2048M; 表示JVM Heap的初始大小为1GB,最大可自动伸缩到2GB;
- 平时所说的Java内存管理即指的是内存管理器对这部分内存的管理(创建/回收);
- 所有线程共享;
2.2 JVM栈
JVM栈是伴随着线程的产生而产生的,属于线程私有区域,生命周期和线程保持一致,所以,JVM的内存管理器对这部分内存无需管理; 从图1.1中可以看出,栈中有包含了多个栈帧(frame),frame 伴随的方法的调用而产生,方法调用完毕,frame也就出栈,从JVM栈中被移除。frame主要保存方法的局部变量、操作数栈、动态链接、方法出口(reternAddress或者抛Exception)。 虚拟机规范中,对此内存区域规定了两种异常情况:
- 当线程请求的栈的深度大于JVM所允许的最大深度,则抛出
StackOverflowError
; - 当栈的内存是可动态扩展的时候,如果扩展时发现堆内存不足时,会抛出
OutofMemoryError
;
2.3 PC(程序计数器)
简称PC(Program Counter Register),为线程私有,如果执行的是一个Java方法,那么此时PC里保存的是当前Java字节码的指令地址;如果说执行的是Native方法,那么PC的内容则为空。
2.4 方法区
主要用于存储已经由JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Hotspot JVM中,设计者使用“永久代”来实现方法区,这样带来的好处是,JVM可以不用再特别的写代码来管理方法区的内存,而可以像管理 JVM 堆一样来管理。带来的弊端在于,这很容易造成内存溢出的问题。我们知道,JVM的永久代使用 --XX:MAXPermSize来设定永久代的内存上限。 值得一提的是,运行时常量池也属于方法区的一部分,所以,它的大小是受到方法区的限制的。运行期间也可以将新的常量放入池中,运用的比较多的就是String的intern()方法。 注意:
- 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
- 在JDK7.0版本,字符串常量池被移到了堆中了。
2.5 本地方法栈
与JVM栈非常类似,只不过,本地方法栈是为Java的Native method方法调用的时候服务。JVM规范并未定义如何实现该区域,但是,在Hotspot JVM中,本地方法栈和JVM栈合二为一,所以,本地方法栈同样会抛出StackOverflowError
和OutofMemoryError
。
3 GC(Garbage Collection)回收机制
3.1 基本概念
垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
- 引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(引用都有哪些?对垃圾回收又有什么影响?)
- 垃圾:无任何对象引用的对象(怎么通过算法找到这些对象呢?)。
- 回收:清理“垃圾”占用的内存空间而非对象本身(怎么通过算法实现回收呢?)。
- 发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中(堆内存为了配合垃圾回收有什么不同区域划分,各区域有什么不同?)。
- 发生时间:程序空闲时间不定时回收(回收的执行机制是什么?是否可以通过显示调用函数的方式来确定的进行回收过程?)
3.2 判断 “垃圾” 的方法
垃圾收集器会对堆进行回收前,判断对象中哪些是“存活”,哪些是“死亡”。
- 引用计数算法
每当一个地方引用它时,计数器+1;引用失效时,计数器-1;计数值=0 —— 不可能再被引用。
缺点:对象之间相互矛盾循环引用的问题。
- 可达性分析算法
把一系列“GC Roots”作为起始点,从节点向下搜索,路径称为引用链,当一个对象到GC Roots没有任何引用链相连,即不可达时,则证明此对象时不可用的。
3.3 垃圾回收算法
- 标记清除算法
标记-清除(Mark-Sweep)算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就可以了)。然后,在清除阶段,清除所有未被标记的对象。
- 标记整理算法
标记整理算法类似与标记清除算法,不过它标记完对象后,不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
- 复制算法
复制算法可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。
3.4 垃圾回收流程
在JVM的内存空间中把堆空间分为年老代和年轻代。将大量(据说是90%以上)创建了没多久就会消亡的对象存储在年轻代,而年老代中存放生命周期长久的实例对象。年轻代中又被分为Eden区(圣经中的伊甸园)、和两个Survivor区。新的对象分配是首先放在Eden区,Survivor区作为Eden区和Old区的缓冲,在Survivor区的对象经历若干次收集仍然存活的,就会被转移到年老区。
- 新建的对象,大部分存储在Eden区中。
- 当Eden内存不够,就进行Minor GC释放掉不活跃对象;然后将部分活跃对象复制到Survivor区中(如Survivor1),同时清空Eden区。
- 当Eden区再次满了,将Survivor1中不能清空的对象存放到另一个Survivor中(如1. Survivor2),同时将Eden区中的不能清空的对象,复制到Survivor1,同时清空Eden区。
- 重复多次(默认15次):Survivor中没有被清理的对象就会复制到老年区(Old)。
- 当Old达到一定比例,则会触发Major GC释放老年代。
- 当Old区满了,则触发一个一次完整的垃圾回收(Full GC)。
- 如果内存还是不够,JVM会抛出内存不足,发生oom,内存泄漏。
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)