JVM内存模型图
当通过命令来执行字节码文件的时候,java虚拟机首先会通过类装载子系统将字节码文件加载到虚拟机中,然后再将字节码文件丢到内存区域中的方法区中,最后再由字节码执行引擎去执行方法区中的代码
当一个线程执行上面的代码的时候,虚拟机就会给我们线程分配一块内存区域,从线程栈中划一小块区域给当前线程,用来存放局部变量,再来一个线程也是一样
线程栈: 是由一块一块的栈帧组成的,一个方法对应一块栈帧内存区域,用来存放方法中的局部变量,因为局部变量只在方法的作用域范围内有效,所以放到不同的内存空间把它们隔离起来更好一点,线程栈用的就是我们常说的栈结构,栈结构的特点是先进后出,和程序嵌套调用方法的逻辑是非常相符的,先执行的方法先分配内存,但是后释放内存,后执行的方法后分配内存,但是先执行完毕所以先释放内存,比如上面的main方法和computer()方法的嵌套调用
栈帧由局部变量表,操作数栈,动态链接和方法出口四部分组成
局部变量表: 好理解,就是存放我们的局部变量, 但是要注意,当局部变量是非对象类型的时候,局部变量的值是存放于局部变量表中, 但是当局部变量是对象类型的时候,局部变量表存的是对象的内存地址,因为对象是存放于堆中的(比如上图中main方法的局部变量就是对象类型)
所以栈和堆是引用的关系,栈引用堆中对象的内存地址
操作数栈:打开terminal,使用javap命令对我们的代码进行反汇编,生成一个字节指令码文件,将结果输出到txt文件中:
javap -c Math.class > Math.txt
字节码指令文件中的computer方法:
从上图可以看出: 操作数栈就是操作数在内存中进行加减乘除运算时的暂存空间(临时内存空间).
动态链接: 如下图
方法出口: 上图中,当computer方法执行完之后,需要执行System.out.println(“test”),而不能从头开始执行,所以方法出口就是用来记录方法在代码中的位置
程序计数器:线程专享的,独有的,因为不同的线程执行的代码的位置都不一样,只要java虚拟机运行,就会给当前的线程分配一块专属的内存空间,也就是程序计数器空间,用来记录当前线程正在运行的代码的位置,或者说行号,为什么要设计程序计数器呢,举个例子,比如一个线程正在执行我们上面的代码,当执行到int b = 2的时候,这时候,另外一个线程抢到了cpu的执行权,这时候我们之前的线程就要挂起,把cpu让出来,当该线程执行完之后,如果之前的线程又抢到了cpu的执行权,这时候应该从之前挂起的位置去执行代码,即int b = 2的位置,而不是从头开始执行,但是cpu并不知道从哪里开始执行,这时候程序计数器就有作用了
程序计数器的值是由字节码执行引擎修改的,字节码执行引擎在执行我们的代码的时候顺便会记录代码执行到的位置
方法区: 在jdk1.8之后改名为元空间,方法区是直接内存,简单的说就是非堆内存,用来存放常量,静态变量和类元信息, 线程在执行我们的代码的时候,类装载子系统会先将字节码文件加载到方法区中,所以类的一些基本信息存放于方法区中
注意: 当方法区中的静态变量是对象类型的时候,比如上面的代码中的user静态变量,方法区中存放的也是对象的内存地址,所以方法区和堆也是引用的关系
本地方法栈:
要理解本地方法栈,得先知道什么是本地方法,举个例子:当通过new Thread().start()调用线程的start方法的时候,进入start方法的源码:如下图
JVM对象创建与内存分配机制深度剖析
可达性分析算法:
将"GC Roots"对象作为起点,从这些节点开始向下搜素引用的对象,找到的对象都标记为非垃圾对象,其余未标记的都是垃圾对象
GC Roots根节点: 线程栈的本地变量, 静态变量, 本地方法栈的变量等.
上面的本地变量math和静态变量user就可以看作是GC Roots
当Eden区放满了的时候(不是完全放满,占一定比例就会开始gc),会进行minor gc
第一次minor gc的时候,会先去找math变量引用的那个对象,然后再看是否有成员变量引用了其他对象,新找出来的对象如果又有成员变量引用了其他的对象,则继续往下找,直到找到最后一个对象没有任何一个成员变量引用其他的对象,这条链接就算找完了,这条链接上的所有对象都会被标记为非垃圾对象,这些非垃圾对象会被保留并转移到空的Survivor区(第一次会放到S0),对象的分代年龄为1,垃圾对象会被直接干掉,这样Eden区就被清空了
第二次minor gc的时候,会回收Eden区和Survivor区, Eden区的回收方式和第一次回收的时候是一样的, S0中的非垃圾对象会转移到S1中,并且分代年龄+1, 当对象的分代年龄等于15的时候,会被放到老年代中
当老年代也放满了的时候,会进行full gc,当老年代中的对象都是非垃圾对象而回收不掉, 又不断的有对象往老年代中转移,这时候就会OOM(堆内存溢出)
打开cmd输入jvisualvm可以打开jdk自带的jvm调优诊断工具,可以识别所有的jvm进程,点击Visual GC可以查看堆内存中各块区域内存的实时扭转图
JVM调优的目的:是为了减少gc的次数
因为当jvm底层在做gc的时候,不管是minor gc还是full gc,都会执行一个叫STW的机制,STW就是stop the world,会让应用程序暂停掉,停掉所有的用户线程,专心做垃圾回收,比如用户点击下单的时候会感觉到卡顿
所以jvm调优的目的也可以说是为了减少STW的次数
那么开发人员为什么要设计STW机制呢?
在gc的时候,jvm底层会根据GC Roots去找变量所引用的对象,如果在这个过程中还有用户线程在执行,那么当用户线程执行结束的时候,该线程对应的栈内存空间会被释放掉,该局部变量也会被释放掉, 所以对应的指针(指向对象的内存地址)也没有了, 则之前找到的非垃圾对象变成了垃圾对象,这样gc就永远结束不了了
简单的说就是: 如果没有STW机制,那么在gc的时候同时又会产生垃圾对象,这样会导致gc无法结束
gc的时候,jvm会专门搞一个线程用来执行gc