Java内存区域
程序计数器
当前线程所执行字节码的信号指示器;此内存区域是唯一不会有 OOM 的区域
虚拟机栈
生命周期与线程相同;
虚拟机栈描述的是 java 方法执行的内存模型;每个方法执行都会创建一个栈帧用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息;每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟键栈中入栈到出栈的过程
很多人吧 java 内存简单分为"栈"、"堆"这样的分法比较粗糙;可能大家只关心与程序最相关的2块;"栈"就是"虚拟机栈";
存储局部变量表
- 存储了熟悉的各种基本类型:
boolean
(1位)、int
(32位)、long
(64位)、float
(32位)、double
(64位)、char
(16位)、byte
(8位)、short
(16位) - 对象引用 reference类型
java 虚拟机中对这个区域规定了2中异常情况:
- 如果线程请求的
栈深度
大于虚拟机
所允许的深度;抛出StackOverflowError
- 如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存;抛出
OutOfMemoryError
本地方法栈
本地方法栈跟虚拟机栈非常相似;主要的区别是虚拟机栈
为虚拟机执行java
方法服务、本地方法栈
为虚拟机使用到的 Native
方法服务
相同的也会抛出StackOverflowError
、OutOfMemoryError
异常
堆
存放对象实例,几乎所有的对象实例都放在这里分配内测
方法区
用来存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等
会抛出OutOfMemoryError
运行时常量池是方法区的一部分 会抛出
OutOfMemoryError
直接内存
不是 java 虚拟机的一部分,但是这块内存被频繁的使用也会抛出OutOfMemoryError
jdk1.4后加入了NIO
一种基于通道(channel
)与缓冲区(buffer
)的 I/O方式,可使用 Native 函数库直接分配堆外内存,然后通过一个存储在 java堆
中的 DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 java堆
和 native堆
中来回复制数据
自动内存管理机制
Q:垃圾收集器在对堆进行回收前,如何判断
对象
已死 (即不可能再被任何途径使用的对象)
引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它时计数器+1;引用失效计数器-1;任何时刻计数器为0的对象就是不可能再被使用
但是这个针对引用计数算法很难去解决对象之间相互循环引用的问题
可达性分析算法 (虚拟机就是用这个算法)
通过一系列的称为GCRoot
的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain
),当一个对象到GCRoot
没有任何引用链相链接时,则认为该对象不可用的
Q: 当一个对象到
GCRoot
没有任何引用链的时候,这个对象就"非死不可"吗?
可达性分析不可达对象也不是"非死不可",这个时候的他们处于"缓刑"期,要真正宣告一个对象死亡,至少要经历2次标记过程;如果对象"A"在经历可达性分析之后发现没有与 GCRoot
相链接的引用链,那么"A"会被第一次标记并且进行一次筛选,筛选的条件是对象"A"是否有必要执行finalize()
方法。"A"没有重写finalize()
或者finalize()
已经被虚拟机调用过,虚拟机视这2种情况为"没有必要执行"
如果对象"A"被判断有必要执行 finalize()
方法,那么"A"将会被放置在一个叫F-Queue
队列中,并且稍后由虚拟机去执行,但是这个执行是指虚拟机触发这个方法(A.finalize()
),但是虚拟机不会承诺等待它(A.finalize()
)运行结束。
finlize()
方法是对象逃离死亡命运的最后一次机会,稍后 GC
将对F-Queue
中进行第二次小规模的标记,如果A想要在finlize
中成功拯救自己-->只要重新与引用链的任何一个对象建立关联。比如把自己(this
关键字)赋值给某个类的变量或者对象的成员变量;
引用
在JDK1.2之后引用可分为:强引用(Strong Reference
)、软引用(soft Reference
)、弱引用(weak Reference
)、虚引用(Phantom Reference
)
- 强引用(
Strong Reference
)
比较普遍,类似Object obj = new Object() 这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象 - 软引用(
soft Reference
)
用来描述一些还有用但是非必须的对象;
在系统将要发生内存泄露之前,将会把这些对象列进行回收范围之中进行二次回收。如果这次回收还没有足够内存,才会抛出异常 - 弱引用(
weak Reference
)
用来描述非必须的对象;
被引用关联的对象只能生存到下一次垃圾收集发生前.当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象 - 虚引用(
Phantom Reference
)
用的少基本没用到
垃圾回收算法
标记-清除
首先标记所有需要回收对象,在标记完之后统一回收所有被标记的对象
缺点
- 效率问题,标记、清除的效率都不高
- 空间问题,会产生大量不连续的内存碎片
[图片上传失败...(image-dc6116-1525695257170)]
复制算法 (新生代)
将内存划分相等的2块,每次只使用一块,当一块用完了就将存活的对象移动另一块,然后在把已经使用的那块一次清理掉
缺点:代价是将内存为原来的一半
虚拟机中新生代就是用这种算法,因为新生代中的对象98%都是"朝生夕死",新生代中的内存分配是eden
、survivor1
、survivor2
;默认情况下他们的比例是8:1:1,每次使用 eden
和一个 survivor
。回收的时候将 eden
和 survivor
存活的对象复制到另一个 survivor
上,最后清理掉 eden 和使用的 survivor
标记整理算法 (老年代)
过程和标记-清除
相似,但是后续的步奏是:让所有存活的对象都向一端移动,然后直接清理掉端边界的内存
[图片上传失败...(image-7c63be-1525695257170)]
JVM中的分代回收算法:
现代的JVM虚拟机都采用了分代回收,即分为新生代和老年代,新生代采用复制回收算法,老年代采用标记整理算法。
新生代:
- new出来的对象一般都放在新生代
- hotspot虚拟机新生代分为三块,Eden区、From Survivor、To Survivor区(其中两者相等),大小比例为8:1:1.
- 之所以新生代采用复制算法,并且这样设计大小,是有依据的,IBM研究过新生代的对象98%的对象都熬不到下一次GC,即“朝生夕死”,所以这样设计效率很高。
- 新生代发生GC(minor GC)的时候,会将Eden区存活的对象和Survivor中的对象,一起复制到另外一个Survivor中,然后清空Eden和Survivor区。
- 新生代的对象可能在多次Minor GC后移到老年代。
老年代:
老年代的对象,一般存活时间较长,因此回收效率不高,性价比不高。
老年代的对象一般来自下面几种情况:
- 经历多次minor GC未被回收的(一般是15次,在虚拟机中对象有个age属性,通常每经历一次minorGC,对象的age属性+1,当age大于15即放到老年第中)
- new的对象过大,而新生代经历了minor GC之后,依然放不下这个对象,那么这个对象将直接被放入到老年代。
- 可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。
-
如1中所述,新生代的某个age的对象,达到新生代Survivor空间一半以上时,大于或等于这个年里的这些对象将会被移到老年代中去。
新生代和老年代的内存图如下:
JVM中的GC类型:
上面讲到了新生代和老年代:
这两块区域对应的GC分别为Minor GC和Major GC,这两个GC一起触发叫做Full GC(当然也有资料称Major GC 就是Full GC)
- Minor GC:新生代的GC,由于采用复制回收算法,所以速度很快。
- Major GC:老年代的GC,一般发生时都会伴随一次Minor GC,由于采用标记整理算法,而且Major GC 一般是Minor GC的10倍以上。同时,由于老年代的对象,存活时间一般较长,因此回收的性价比不高。
一般的,我们要尽量避免Major GC的发生,比如不要频繁申请大块内存,这样新生代放不下就放到了老年代,老年代内存回收,速度慢,就会导致系统卡顿,影响体验。
GC 日志
不同的收集器的日志形式都是由他们自身实现决定的,可能每个收集器日志格式都可以不一致;但是有共性
> 以下2条比较经典的日志:
33.02:[GC [DefNew:3324k -> 152k(3712k), 0.0025925 secs] 3324K->152k(11904k),0.0031680 sec]
100.33 :[Full GC [Tenured: 0k -> 210k (10240k), 0.0149114 secs] 4603k -> 210k(19456K), [perm:2999k -> 2999k(21248k)], 0.01150007 secs] [Times: user=0.01 sys=0.00, real = 0.02secs]
33.02 / 100.33 :代表GC 发生时间,数字的含义是从 虚拟机启动以来经过的秒速
GC / Full GC :垃圾收集类型的停顿类型,注意这个不是用来区分新生代GC还是老年代GC;有"full"说明这次GC 是发生了 Stop-The-World(暂停所有用户线程)
的;如果是调用 System.gc()
触发的收集,这里将显示[Full GC(System)
]
DefNew / Tenured / perm: 表示 GC 发生的区域 不同收集器对应区域名称可能不同
[]内的 3324k -> 152k(3712k):GC 前该内存区域已使用容量->GC 后该内存区域已使用容量(该内存区域总容量)
[]外的 3324K->152k(11904k),0.0031680 sec :GC 前 java 堆已使用容量->GC后 Java 堆已使用容量(java堆总容量);往后的 0.0031680 sec
表示用时,单位是秒
内存分配、回收策略
- 对象优先在 Eden 分配;这点很容易理解
- 大对象直接进入老年代;虚拟机提供一个-xx:pretenureSizeThreshold参数,大于这个值的对象直接在老年代
- 长期存活对象将进入老年代;每个对象有个 Age 计数器,熬过一次 MinorGC 年龄+1;年龄到了一个指定岁数的时候会被移到老年代(-xx:maxTenuringThreshold这个配置年龄到了多少移动到老年代)
- 动态对象年龄判断;新生代某一个年龄的对象,达到Survivor空间的一半时候,则年龄大于等于该年龄的对象将进入老年代。
- 控件分配担保;不在此展开
何时出发内存回收(GC)?
GC有JVM发起,不受应用控制。即使调用system.gc()
方法也只是建议JVM进行一次GC,而不会立即进行系统的GC操作。
GC时机:
新生代:Eden区放不下会进行一次新生代GC,即Minor GC。
老年代:老年代的内存不够的时候,会触发一次老年代GC,即Major GC,一般都会伴随一次Minor GC。
JVM进行GC的时候,也不是随时随刻都可以进行的,一般要等到安全节点(safe point)或者安全区域(safe region)才会进行,以在特定的位置记录一些栈的信息,这一块比较复杂,就不在这里展开了。
参考资料:
深入理解 java 虚拟机(书)
https://www.jianshu.com/p/c79ab9ab5a90