从一道面试题说起
i=i++
结果为8i=++i
结果为9
i=i++ 生成的指令执行过程分析
i=++i 生成的指令
JDK1.8 JVM运行时数据区域概览
PC 程序计数器
JVM 栈空间
线程私有,每个线程对应一个Java虚拟机栈,其生命周期与线程同进同退。每个Java方法在被调用的时候都会创建一个栈帧,并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就完成了使命。
栈帧
局部变量
局部变量表存放了编译器可知的各种基本数据类型(int、short、byte、char、double、float、long、boolean)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一跳字节码指令的地址)。其中64 位长度的long 和double 类型的数据会占用2 个局部变量空间(Slot)即使在64位虚拟机中使用了64位长度的内存空间来实现一个slot,虚拟机仍要使用对齐和补白的手段让slot在外观上看起来与32位虚拟机中的一致。其余的数据类型只占用1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
操作数栈
JVM规范中,对这个区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
动态链接:
指向运行时常量池里面的符号链接,看有没有解析,如果没有解析,就动态解析,如果已经解析了,就拿过来使用。例如,A方法要调用B方法,B方法在哪儿呢?就要去常量池里面找。
返回值地址:
A方法调用了B方法,如果有返回值,要记录返回值返回到那个地方,也就是记录继续执行的位置。
为什么我们可以在非static方法中使用this?
因为this在局部变量表中是已经存在的。
(局部变量表中,0位置是this,1位置是k,2位置是i)
i 因为超过了 127,所以用的是sipush; 若小于127,则用bipush
下面这个例子,之前有一道面试题:DCL 单例为什么要加 volitile?因为你看下面的第一条指令,我们知道,刚new出来对象是半初始化的对象,只是赋一个默认值,而involespecial才是调用构造方法,给变量赋初始值,而这两条指令之间是可能会发生指令重排的。
递归的调用
下面是m方法的执行,没有把main方法放上来
在这个3层递归中,使用到的是3个栈
总结
store
load
pop
mul
sub
invokeXXX指令
invokestatic
调用一个静态方法
invokevirtual
new一个对象,调用一个非静态方法
自带多态:new 的是哪个对象,调用的就是哪个对象的方法
invokespecial
调用实例方法;对超类、私有和实例初始化方法调用的特殊处理
可以直接定位的,不需要多态的方法
private方法
构造方法
final方法不是invokespecial的,它是invokevirtual的。
invokeinterface
调用接口方法
invokedynamic
JVM最难的指令
invokedynamic是lambda表达式或者反射或者其他动态语言scala kotlin,或者CGLib ASM,动态产生的class,会用到的指令
每一个lambda表达式都有一个自己的内部类,java没有纯粹的函数。匿名内部类每次都是动态产生的。
package com.mashibing.jvm.c4_RuntimeDataAreaAndInstructionSet;
public class T05_InvokeDynamic {
public static void main(String[] args) {
I i = C::n;
I i2 = C::n;
I i3 = C::n;
I i4 = () -> {
C.n();
};
System.out.println(i.getClass());
System.out.println(i2.getClass());
System.out.println(i3.getClass());
for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)
}
@FunctionalInterface
public interface I {
void m();
}
public static class C {
static void n() {
System.out.println("hello");
}
}
}
关于Lambda表达式的一个坑
如果你用Lambda表达式写出了这样的代码:
for(;;) {I j = C::n;} //MethodArea <1.8 Perm Space (FGC不回收)
1
在1.8之前有一个巨大的bug,就是你在里面产生了很多对象,但是Perm Space在FGC的时候是不会回收的。
本地方法栈
功能与Java虚拟机栈十分相同。它和虚拟机栈之间的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
堆空间
所有线程共享同一个堆空间。
堆空间是用来存放所有类实例和数组空间分配的运行时数据区。
堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:几乎所有的对象实例及数组都在对上进行分配。字符串常量池存放在堆中。堆有自己进一步的内存分块划分,按照GC分代收集角度的划分请参见上图。
运行时常量池
元数据区
元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。方法区存放虚拟机加载的类信息,静态变量,常量等数据。(会触发FGC清理)
在jdk1.8中也就是Metaspace内存溢出,可以通过参数JVM参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize设置Metaspace的空间大小。
为什么移除永久代?
1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、永久代大小不容易确定,PermSize指定太小容易造成永久代OOM
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。
直接内存
jdk1.4引入了NIO,它可以使用Native函数库直接分配堆外内存。
堆内存分配机制
对象在内存中的哪个区域,分配过程
栈上分配
为了对标C,Java中的小对象、无逃逸(就在某段代码中使用)、支持标量替换(可以用普通的int等属性代替整个对象)、无需调整这样的对象分配在栈上。
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域。
如果一个对象在方法中被定义,但是对象的使用仅是在当前方法中,而且对象本身比较简单,那么对象就有可能被存储在线程栈中。
使用逃逸分析,编译器可以对代码做如下优化:
同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可 以不考虑同步。
将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
从jdk1.7开始,默认开启逃逸分析 -XX:-DoEscapeAnalysis
TLAB分配
如果栈上分配不下的话,会把它们分配到 线程本地 TLAB(ThreadLocal Allocation Buffer)
线程本地空间是线程独有的,避免多线程的争用,效率较高
堆分配
优先在Eden区分配
Eden区不足,进行MinorGC
大对象直接进入老年代
大对象指需要大量连续内存空间的对象(比如字符串、数组)
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
长期存活的对象进入老年代
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁。当它的年龄增加到晋升年龄阈值,就会被晋升到老年代中。对象晋升到老年代的年龄阈值。
对象动态年龄判断
Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的 50% 时(默认值及最大值是 50%,可以通过 -XX:TargetSurvivorRatio=percent 来设置 ),取这个年龄和 MaxTenuringThreshold (默认为 15,CMS默认值为6 岁,可以通过参数 -XX:MaxTenuringThreshold 来设置)中更小的一个值,作为新的晋升年龄阈值”。
eg:
年龄1+年龄2+年龄3+…+年龄n>50%;
则把年龄大于等于N的对象转移到老年代。
一次年轻代GC长暂停问题的解决与思考 - 简书 (jianshu.com)
分配担保:
(不重要)YGC期间 survivor区空间不够了 空间担保直接进入老年代参考:JVM内存分配担保机制-腾讯云开发者社区-腾讯云