JVM小结【持续更新】

// 回过头学习了JVM,进行一次全面的总结
// 网上关于JVM的帖子很多都存在问题,我查阅了很多资料确保内容的正确性。如有问题欢迎指正。

JVM的结构

JVM是在硬件和操作系统支持之下的java虚拟机
JVM小结【持续更新】_第1张图片
JVM小结【持续更新】_第2张图片

类装载子系统

xxx.java 经过javac编译为xxx.class,此文件经过类装载子系统编译为xxx.Class字节码文件。
类装载子系统共有四种。

  1. 启动类装载器:用来编译java运行所需的基础类模板。即jre中rt.jar包中的内容
  2. 扩展类装载器:用来编译java运行时需要用到的javax包中的内容
  3. 用户自定义类装载器:用来编译java运行时用户自定义的类的内容
    类装载子系统的双亲委派机制保证了沙箱机制。即不会被用户自定义的类污染java原有的类

本地方法栈、本地方法接口

本地方法接口是指java代码中用native关键字表示的函数,没有方法体。java会调用第三方程序实现这些方法。
本地方法栈用于管理native方法的调用。

执行引擎

执行引擎把字节码转换成可以直接被JVM执行的机器语言

程序计数器

等价于汇编中的IP计数器,内部存放指向程序下一步代码的指针。

存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆。
栈满足先进后出的原则。
栈中有很多的栈帧,随着方法的调用被创建。方法调用结束,栈帧也会弹出。
栈帧结构有:局部变量表、操作数栈、动态链接、方法返回地址、附加信息

1. 局部变量表

局部变量表基本的存储单元是slot,存放编译期可知的8种基本数据类型+引用类型(reference)+返回地址类型(returnAddress)
当方法被调用,它的参数列表和局部变量都会按照顺序复制到局部变量表的slot上。
如果该栈帧是由构造方法或实例方法创建的,index=0的slot保存的是对象的引用this
只要局部变量表的指针不存在,其指向的内容就会被回收

2. 操作数栈

操作数栈使用集合结构实现。其最大深度在编译期就定义好,是Code属性max_stack的值
计算的中间过程由操作数栈完成,比如复制、交换、求和等
方法的返回值会压入当前栈帧的操作数栈中

3. 动态链接

栈帧包含一个指向运行时常量池中该栈帧所属方法的引用
动态链接的作用就是 将符号引用(#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

4. 方法返回地址

用来保存当前方法PC寄存器的值

5. 附加信息

栈帧中与虚拟机相关的信息,不一定存在

方法区

方法区和堆一样是各个线程共享的内存空间,并且可以选择大小。
方法区里存储着class文件的信息和运行时常量池,class文件的信息包括类型信息、静态变量、域信息、方法信息。
当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,即每个class都有一个运行时常量池。
运行时常量池里的内容除了是class文件常量池里的内容外,还将class文件常量池里的符号引用转变为直接引用,这点类似于动态链接
JVM小结【持续更新】_第3张图片至今,方法区的实现方式有为永久代或元空间两种。
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)。
这里的逻辑举例:

  1. 程序new出来的100个对象都在伊甸园,然后经过jvm的minor gc进行筛选(大约删除98%),存活下来的两个进入幸存0区(又称作from)
  2. 程序继续new出100个新对象,伊甸园又满了,jvm将伊甸园和幸存0区(又称作from)的102个对象进行minor gc,存活下来的进入幸存1区(又称作to)
  3. 程序继续new出100个新对象,伊甸园又又满了,jvm将伊甸园和幸存1区(又称作from)的对象进行minor gc,存活下来的进入幸存0区(又称作to)
  4. 由此可见,幸存0区和幸存1区的进出是不断变化的,但都是从from到to。
  5. 若有对象经过15次minor gc依然存活(可通过-XX:MaxTenuringThreshold更改默认次数),则进入老年代。
  6. 如果老年代也满了,JVM就会执行major gc
  7. major gc通常会和full gc混合使用。minor gc只收集新生代垃圾,major gc只收集老年代垃圾,full gc是整个堆和方法区的垃圾收集
  8. 若多次major gc仍然无法保存新进老年代的对象,就会报OutOfMemoryError(OOM)
    另外
    1. gc在处理的时候会导致用户线程的暂停(STW)。major gc和full gc所需时间是minor gc的十倍以上,所以JVM调优就是让前两种gc更少的出现。
    2. 由此可得平常写代码时要避免创建过多的临时的大对象,因为伊甸园剩余空间可能放不下,导致占用老年代内存或是执行无用的major gc。
    3. 所有线程访问唯一的堆空间,但为了避免加锁导致的性能降低,JVM默认在每个线程开启后会分配一小块线程独立的区域,称作TLAB(默认为伊甸园的1%,可通过-XX:TLABWasteTargetPercent修改)。

利用JVM分析常见代码及问题

1
public void changeValue(String str) { str = "xxx"; }
public static void main(String[] args) {
	String str = "aaa";
	test.changeValue(str);
}

打印结果依然是aaa。
执行细节如下:

  1. main方法先被压进栈,然后main的栈帧的局部变量表中的slot依次为args、str。
  2. 在堆的字符串常量池中创建aaa,main栈帧的str指向aaa
  3. 调用changeValue方法,开辟新栈帧,局部变量表中的slot为str。堆中创建xxx,str指向aaa
  4. 执行方法体,str指向xxx
  5. changeValue方法结束,弹出栈帧,局部变量str随即被销毁
  6. main方法结束,弹出栈帧,局部变量表被销毁
2
class Test {
	private int count = 0;
	public static void testStatic() {
		int count = 1;
		System.out.println(this.count);
	}
}

这里的this会报错,通常的解释是this指代的当前对象还未创建所以没法使用。
本质上是因为static不是实例方法,this不存在于当前方法栈帧的局部变量表中

3
public class SubClass extends SuperClass{
	public static void main(String[] args){
		SubClass sb = new SubClass();
		sb.method4SuperClass();
	}
}

代码作用是子类继承父类并调用父类方法。
method4SuperClass方法的执行是栈内动态链接,如果换成super.method4SuperClass()则使用静态链接,在编译期间就完成链接,执行效率会有所提升。

4

方法中定义的局部变量是线程安全的。
因为局部变量存于方法的栈帧中,只有方法内部能够访问以及修改。

5

垃圾回收不会涉及到栈。
因为当方法调用结束后,顶层栈帧自动弹出,不会存在积压。
垃圾回收的主要是堆内无用的对象,无用的判断依据是看是否有对该对象的引用存在

IDEA配置JVM

1. 验证方法区独立于堆内存之外

在Run/Debug Configurations窗口配置Application的VM options为 -Xms1024m -Xmx1024m -XX:+PrintGCDetails
添加好参数后运行程序:
JDK11测试结果如下:
JVM小结【持续更新】_第4张图片在控制台可以看出G1的总大小是等于堆内存的大小的。G1垃圾回收器下面会有介绍。
这里可以得到,元空间逻辑上属于堆,但实际占用的是系统内存。

JDK8的测试结果如下:

JVM小结【持续更新】_第5张图片

2. 常用的JVM堆空间参数

-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)
下面是各个回收器的收集范围:
JVM小结【持续更新】_第6张图片

你可能感兴趣的:(jvm)