什么是虚拟机?
虚拟机就是将平台无关的.class文件的字节码翻译成平台相关的机器码,来实现跨平台;
JVM
jvm是一种规范,能将按照JVM规范生成的字节码转换为机器能执行的机器码;
程序执行:
JVM三大角色
- 类加载器 :将编译好的.class文件加载至运行时数据区
- 运行时数据区 :存放系统运行时的数据
- 执行引擎 :执行当前进程内要完成的操作

栈
是在运行时数据区;
栈是线程独有的,保存其运行状态和局部自动变量的(所以多线程中局部变量都是相互独立的,不同于类变量)。栈在线程开始的时候初始化(线程的Start方法,初始化分配栈),每个线程的栈互相独立。每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈
栈中放的是一个个的栈帧(Method);
一个栈默认的大小为1024K(1兆);
当方法循环调用或递归调用过多时,会发生栈溢出;
栈帧
每个方法对应一个栈帧;
栈帧包含
- 局部变量表:局部变量数组或本地变量表;存放了代码起始行号、变量下标等信息;
- 操作数栈:进行运算时,将变量和结果压入和移除的先进后出的栈;
- 动态链接:运行时常量池中,结尾处以特殊符号(#)为指引的“路标”,以进行执行顺序;
- 方法返回地址:调用方法的PC寄存器的值;
堆
堆区就是一组连续指定的内存地址的逻辑空间;
堆区是共享区,任何线程都可以访问堆中的共享数据;
为了优化GC性能,堆区以“年龄”划分为三块结构区域:
(Android中以年龄6为划分,其他为15);
- 年轻代young:多为一般的临时对象,GC次数小于6,即:GC5次不死;
- 老年代old:GC次数大于等于6,即GC6次不死,会从年轻代划分为老年代;
- 永久代(对HotSpot虚拟机而言);
年轻代和老年代的内存大小默认为1:2;
三种GC
- minor GC: 只发生在年轻代;
- major GC:只发生在老年代,在发生前会尝试调用minor GC(不是绝对发生);性能比 minor GC慢10倍;
- Full GC:发生在全部;
年轻代
划分为2个区域:内存比例默认8:2;
- Eden区:当年轻代内存够用时,所有出生的对象都放在这里
-
survivor区:对象内存地址排序使用;
- From区(s0)
- To区(s1)
TLAB:
- 在Eden区中,总内存大小只占Eden的1%;
- 为每个线程开辟的一小块私有缓存区域,来放每个线程的私有数据,防止加锁影响分配速度;
- 若对象在TLAB中分配失败,则会尝试加锁保证原子性,从而直接在Eden中分配;
对象在年轻代区域内存分配流程:
- 很多很多对象出生在Eden区;
- 当Eden区域内存满时,触发第一次minor gc;
- Eden区存活的对象进入survivor中的From区域,年龄+1,进行内存地址排序;
- 又有很多很多对象出生在Eden区;
- 当Eden区域内存满时,触发第二次minor gc;
- Eden区存活的对象和上次在From区域中仍然存活的对象,一起进入To区域,年龄+1,进行内存地址排序;
- 又有很多很多对象出生在Eden区;
- 当Eden区域内存满时,触发第三次minor gc;
- Eden区存活的对象和上次在To区域中仍然存活的对象,一起进入From区域,年龄+1,进行内存地址排序;
- 一直循环
- 当某次minor GC时,survivor区(From/To)中有对象年龄等于6 或者From/To放不下时,将From/To中的部分对象移入老年代;
注意:当发生minorGC时会触发STW行为:暂停当前所有的其他线程!!! 所以频繁的内存抖动,会带来卡顿;
老年代
默认内存是年轻代的2倍;放年龄大的对象;
当minor gc完年轻代,年轻代内存不足时:
- 当老年代区域内存够时,把对象放进来;
- 当老年代区域内存不足时,发生major gc;
- 当gc完,内存还不足时,调用Full GC;
- 当gc完,内存还不足时, 毁灭吧OOM;
对象逃逸:
逃逸:在当前方法中,创建的对象被其他方法使用;
未逃逸:在当前方法中,创建的对象并没有被其他方法使用
栈上分配:
在对象未逃逸时,在JIT编译器指令调优时,会将未逃逸对象直接在栈上进行分配,不再进行堆区分配;(栈帧用完就销毁了)
标量替换:
在对象未逃逸时,JIT编译器指令调优时,会将未逃逸的引用对象进行标量替换(将引用对象转化为其内部的一个个基本类型),提升性能;
对象在内存中的分布:
- 对象头:64位;头信息和类型指针;
- 实例数据:方法和内部的变量;
- 对齐填充:用若干个0对齐;
头信息:包含hashcode、GC分代年龄、线程持有锁、偏向线程ID、偏向时间戳、锁类型(最后两位);
GC
什么是垃圾?
在程序中没有任何指针指向的对象,这个对象就是要被回收的垃圾;
为什么要进行GC?
- 防止内存被消耗完,释放垃圾对象
- 对内存空间碎片进行管理:整理或者在一个空闲表里按顺序将地址
垃圾回收相关算法:
1、垃圾确认算法(标记阶段算法):
- 引用计数算法:用一个整型变量记录对象被其他对象引用的次数;当引用数为0时它就是一个垃圾对象;缺陷:当有对象进行循环引用时,引用数无法为0,也就无法释放,发生内存泄漏;
- GCRoot可达性分析算法:解决了对象循环引用问题;当对象没有被顶层的root对象所引用,那么它就是一个垃圾对象;
GCRoot:
一个set集合,存放root对象;
root:一个指针,他保存了堆里面的对象,但自己又不在堆里面;
2、清除垃圾算法(清除阶段算法):
所有内存回收都是基于快照(只要动内存):停掉所有线程STW;
- 标记-清除算法(Mark-Sweep):从头到尾遍历堆内存;遍历到对象header里不是GCRoot可达对象,则清除;把内存地址进行回收,放到空闲表中
- 复制算法(Copying):内存分为两块同样大小;一块满了,把活着的对象按顺序放入另一块中,之前满的里面的对象,全部干掉;一直循环;场景:年轻代中的survivor区(From和To);缺点:每次交换时地址都发生改变,需要维护对象的地址;
- 标记-压缩算法(Mark-Compact):先进行清除算法,清除完,经过算法运算,将存活对象地址进行依次按顺序排列整理;
3、分代收集算法
经过研究发现:
70%~90%的对象都是生命周期短的对象,且频繁产生;
1%~30%的对象都是生命周期长的对象;
通过上面两种现象,将堆区划分为年轻代和老年代;
年轻代:将复制算法进行优化:分为8:2的区域,2又分为1:1进行复制算法;
老年代:使用压缩算法:先清除再整理排序,不需要维护一个空闲表;
4、增量收集算法
上述的算法都会触发STW,挂起所有线程,影响用户体验;
增量收集算法:针对老年代;
- 标记:线程都挂起
- 清除:和线程一起运行
- 整理:每次只收集一部分区域(线程挂起),收集完,线程继续运行;用来减小每次线程挂起时间;
缺点:增大线程切换开销,系统吞吐量增大;
应用于CMS收集器;