C语言申请内存:malloc free
C++: new delete
Java: new ?
自动内存回收,编程上简单,系统不容易出错,手动释放内存,容易出两种类型的问题:
- 忘记回收
- 多次回收
什么是垃圾?
没有任何引用指向的一个对象或者多个对象(多个对象之间是循环引用)
浮动垃圾:这次没有回收,下次再回收
如何定义垃圾?
reference count(引用计数)
但是如果有循环引用的多个对象,是找不到的,就会发生内存泄漏。Root Searching(根可达算法)
java的hotspot正在使用的算法。
GC roots:根对象。包括
一个程序运行起来以后,通过根找不到的全是垃圾。
object o = new Object();这个new Object()一定不是垃圾。
根对象指的是,我们现在正在跑的这个栈里面的局部变量(就是main方法中的局部变量)【线程栈变量】,以及我们从class里面引进的静态变量,还有一些常量池和JNI指针引用到的变量。
常见的GC算法
- 标记-清除(Mark-Sweep)
首先标记出想要回收的对象,接着统一进行清除。
位置不连续,产生碎片 - 拷贝(Copying)
“半区复制”:将内存分为两部分(1:1),每次只使用其中一块。用完一块,就将存后的对象复制到另一块上,清理掉这一块内存空间。
不产生碎片,内存拷贝很快,浪费空间。
“Appel式回收”:每次只使用Eden和一块Survivor空间,用完后复制到另一块Survivor空间上。 - 标记压缩(Mark-Compact)
与复制算法相似,区别是先让所有存活的对象都向内存一端移动,再清理掉后面的内存空间。
不产生碎片,但效率低,因为任何一块的挪动都需要进行线程同步。
JVM内存分代模型(用于分代垃圾回收算法)
部分垃圾回收器使用的模型
新时代 + 老年代 + 永久代(1.7)/ 元数据区(1.8) Metaspace
· 永久代和元数据都是装Class对象的;
· 永久代必须指定大小限制,但是元数据区可以设置,也可以不设置。无上限(受限于物理内存);
· 字符串常量 1.7 - 永久代; 1.8 - 堆
·MethodArea
是一个逻辑概念,对应的是永久代/元数据区新生代 = Eden + 2个suvivor区
(1)YGC回收之后,eden区大部分对象都会被回收。活的对象拷贝进survivor区。
(2)再次YGC,eden活着的对象 +s0 -> s1
(3)再次YGC,eden活着的对象 +s1 -> s0
(4)年龄足够 -> 老年代 (老一点的垃圾回收器是15岁, CMS是6岁)
(5)s区装不下 -> l老年代老年代
(1)顽固分子
(2)老年代满了FGC【Full GC】 新生代老年代同时进行回收
尽量减少产生FGC,因为使用到了标记压缩算法动态对象年龄判定
年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域*TargetSurvivorRatio的时候,就从这个年龄段网上的年龄的对象进行晋升。
例子:年龄1的占用了33%,年龄2的占用了33%,累加和超过默认的TargetSurvivorRatio(50%),年龄2和年龄3的对象都要晋升。
MinorGC = YGC
MajarGC =FGC
常见的垃圾回收器
Epsilon是测试使用的空的GC,调试JDK的。
Serial和Parallel可以看成是同样的种类用在不同的区域上。
Serial 运行在年轻代 单线程回收【串行回收】
垃圾回收的时候,应用程序是没办法运行的(STW,Stop the world)出现卡顿Parallel Scavenge 年轻代 并行回收
ParNew 年轻代 配合CMS并行回收
SerialOld
ParallelOld
ConcurrentMarkSweep(CMS) 并发的 垃圾回收和应用程序同时运行,降低STW时间(200ms内)
前面长的甚至有几小时STW
G1(10ms)
ZGC (1ms PK C++)
Shenandoah
Epsilon
调优主要是1,2和4,5
因为1.8默认回收器是 PS+ parallelOld
CMS
承上启下垃圾回收器。第一款并发垃圾回收器。
有巨大问题:最长时间的STW就是CMS产生的。因为cms失败了,就要shang
从线程角度
初始标记会把根对象标出来。
错标
如果已经标记好了要清除的对象,结果在并发标记中,有的垃圾又被引用到了,这时候清除垃圾就会产生错标现象。
很多算法就是为了高效率解决算法问题。重新标记就会修正错标
并发清理可能会产生浮动垃圾,就是标记好了要清除的对象,结果在这时候又出现了垃圾。这些垃圾会留到下一次清理的时候清理。
三色标记算法
黑色:自己已经标记,fields都标记完成(即接下来的遍历不会再遍历到它了,它的孩子都已经遍历完了)
灰色:自己已经标记,还没来得及标记fields
白色:没有遍历到的节点
如果灰色B>白色C的引用消失了,黑色A->白色C的引用出现了。此时就会把C当成垃圾,出现了错标。
CMS解决方案——Incremental Update
把黑色B变为灰色。(写屏障)
了解生产环境下的垃圾回收器组合
- JVM命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
- JVM命令参数分类
标准:-
开头 ,所有HotSpot都支持java -version
非标准:-X
开头Java -X
不稳定:-XX
开头1
2
3java -XX:+PrintCommandLineFlags 设置值(启动了哪些参数)
java -XX:+PrintFlagsFinal 最终默认值
java -XX:+PrintFlagsInitial 默认参数值
分配和垃圾回收大致流程
- 尝试在栈中分配对象,如果能分就分【逃逸分析,不能有逃逸,野指针问题;标量替换:把聚合量分解成为标量(原始数据类型)】方法结束,栈帧弹出的时候,对象顺带直接销毁,不需要垃圾回收。
- 如果逃逸分析失败,有引用指向它,就放到堆上。如果大对象,直接放老年代。老年代垃圾回收:FGC
- 假如不够大,TLAB(Thread Local Allocation Buffer,线程本地分配缓存区)一般是直接放到Eden区的。但是为啥有TLAB呢?因为一个程序开始,为许多线程分配资源的时候,一定会发生资源争用。JVM就要对这些线程进行管理、同步。所以效率偏低。为了提高效率,JVM设置了一种机制:JVM启动的每个线程都可以有私有的一块空间(放在eden区里面),以后new对象就new到自己的空间,满了再抢占。YGC最后到老年代。