// 回过头学习了JVM,进行一次全面的总结
// 网上关于JVM的帖子很多都存在问题,我查阅了很多资料确保内容的正确性。如有问题欢迎指正。
xxx.java 经过javac编译为xxx.class,此文件经过类装载子系统编译为xxx.Class字节码文件。
类装载子系统共有四种。
本地方法接口是指java代码中用native关键字表示的函数,没有方法体。java会调用第三方程序实现这些方法。
本地方法栈用于管理native方法的调用。
执行引擎把字节码转换成可以直接被JVM执行的机器语言
等价于汇编中的IP计数器,内部存放指向程序下一步代码的指针。
存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆。
栈满足先进后出的原则。
栈中有很多的栈帧,随着方法的调用被创建。方法调用结束,栈帧也会弹出。
栈帧结构有:局部变量表、操作数栈、动态链接、方法返回地址、附加信息
局部变量表基本的存储单元是slot,存放编译期可知的8种基本数据类型+引用类型(reference)+返回地址类型(returnAddress)
当方法被调用,它的参数列表和局部变量都会按照顺序复制到局部变量表的slot上。
如果该栈帧是由构造方法或实例方法创建的,index=0的slot保存的是对象的引用this
只要局部变量表的指针不存在,其指向的内容就会被回收
操作数栈使用集合结构实现。其最大深度在编译期就定义好,是Code属性max_stack的值
计算的中间过程由操作数栈完成,比如复制、交换、求和等
方法的返回值会压入当前栈帧的操作数栈中
栈帧包含一个指向运行时常量池中该栈帧所属方法的引用
动态链接的作用就是 将符号引用(#7)转化为调用方法的直接引用(Methodref) => 方便程序访问运行时常量池
下面是B方法调用A方法反汇编后(javap)得到的字节码文件:
Constant pool
...
#7 = Methodref #8.#31 // com.test.methodA:()V
...
public void methodB();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
...
9: invokevirtual #7
可以看到方法B的第九行指向了常量池第七行的内容。
invokevirtual就是指动态链接,在编译期间不能确定调用对象;静态链接invokespecial,只能调用下面的方法:static方法、private方法、final方法、构造器、super.method(),调用对象在编译时就可以确定。
Code的stack=3 locals=1表示方法需要的操作数栈空间为3,局部变量数组空间为1
用来保存当前方法PC寄存器的值
栈帧中与虚拟机相关的信息,不一定存在
方法区和堆一样是各个线程共享的内存空间,并且可以选择大小。
方法区里存储着class文件的信息和运行时常量池,class文件的信息包括类型信息、静态变量、域信息、方法信息。
当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,即每个class都有一个运行时常量池。
运行时常量池里的内容除了是class文件常量池里的内容外,还将class文件常量池里的符号引用转变为直接引用,这点类似于动态链接
至今,方法区的实现方式有为永久代或元空间两种。
jdk1.7之前,方法区实现方式为永久代,存储的是JVM启动类装载器的字节码文件。
jdk1.7时,字符串常量池从方法区移到堆中。也就是说String str1=“abc”;String str2=“abc”;,str1==str2为true,abc在堆中且唯一。
jdk1.8,方法区改为元空间实现。方法区不再占用堆内存,改为占用系统内存。
这里网上会有不同观点,认为JDK1.8方法区还是在堆中。但实际经过下面的idea程序运行结果就可以进行反驳。
存放所有new出来的对象。
在JDK8中:
堆中还可以划分出新生代(1/3)和老年代(2/3),元空间(逻辑上属于堆)。
新生代可以分为伊甸园(8/10)、幸存0区(1/10)、幸存1区(1/10)。
这里的逻辑举例:
public void changeValue(String str) { str = "xxx"; }
public static void main(String[] args) {
String str = "aaa";
test.changeValue(str);
}
打印结果依然是aaa。
执行细节如下:
class Test {
private int count = 0;
public static void testStatic() {
int count = 1;
System.out.println(this.count);
}
}
这里的this会报错,通常的解释是this指代的当前对象还未创建所以没法使用。
本质上是因为static不是实例方法,this不存在于当前方法栈帧的局部变量表中
public class SubClass extends SuperClass{
public static void main(String[] args){
SubClass sb = new SubClass();
sb.method4SuperClass();
}
}
代码作用是子类继承父类并调用父类方法。
method4SuperClass方法的执行是栈内动态链接,如果换成super.method4SuperClass()则使用静态链接,在编译期间就完成链接,执行效率会有所提升。
方法中定义的局部变量是线程安全的。
因为局部变量存于方法的栈帧中,只有方法内部能够访问以及修改。
垃圾回收不会涉及到栈。
因为当方法调用结束后,顶层栈帧自动弹出,不会存在积压。
垃圾回收的主要是堆内无用的对象,无用的判断依据是看是否有对该对象的引用存在
在Run/Debug Configurations窗口配置Application的VM options为 -Xms1024m -Xmx1024m -XX:+PrintGCDetails
添加好参数后运行程序:
JDK11测试结果如下:
在控制台可以看出G1的总大小是等于堆内存的大小的。G1垃圾回收器下面会有介绍。
这里可以得到,元空间逻辑上属于堆,但实际占用的是系统内存。
JDK8的测试结果如下:
-XX:+PrintFlagsInitail 查看所有参数的初始值
-XX:+PrintFlagsFinal 查看所有参数的当前值
-XX:+PrintGCDetails 打印GC处理日志
-XX:+PrintGC 打印GC简要信息
-Xms 设置初始堆空间的大小
-Xmx 设置最大堆空间大小
-Xmn 设置新生代大小
-XX:NewRatio 设置新生代与老年代的比值
-XX:SurvivorRatio 设置新生代与S0/S1的比值
-XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄
-XX:HandlePromotionFailure 是否设置空间分配担保 => 在JDK6后不再使用。在minor gc之前,JVM会进行检查。如果老年代最大可用的连续空间大于新生代所有对象的总空间或者大于历次晋升的平均大小,就执行minor gc。其余情况改为执行full gc。
Parallel是jdk8中的垃圾回收器。在jdk8中需要使用-XX:+UseG1GC开启G1
G1是在jdk8之后默认的垃圾回收器,作用是在延迟可控的情况下尽可能提高吞吐量,以适应不断扩大的内存和不断增加的处理器数量,承担着全功能收集器的期望。
G1避免整个java堆中进行全局垃圾收集,优先回收价值最大的区间(Region)
下面是各个回收器的收集范围: