Java 内存分配与垃圾回收机制
  
程序计数器:
-    
用于指示当前线程执行的指令行号,字节码解释器通过改变它的值选取下一条待执行的指令;
 -    
分支、循环、跳转、异常处理、线程恢复都需要依赖它;
 -    
它是线程私有的;
 
栈:
-    
存储和方法执行相关的信息:栈帧(Stack Frame);
 -    
栈帧包含: 局部变量表(基本数据类型和引用)、操作栈、动态链接、方法出口等信息;
 -    
每一个方法从被调用到运行结束都对应着栈帧从入栈到出栈的过程;
 -    
根据方法类型,可分为虚拟机栈和本地方法栈;
 -    
它也是线程私有的;
 
堆:
-    
存储对象实例,是 GC 管理的主要区域,
 -    
根据对象生存周期,它被分为新生代(YoungGen)和老年代(TenuredGen),而新生代又分为 Eden、FromSurvivor 和 ToSurvivor;
 -    
它也是所有线程共享的;
 
方法区:
-    
存储已被虚拟机加载的类信息、常量、静态变量、即时编译的代码等;
 -    
HotSpot 虚拟机通常将它和”永久代”(PermanentGen)等同,该区域一般不会发生 GC;
 -    
它也是所有线程共享的;
 
直接内存:
-    
它与 NIO 中的 Channel 和 Buffer 有关,采用 native 方法直接分配堆外内存;
 -    
它通过存储在 Java 堆内存中
DirectByteBuffer对象中的引用进行 I/O 操作; 
“内存溢出”与”内存泄漏”:
-    
“内存溢出”是指无法再分配所需的内存;
 -    
“内存泄漏”是指已分配的内存无法释放,多次泄露会导致内存溢出;
 -    
常见的情况是
new的对象用完后没有及时delete(回收),造成内存无法释放; -    
发生的位置:
-      
堆(
Xmx/Xms):对象实例未及时释放或生命周期过长; -      
栈(
Xss):请求的栈深度(方法调用层级或递归)超出允许最大值,或无法分配更多内存; -      
常量区(
XX:PermSize/XX:MaxPermSize):通常是String相关操作导致的,比如String.intern(),如果该对象不存在就会添加到常量池; -      
直接内存(
XX:MaxDirectMemorySize):其大小默认与最大堆内存一样; 
 -      
 
垃圾回收算法:
引用计数:
-    
标记引用数,检查是否为 0;
 -    
无法解决循环引用问题;
 
分代:
-    
根据对象生存周期将内存分为:新生代(Eden/Survivor)、老年代(TenuredGen)、永久代(PermanentGen);
 -    
绝大部分对象存活周期很短,所以新生代一般是一个 80% 的 Eden + 两个 10% 的 Survivor;
 -    
新生代一般采用”拷贝”算法;老年代一般采用”标记清理”或”标记整理”算法;
 
拷贝:
-    
将可用内存分为两个部分,每次只使用其中一块,避免产生大量内存碎片;
 -    
如果使用中的那一块内存用完,就将存活的对象拷贝到另一块未使用的内存上,并将使用过的那一块全部清理;
 -    
实际拷贝时是将 Eden 空间和其中一个 Survivor 中存活的对象拷贝到另一个 Survivor 中;
 
根搜索:
-    
从所有 GC Root 对象向下搜索,如果和指定对象之间没有可达的引用路径,则可被回收;
 -    
Root 对象包括:
-      
虚拟机栈中的引用对象;
 -      
本地方法栈中的 JNI 引用对象;
 -      
方法区中的静态引用对象和常量引用对象;
 
 -      
 
标记-清理:
-    
搜索结束后,没有引用链的对象会被标记,一般会经历两次标记;
 -    
如果
finallize()方法没有重写或者已经调用过,则会将该对象放到 F-Queue 队列中等待 Finallizer 线程执行清理; -    
如果该对象此时要防止被回收,只需要将自己与其他存活对象建立引用关联即可;
 -    
缺点:标记和清理过程效率低,并且会产生大量内存碎片;如果以后程序无法分配到足够的连续内存,会再次触发 GC;
 
标记-整理:
- 标记后并不是直接清理,而是将所有存活对象都向一端移动,然后清理掉边界以外的内存;
 
垃圾回收器:
Serial:
-    
回收时需要暂停所有工作线程;
 -    
简单高效(没有线程交互开销),是 Client 模式下的默认新生代收集器;
 
Serial Old:
- 它是 Serial 的老年代版本,同样单线程;
 
ParNew:
-    
在 Serial 基础上添加了多线程支持,是第一款并发收集器,也是 Server 模式下的默认新生代收集器;
 -    
除了 Serial,目前只有 ParNew 能和 CMS 配合工作;
 
CMS(Concurrent Mark Sweep):
-    
它追求的是最短停顿时间,适合频繁与用户交互的场景;
 -    
分为四个步骤:
-      
初始标记:快速标记 Root 对象能直接关联的对象(需要暂停用户线程);
 -      
并发标记:跟踪查找引用链;
 -      
重新标记:修正上一步并发期间的标记(需要暂停用户线程);
 -      
并发清理;
 
 -      
 -    
缺点:
-      
占用 CPU 资源,影响吞吐量;
 -      
无法处理并行过程中新产生的垃圾,只能等待下次 GC;
 -      
由于采用“标记-清理”算法,会产生内存碎片;
 
 -      
 
Parallel Scavenge:
-    
它追求的是更大的吞吐量(用户代码运行时间与总时间的比值),适合后台任务;
 -    
如果粗暴地减小新生代,虽然可以减小停顿时间,但会是 GC 变得频繁,并且牺牲了吞吐量;
 -    
该收集器除了可以设置最大停顿时间和吞吐量,还可以开启自适应策略动态调整参数;
 
Parallel Old:
- 它是 Parallel Scavenge 的老年代版本;
 
G1:
-    
基于“标记-整理”算法,不会产生内存碎片;
 -    
可以精确控制某个时间段内的最大 GC 停顿时间;
 -    
之前的回收器的收集范围都是整个新生代或老年代,而 G1 将则它们分为多个大小固定的区(Region),并且跟踪其垃圾堆积程度,每次都优先回收垃圾最多的区域;
 
内存分配与 GC 触发策略:
-    
一般新对象优先分配在新生代的 Eden,如果空间不够则发起一次 Minor GC;
 -    
大对象(很长的字符串或数组)直接分配在老年代,避免 GC 时发生大量内存拷贝;
 -    
更新对象年龄:
-      
虚拟机给每个对象定义了 Age 计数器,Eden 中新创建的对象 Age 为 0;
 -      
Eden 中的对象若在 Minor GC 后存活,则移入 Survivor(如果可以容纳的话),且 Age++;
 -      
Survivor 中的对象若经历 Minor GC 后仍然存活,则 Age++;
 -      
当 Survivor 中某对象的Age超过阈值(默认 15)时,会被移入老年代;
 
 -      
 -    
空间分配担保:
-      
若 Minor GC后 仍有大量存活对象(Survivor 空间不够),则需要老年代进行空间分配担保;
 -      
检测之前移入老年代的对象平均大小,如果大于老年代剩余空间,则进行一次 Full GC 让老年代释放部分空间;
 -      
如果小于则进一步检测
HandlePromotionFailure设置是否允许担保失败,如果不允许则仍会进行 Full GC; 
 -