一、JVM 内存
红色是线程共享的,黄色是线程私有的。
1)内存总览
- 程序计数器:当前线程所执行的字节码的行号指示器。
- 虚拟机栈:Java方法执行的内存模型,里面是栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈:本地方法执行的内存模型,和虚拟机栈非常相似,其区别是本地方法栈为JVM使用到的Native方法服务。
- 堆:用于存储对象实例,是垃圾收集器管理的主要区域。
- 方法区:用于存储已被JVM加载的类信息、常量、静态变量、JIT编译器编译后的代码等数据。
2)程序计数器——线程私有的
- 记录线程所执行的字节码的行号
- 执行引擎:
- 解释器(解释运行,每次一行)
- JIT编译器(三类,C1,C2,分层):
- C1:刚启动就运行
- C2:收集更多信息,对代码进行编译优化,使用与长时间的后台接口
- 分层:刚启动用C1,过段时间用C2,JDK8以后默认这种编译器
- GC器:目前的JDK版本已经在新生代和老年代中均采用G1收集器
- 执行引擎中的解释器就是改变程序计数器的值来选取下一条字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都是依赖它来完成的
- 在多线程中,任何一个确定的时间,1CPU仅执行1指令
- 为了线程切换后还能恢复位置,每条线程都要私有程序计数器
作用:
3)虚拟机栈——线程私有
- 每个线程有自己私有的虚拟机栈
- 线程中每个方法被执行就会同时创造一个栈帧(局部变量表、操作数栈、动态链接、方法出口),每个方法被调用直至执行完成就对应着某个栈帧从入栈到出栈的过程!
- 局部变量表:
编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址)。 - 两种异常:
- StackOverflowError:栈帧太多了
- OutOfMemoryError:创建了过多线程
3)本地方法栈——私有
- 与虚拟机栈类似
本地方法栈 | Native方法 |
---|---|
虚拟机栈 | Java方法 |
- 注意Sun HotSpot虚拟机将以上两个合并
- 虚拟机栈为虚拟机执行 Java 方法 服务,而本地方法栈则为虚拟机使用到的 Native 方法服务
4)堆——线程共享
- 存放对象实例
- GC采用分代收集算法
- 新生代(发生minor GC):每次GC都有大量的对象死去——复制算法,注意存活的大于10%放到老年代
- Eden第一次GC活的放入From
- Eden第二次GC,Eden和From中活的放入To
- 清空Eden和From
- From和To区交换
- 老年代(发生Full GC):标记整理算法(两次遍历,效率低,“整理”就避免了标记清楚算法产生的内存碎片)
- 标记: 先按照根搜索算法进行遍历, 对于遍历到的对象进行标记, 直到遍历结束。
- 整理: 在遍历结束后, 对于标记过的对象, 把它们从内存开始的区域按顺序依次摆好, 整整齐齐的, 中间没有任何的缝隙。在摆放完最后一个标记过的对象后, 把之后的内存区域直接回收掉。(这里最耗时的步骤是, 当你移动一个对象的内存位置时, 你需要让所有之前依赖这个对象的对象更新一下引用地址信息, 这样才不会在移动之后出错。)
5)方法区——线程共享
- 已被虚拟机加载的类信息、常量池、静态变量、JIT编译器编译后的代码等数据
- 注意HotSPOT中使用永久代来实现方法区,JDK8以后,永久代被元空间取代
其中的一部分:
常量池:
存放编译期生成的各种字面量和符号引用。
二、判断对象是否已经死亡
1) 引用计数算法
给对象添加一个引用计数器,每当一个地方引用它,就+1,引用失效就-1,为0就不可能再被使用了。
缺点:无法解决对象之间循环引用的问题。
2) 可达性分析算法
思想:通过一系列的GC roots作为起始点,向下搜索,当某个对象和任何GC roots都没有引用链,证明此对象不可达,要回收!
GC roots:虚拟机栈、本地方法栈、方法区引用的对象
必须两次标记
三、执行引擎
虚拟机栈 = 多个栈帧
一个线程 一个虚拟机栈
一个方法 一个栈帧
一个线程 多个方法
1)栈帧 = 局部变量表+操作数栈+动态连接+方法返回地址
- 来自虚拟机栈,线程私有,只有位于栈顶的栈帧才有效。
局部变量表
存放方法参数和方法内部定义的局部变量。
操作数栈
比如进行加法运算,方法的执行过程中会依照字节码指令,进行入栈、出栈等,取出这两个栈顶元素,相加,再入栈。
动态链接
包含一个指向当前方法所在类型的运行时常量池的引用,便于动态连接:
将这些以符号引用所表示的方法转化为对实际方法的直接引用
方法返回地址
执行完此栈帧,应该跳回上层方法的栈帧中,并将返回值压入调用者栈帧的操作数栈。
2)分派(绑定)(解析)
- 静态分派(绑定)(解析)
与方法重载有关
Human man = new Man();
其中的Human代表静态类型,Man代表实际类型,在编译阶段,javac编译器会根据参数的静态类型决定使用那个重载版本,所以选择了sayHello(Human)
这个。
类加载的解析阶段能将符号引用—>直接引用
- 动态分派(绑定)(解析)
与方法重写有关
利用栈帧中的动态链接运行时选取哪个方法
注意:javap指令,能得到字节码
四、编译器
1)编译期优化:javac编译器
完成了从java程序到字节码的生成,提升编码效率
2)运行期优化:JIT编译器
JIT非必需。
主流的虚拟机都同时包含解释器和编译器
- 解释器:当程序需要迅速启动和执行时,解释器起作用,省去编译时间,立即执行。
- 编译器:程序运行后,将越来越多的代码编译成本地代码,获取更高的执行效率。
- 编译器分为C1编译器和C2编译器。
- 分层编译:
1. C1编译,字节码=》本地代码,简单可靠快速
2. C2编译,字节码=》本地代码,耗时,优化激进
3. C1主攻编译速度。C2主攻编译质量。 - 如何进行热点探测?
①采样法:周期性检测线程虚拟机栈顶
②计数器法:为每个方法建立计数器,统计方法的执行次数,超过阈值,热点!触发JIT编译器!
3)编译优化技术举例
- 以编译方式执行的本地代码比解释更快。
原因:JIT编译器产生的本地代码会比javac产生的字节码更优秀,加入了代码优化 - 代码优化举例:
- 方法内联:去除方法调用的成本(比如建立栈帧)。
- 公共子表达式消除:如果某表达式已经计算过了,并且从之前的计算中直接取值。
- 数组边界检查消除:尽可能把运行期检测检查提前到编译期。
- 逃逸分析:
分析一下这个方法会不会被其他方法调用——方法逃逸;
这个方法会不会被其他线程调用——线程逃逸;
如果都不会,就可以进行优化,比如进行**“栈上分配”“同步消除”“标量替换”**。
栈上分配:分析找到未逃逸的变量,将变量类的实例化内存直接在栈里分配(无需进入堆),分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。
同步消除:如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
五、高效并发
每条线程都有自己的工作内存,工作线程保存了该线程需要的变量的副本拷贝(从主内存)。