目录
JDK体系结构
Java语言的跨平台特性
JVM整体结构及内存模型
JVM具体结构
栈
老师主要认为这个栈就是线程栈,因为一个线程进行执行就会分配对应内存区域,就是栈
每当创建一个方法会在对应栈中创建一个栈帧,用来分配内存空间,可以这么理解,不同栈帧将方法的局部变量隔离出来
如果栈里面存放了很多局部变量,局部变量是对象类型的话,那它的值就是这些对象在堆里面的内存地址
为什么用栈来当存放栈帧内存空间
当我们先调用main方法,会先给main方法分配内存空间,再调用compute方法,然后会给compute方法对应分配内存空间,这个compute方法后调用会先执行完,执行完就将这部分内存空间直接释放掉,跟栈的FIFO特性相吻合,后分配的内存空间反而被先释放掉
局部变量
可以理解为槽或者数组,就是用来存放局部变量的
操作数栈
就是要操作的数据中转存放的临时空间
动态链接
compute相当于一个符号,代码执行遇到这个符号之后需要进行解析,程序加载的时候会对静态资源进行解析,compute这种方法只有在程序运行的时候进行解析,就是要找到运行这行的代码,代码指令加载到方法区里面去,对应有内存的位置,动态链接就是在程序的运行过程中,把这些符号引用转换为方法对应代码的内存地址
方法出口
根据方法出口信息知道在外层方法具体哪个位置继续执行
执行示例:
方法
将int类型常量1压入操作数栈中
将int类型值存入局部变量1中,这个局部变量1就是a
将int类型常量2压入操作数栈中
将int类型值存入局部变量2中,这个局部变量2就是b
从局部变量1中装载int类型的值,就是把1放到操作数栈中
从局部变量2中装载int类型的值,就是把2放到操作数栈中
执行int类型的加法,会把操作数栈的数据拿出来,计算完之后把值重新压回操作数栈中,在cpu内部进行操作
将一个8位带符号整数压入栈中,就是把10放到操作数栈中
执行int类型的乘法
将int类型值存入局部变量3中,这个局部变量3就是
从局部变量3中装载int类型的值,就是把30放到操作数栈中
把值返回到主线程里面去
局部变量0,局部变量1,这个可以理解为索引,局部变量0用来存放this
程序计数器
每个线程独有的内存空间,当前线程马上运行代码的位置
为什么设计程序计数器
每次运行代码,字节码执行引擎都会修改程序计数器里面的这个值,因为多线程,当前线程执行的时候,时间片被其他线程抢占,当前线程会挂起等待下次抢到cpu执行权,等到下次可以继续执行的时候,可以通过程序计数器得知下次代码执行的开始位置
方法区
如果方法区有很多静态变量,恰好很多静态变量类型是对象,对象是存放在堆里面的,那么这些静态变量存放的是这些对象在堆里面的内存地址
在jdk1.8之前叫做永久代,1.8之后包括1.8叫元空间,用的是直接内存就是物理内存
假设我们元空间设置的21M,我们进行回收之后只剩下1M,下一次就会把设置值自动调到15M,假设回收之后剩下20M,下一次会自动设置到30M,一般来说我们设置设置初始和最大大小一样,这样就不需要进行扩容或者缩容
本地方法栈
本地方法也是需要有内存空间进行存放,如果线程在运行过程中,调用了本地方法,本地方法也需要分配内存,就是分配在本地方法栈里面的,也是每个线程独有的
堆
minor GC是在Eden满的时候进行的垃圾回收,触发垃圾线程进行垃圾收集
GC过程在方法栈找GC root, 比如局部变量和静态变量就可以作为gc root,就是堆上被外部变量所引用,从栈上找局部变量,从方法区里面找静态变量,找到所有引用对象,只要从gc root根节点出发在链条上面的所有对象都认为是非垃圾对象,会将非垃圾对象放到survivor区里面,一个对象如果经历过一次gc之后,没有被回收掉分代年龄会+1,如果下一次eden区又被放满了,不但回收eden区,而且会对survivor非空区也进行gc,把存活的对象放到空的survivor区中,分代年龄+1,当分代年龄到15还没有被回收掉,那么这个对象会被放到老年代
缓存对象,静态变量,spring容器对象最后都会放到老年代
heapTests相当于gc root,我们不断地new对象放到heapTests里面,heapTests相当于一直是有用的,eden区不断的产生新对象,因为一直被引用着,即使被放满了也不会进行回收,就会出现oom,老年代如果放满了会触发full gc,full gc是对整个堆和方法区进行回收
gc的过程中会出现stw,stop the world,实际上就是停止掉用户线程,我们用户在电商网站上下单,后端就有对应的执行线程,当我们gc过程中,会停止对用户发起的所有线程,如果用户正在下单,开始gc,如果时间比较长,用户会感觉卡顿一下,对体验和性能有一定影响,我们调优就是减少gc,主要就是full gc,因为full gc收集的时间比较长,减少full gc次数或者full gc回收时间,minor gc也要减少执行次数,它的stw时间特别的短
为什么要STW
我们在gc过程中就是找出非垃圾对象,如果发生了full gc,我们顺着其中一个gc root开始找非垃圾对象,这个链条找完之后,如果我们没有STW,这个线程还在继续执行,假设这个线程执行结束,所有的变量都已经不用了,我们之前找出的非垃圾对象,现在变为垃圾对象,相当于gc白做了
JVM内存参数设置
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
-Xss:每个线程的栈大小
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
StackOverflowError示例:
// JVM设置 -Xss128k(默认1M)
public class StackOverflowTest {
static int count = 0;
static void redo() {
count++;
redo();
}
public static void main(String[] args) {
try {
redo();
} catch (Throwable t) {
t.printStackTrace();
System.out.println(count);
}
}
}
运行结果: java.lang.StackOverflowError at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:12) at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13) at com.tuling.jvm.StackOverflowTest.redo(StackOverflowTest.java:13)
结论:-Xss设置越小count值越小,说明一个线程栈里能分配的栈帧就越少,但是对JVM整体来说能开启的线程数会更多
其他
老师理解full gc和major gc一样,有的人认为major gc只回收老年代的对象