基本存储单元是 slot(变量槽),用于存储各种类型的数据,其中 long 和 double 会占用两个 slot,其他基本数据类型以及对象引用变量占用一个 slot。
这也说明了为什么类方法不能使用 this 而实例方法可以(实例方法会直接在索引为0的位置创建一个 this 参数保存,所以在实例方法中使用 this 就是直接使用这个参数的)
同时局部变量表的槽位是可以重用的,当前一个局部变量失效后,下一个变量使用空出来的位置。
上面这个方法是实例方法,包含 this, 应该有四个 index 槽位 ,但是因为 b 是在括号里作用的,出了括号就失效了,所以它的位置(index=3的位置)被新设置的 c 所占用。
操作数栈
先进后出结构,是当前方法执行的位置,在方法执行时,会根据编译生成的字节码按顺序将要操作的数据从局部变量表中进入入栈,栈中的数据只能从栈顶向下操作,不能跨数据。比如代码 x=x+1,在执行时会将 x 先压入栈,然后将 1 压入栈,然后读取到 + 的指令,将栈顶的两个数相加,再将加的结果存入局部变量表 x 的位置。如果调用了其他方法并获取了返回值,那么在调用方法执行完毕后,该方法的返回值会被压入栈顶,然后再进行后续的操作。
和堆一样是线程共享, 用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存等 。方法区的实现在 1.8 之前是永久代,使用的是 JVM 的内存,在1.8开始实现变成元空间,使用的是本地内存。之所以这样改变,是因为原来的方法区很容易发生 OOM,因为方法区的类信息被回收的条件非常苛刻,必须满足以下三点:
1、该类的所有对象都被回收;2、加载该类的类加载器被回收;3、该类对应的 Class 对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。
关于第三点的 Class 对象,在一个类被加载时,会在堆中创建一个用于用于访问这个类的类信息 Class 对象。而在成为元空间后,使用的是本地内存,所以方法区发生 OOM 的情况会极大改善。
运行时常量池
当 Class 文件被类加载器加载到 JVM 中时,存储的位置就是在方法区,而在 Class 文件信息中包括着 class 文件的常量池,当 JVM 开始执行时,就会将文件常量池中的数据加载到 方法区内部的运行时常量池,变成运行时状态,并将符号引用转成直接引用。
符号引用和直接引用:当在调用中调用某个类的类方法、类属性、接口方法、接口属性时,因为在执行前,对应的类、接口都还在 Class 文件常量池中,没有加载到内存中,所以不能确定这些类、接口加载后的具体位置,这时就需要一种方式来确认位置,通常使用类的全名+属性名/方法名 来唯一标识要调用的方法/属性,这种标识就是符号引用,等到对应的类加载到内存后,再将这些唯一标识改成在内存中的位置,这种就是直接引用。
字符串常量池
在 JDK 1.7 开始,字符串常量池就由方法区移入了堆中,字符串常量池是专门存放字符串常量的,至于为什么移入堆中,这是因为字符串的创建和对象一样频繁,销毁也就变得尤其频繁,而方法区的 GC 是伴随着 full gc 的, 因为 full gc 会造成 STW,在 full gc 期间其他程序都会停止,所以都会避免 full gc,而字符串常量池放在方法区中就减少了 字符串被回收的频率,提高了 OOM 的概率。
类加载
为类属性分配内存并设置零值( 这里不包括使用 static final 修饰的属性且赋值的值是一个字符串常量或一个基本数据类型常量或其他不触发方法的情况(也就是过程不会涉及构造器或者其他方法),因为字符串或者基本数据是常量,在编译时期就会分配地址,准备阶段直接就会显式初始化,而如果赋的值包括方法调用就需要在 <client> 方法里执行 )。如果属性值是常量,那么常量值就会在方法区中分配内存,而如果是对象,那么对象则会在堆中创建;并且实例属性参数也会跟随对象的创建在堆中,只有静态属性和对应的常量值在方法区中分配内存。而设置的零值是当前类型的默认值,比如 private int a = 2;那么设的零值就是 0, a = 2 是在后面的<client>方法中执行的。
解析
上面说过一个类卸载所需要的条件:1、该类的所有对象都被回收;2、加载该类的类加载器被回收;3、该类对应的 Class 对象没有在任何地方被引用(无法在任何地方通过反射访问该类的方法)。那么具体原因是什么?
我们知道,对象被回收的条件是这个对象没有被引用,类也是如此,在类被加载到内存后,它会在堆中创建一个 Class 对象,并且和加载它的加载器互相关联,也就是图中的 MyClassLoader,而这个对象也和类对应的实例对象所关联,这种关联是无法切断的,而如果对应的三种变量都没有再引用,那么就相当于这个类信息没有被引用,那么也就可以被回收了。
类被加载的场景
1、访问的类属性不是当前类的属性,比如从父类继承而来的或者实现接口得到的,比如
public class InitTest{
public static void main(String[] args) {
int a = son.a;
}
}
class parent{
public static int a =0;
static {
System.out.println(&#34;12&#34;);
}
}
class son extends parent{
public static int b =0;
static {
System.out.println(&#34;1ss2&#34;);
}
}这里只会触发 parent 的初始化,而不会触发 son 类的初始化,而如果 son 重写了属性 a 或者调用的是 son 的另一个属性 b ,那么就会触发 son 类的初始化,并且因为 son 继承了 parent 类,所以在 son 初始化前还会先初始化 parent。
2、通过数组定义类引用,不会触发此类的初始化(如果数组类型是基本数据类型,那么不需要加载;如果是引用数据类型,那么就进行类的加载,但不会进行初始化操作)
3、调用 static final 修饰的且是常量或者是字符串或是其他没有方法触发的情况,也不会触发初始化操作。
4、调用 ClassLoader 的 loadClass() 方法加载一个类,只会触发加载操作,而不会触发初始化操作。
类加载器的拓展
加载 name 类,如果找不到该类,就抛出异常。内部的实现是父类委托机制。
3、findClass(name)
查找二进制的 name 类,返回该类的实例,这个类是 loadClass 内部调用的一个方法,JDK 维护了一个推荐的重写方法,鼓励我们去重写这个方法来实现对功能的拓展。JDK 1.2 之前还未引入父类委托机制,所以要拓展就需要去重写 loadClass 方法,1.2 引入父类委托机制后通过重写 findClass 方法来拓展,并且也没有破坏父类委托机制。
4、defineClass(String name, byte[] b,int off, int len)
将字节数组 b 转换为 Class 的实例,off 和 len 参数表示实际 Class 信息在 byte 数组中的位置和长度。其中 b 是ClassLoader 从外部获取的。这是受保护的方法,只有在自定义的 ClassLoader 子类中使用。一般在 findClass 方法中被调用,在 findClass 方法中先类的字节码数组,然后调用 defineClass 获取类实例返回。
ClassLoader.loadClass 是一个实例方法,该方法将 Class 文件加载到内存中后,只会执行类加载过程的加载、验证、准备、 解析。初始化等到类的第一次使用时才会执行。Class.forName 是静态方法,该方法在将 Class 文件加载到内存的同时,还会执行类的初始化。
破坏双亲委派机制的三次场景