参考:
Java内存模型:https://www.jianshu.com/p/cc640f7e919b
GC收集器:https://www.jianshu.com/p/50d5c88b272d
Java垃圾回收:https://www.jianshu.com/p/424e12b3a08f
Java垃圾回收算法和垃圾收集器:https://www.jianshu.com/p/6dd6ac40b58e
一.内存结构
1.JVM内存
JVM内存组成:堆内存(Heap) + 方法区(Method Area) + 栈(Thread 1..N)
- a.堆内存 Heap(或称GC堆)
Heap = YoungGeneration + OldGeneration
YoungGeneration = EdenSpace + FromSpace + ToSpace
虚拟机启动时创建,存放对象(和对象的实例变量)
线程共享
逻辑上连续即可,物理上可处于不连续空间
堆中没有内存完成对象实例分配,且堆无法再扩展时,抛出OutOfMemoryError - b.方法区 Method Area(或称Non-Heap)
运行时常量池 + 字段和方法数据 + 代码
存储类信息、常量、静态变量(类变量)、即时编译器编译后的代码
线程共享
逻辑上连续即可,物理上可处于不连续空间
可选择不实现垃圾收集
方法区无法满足内存分配需求时,抛出OutOfMemoryError - c.栈
用于方法执行:本地方法栈 + 虚拟机栈 + 程序计数器
详情见下文
线程私有
2.JVM内存区域控制
- -Xms 堆的最小空间大小
- -Xmx 堆得最大空间大小
- -XX:NewSize 新生代最小空间大小
- -XX:MaxNewSize 新生代最大空间大小
- -XX:PermSize 永久代最小空间大小
- -XX:MaxPermSize 永久代最大空间大小
- -Xss设置每个线程的栈大小
3.JVM运行时内存
堆和方法区线程共享,具体信息见上文;
栈 = Java栈 + 本地方法栈 + 程序计数器,线程私有
- Java栈(JVM栈)
生命周期与线程相同
描述Java方法执行的内存模型:方法从调用至完成 对应 栈帧在JVM栈入栈至出栈
栈帧用于存储 局部变量表+操作栈+动态链接+方法出口 等信息
局部变量表 = 基本数据类型+对象引用(指向对象起始地址) +returnAddress类型(指向一条字节码地址)
局部变量表内存空间在编译期完成分配,在方法运行期不会改变
当 线程请求的栈深度>虚拟机允许的栈深度 抛出StackOverflowError
当 虚拟机栈扩展无法申请足够内存时 抛出OutOfMemoryError - 本地方法栈(Native Method Stack)
与JVM栈类似
JVM栈为虚拟机执行Java方法服务,本地方法栈为虚拟机执行Native方法服务 - 程序计数器
记录当前线程执行的字节码的行号
分支、循环、跳转、异常处理、线程恢复依赖程序计数器实现
执行Native方法时,程序计数器值为空(Undefined)
4.注意事项
- a.引用、实例和对象
Class a = new Class();
new了一个对象,是Class类的实例
其中a该对象的引用,对象在堆中,引用在栈中
操作对象实际上是通过引用操作对象
多个引用可以指向同一对象 - b.内存回收
方法一旦结束,栈中局部变量立即销毁
堆中对象需要等待虚拟机启动垃圾回收时,按规则销毁 - c.类的成员方法
仅有一套,由该类的对象共享
当对象调用该方法时入栈,不调用就不入栈 - d.堆和栈
堆可动态分配内存大小,不用确定生存期,较栈更灵活但速度慢
栈的速度快于堆,但需明确大小和生存期,灵活性较差 - native方法
Java调用非Java代码的接口
native方法由非Java语言实现
二、垃圾收集
1.垃圾收集内存(回收哪里?)
- a.概念
堆内存 + 方法区(即持久代,可选择不进行垃圾收集)
根据对象生命周期长短,分为新生代Young、老年代Old、持久代Permanent(即方法区,可选)
不同代采取不同的垃圾回收算法。
JVM初始分配内存由-Xms指定,默认为物理内存的1/64且小于1G。
JVM最大分配内存由-Xmx指定,默认为物理内存的1/4且小于1G。
默认当空余堆内存小于40%时,JVM将增大堆直到-Xmx的最大限制。
默认当空余堆内存大于70%时,JVM将减小堆直到-Xms的最小限制。 - b.结构
Heap: Young + Tenured
Young: Eden(存放新生对象) +Survivor(=From + To)(存放每次垃圾回收后存活的对象)
Old: 存放生命周期长的对象
下图中 Virtual = 堆最大内存 - 堆初始内存
随程序运行,Young/Tenured/Permanent将逐渐使用保留的Virtual空间。
2.垃圾对象判定(回收哪些?)
- 判断对象是否存活的算法
1)引用计数法:给对象添加一个引用计数器,每当有一个地方引用它,计数器值加1,当引用失效,计数器值减1,任何计数器为0的对象就是不可能再被使用。
优点:实现简单,判定效率高
缺点:无法解决对象间循环引用问题(两个互相引用的对象再无其他引用),故Java未使用该算法
2)可达性分析算法:以一系列名为“GC Roots”的对象作为起始点,向下搜索,搜索经过的路径称为引用链,当一个对象到GC Roots没有任何引用链连接时,即为不可达对象,可被回收
GC Roots对象 = 虚拟机栈(栈帧中的本地变量表)中引用的对象+方法区中类静态属性引用的对象+方法区中的常量引用的对象+本地方法栈中JNI的引用对象
- 引用的扩展定义
Java传统的引用定义无法描述内存充裕情况与对象是否抛弃的关系,JDK1.2后Java扩展了引用的定义:
1)强引用:程序代码中普遍存在,类似Object obj = new Object()这类引用。垃圾收集器永远不会回收存在强引用的对象
2)软引用:描述还有用但并非必需的对象。软引用关联的对象在系统要发生内存溢出异常之前,会被列入回收范围进行第二次回收,如果回收软引用对象后内存依然不足,才会抛出内存溢出异常
3)弱引用:也用来描述非必需对象,强度比软引用更弱。弱引用关联的对象在下次GC时,无论内存是否充足,都将被回收;
4)虚引用:最弱的引用关系,是否有虚引用存在并不影响对象的生存时间,也无法通过虚引用获取对象实例。为某个对象设置虚引用仅意味在着该对象被GC时能收到一个系统通知。 - 对象死亡的标记过程
在可达性分析算法中不可达的对象,在真正回收前会筛选此对象是否有必要执行finalize()方法。如果对象没有覆盖finalize()方法或finalize()方法已被虚拟机调用过,将被GC回收;如果对象覆盖finalize()方法且finalize()方法未被虚拟机调用过,对象可在finalize()方法中实现自救(重新与引用链建立关联)。
演示代码:a.对象可在GC时自救;b.自救机会仅有一次。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
3.垃圾回收(如何收)
- a.GC算法
1)标记清除 mark-sweep: 收集器先标记活跃对象,再清除其他不活跃对象
缺点:a.效率低;b.产生大量内存碎片,在分配较大对象时因无连续内存而提前触发GC
2)复制 copying: 收集器把堆分为A、B空间,将空间A的活跃对象复制到空间B,再一次性回收空间A
优点:每次均对半区内存回收,内存分配不必考虑内存碎片等情况,实现简单,运行高效
缺点:内存缩小为原来的一半
3)标记整理 mark-compact: 收集器先标记活跃对象,将其合并在较大内存区,再清除不活跃对象 - b.分代收集算法
1)新生代:复制算法。仅需通过少量存活对象的复制即完成收集
2)老年代:标记整理算法。对象存活率高、没有额外空间进行分配担保 - c.GC收集器类型
1)串行: GC线程单独运行
2)并行: 多条GC线程并行,此时用户线程未运行
3)并发: 用户线程和GC线程同时执行
并发收集会很短暂地暂停所有线程来标记对象,而清除过程与应用程序并发执行
串行标记清除在老年代满之后停止应用程序开始执行 -
d.常见GC收集器
- e.Minor GC 流程
1)GC开始前,对象只存在Eden区和Survivor的From区
2)GC开始,Eden区存活对象移到Survivor的To区,Survivor的From区中对象根据年龄进入To区或老年代
3)GC完成,Eden区和From区被清空,Survivor中的From区和To区互换角色
4)Minor GC重复上述过程,直到To区满,此时,将所有对象移入老年代
4.其他
- a.内存泄漏和内存溢出
1)内存泄漏(Memory Leak)
程序中一些对象无法被GC回收,始终占据内存,导致可用内存减少
2)内存溢出(Memory Overflow)
程序运行过程中无法申请到足够的内存。常见于老年代或永久代垃圾回收后仍无内存空间容纳新的Java对象的情况 - b.Young设置大小对GC的影响
Young的复制收集必须停止所有应用程序的线程,是GC主要的暂停时间瓶颈
Young设置较大时,可以让更多的对象自行死去,不必频繁GC;一旦GC,停顿时间较长 - c.内存申请过程
1)JVM试图为相关Java对象在Eden中初始化一块内存区域
2)当Eden空间足够,内存申请成功。否则进行下一步
3)JVM试图释放Eden中所有不活跃对象,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
4)作为Eden及Old的中间交换区域,若Old区空间足够,Survivor区将区中对象移入Old区,否则保留在Survivor区
5)当Old区空间不足时,JVM将在Old区进行完全GC
6)完全GC后,若Survivor和Old仍然无法存放从Eden复制来的对象,从而导致JVM无法在Eden区为新对象创建内存区域,则出现out of memory错误 - d.Minor GC & Full GC
1)新生代GC(Minor GC):发生在新生代的GC,非常频繁,速度较快
2)老年代GC(Major GC / Full GC):发生在老年代的GC。一般比Minor GC慢10倍以上 - e.吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
CPU运行用户代码的时间与CPU总消耗时间的比值
三.解释执行和编译执行
1.解释执行
- 逐句翻译源代码,每翻译一句就提交给计算机执行一句,并不会形成目标程序
- 缺点是*运行速度慢,比如程序中存在循环语句时,循环体内的语句会被多次翻译,从而影响速度
2.编译执行
- 先需要编译源代码,并生成目标文件,计算机再执行该目标文件
- 编译过程较复杂(对代码进行语法分析、优化并分配内存等),但一旦形成目标文件就一劳永逸,不必重新编译,所以执行速度较快
3.Java的执行
- JVM内嵌的解释器将由javac命令编译的字节码(.class)文件解释为最终的机器码
- 虚拟机发现某个方法或代码块运行特别频繁时,会把这些代码认定为热点代码
- 为了提高热点代码的运行效率,JVM会使用及时/动态编译器(JIT,Just In Time Compiler)将这些代码编译成机器码,并进行各种层次的优化
- 所以Java结合了解释执行和编译执行
海日生残夜,江春入旧年