JVM学习笔记

JVM的作用

在软件层面上屏蔽不同操作系统在底层硬件和指令的区别。

类装载子系统

将编译好的.class文件装载到内存(运行时数据区)中。

字节码执行引擎

执行java字节码的引擎。

运行时数据区

Java虚拟机由类装载子系统,运行时数据区和字节码执行引擎组成

其中堆和方法区为线程共享,栈(线程),本地方法栈和程序计数器为线程独享。

栈帧
当前线程每运行一个方法就会在栈中为该方法开辟一个栈帧,栈帧中主要储存局部变量表操作数栈动态链接方法出口

首先我们来思考一个问题,为什么要用栈这种数据结构呢? 我们不妨来想想一个java程序是怎么运行的,它首先会从入口函数(main方法)开始运行,于是JVM要先给main方法开辟内存空间(main入栈),如果main方法中调用了一个compute方法,则再给compute方法开辟内存空间(compute入栈),而compute方法运行结束后它的内存空间会被释放掉(compute出栈),程序运行结束后再释放掉main方法的内存空间(main出栈),怎么样?是不是发现Java中函数的运行顺序和栈一样是先进后出的(FILO)?
栈帧的结构
栈帧中的局部变量表存储当前方法的局部变量,操作数栈中存储将要进行操作的数据,干说有点难以理解,我们举个例子,假如有一个compute方法:
public int compute() { int a = 1; int b = 2; int c = (a + b)*10; return c; }
当他被编译成java字节码之后是这个样子的:
在这里插入图片描述
我们一行一行来看,iconst_1表示将int类型常量1压入操作数栈,此时操作数栈中有一个元素“1”,istore_1表示将int类型值存入局部变量1,于是我们让操作数栈顶的“1”出栈,并赋值给局部变量中的第一个变量也就是a。这两行字节码对应的就是int a = 1 这行Java代码啦!我们继续往后看,iconst_2, istore_2对应了int b = 2,而iload_1和iload_2分别表示将局部变量表中的第一个、第二个变量的值装载入操作数栈中(注意此处只是把他们的值取出来,他们依然还在局部变量表中),此时操作数栈中有“1”和“2”两个元素,iadd表示将操作数栈顶弹出两个int类型相加再压入操作数栈,此时操作数栈中有“3”这个元素,bipush表示从常量池中取出int类型常量10压入操作数栈,此时操作数栈中有“3”和“10”两个元素,imul表示将操作数栈中弹出两个int类型相乘再压回栈中,此时操作数栈中有“30”这一个元素……怎么样?现在理解局部变量表和操作数栈的是怎么工作了吗?

另外我们注意到,上面的每行字节码前面都有一个数字,这其实就是程序计数器啦。程序计数器的本质是一个指针指向线程正在执行或马上要执行的那一行字节码的地址每一个线程都有自己的程序计数器,每执行完一行字节码,字节码执行引擎都会对程序计数器进行更新。方法出口则是另一个指针,它指向该方法执行完毕后应该返回到的位置,例如在上面的例子中,compute方法的方法出口就代表compute方法执行完毕后应该返回到main方法中的哪一行。

接下来谈谈线程共享的部分,刚才说了局部变量是存在栈帧中的局部变量表里的,而我们new出来的对象则主要是存放在堆中的而栈中的对象类型的局部变量都会有一个指针指向堆中new出来的对象,堆内的细节我们放在之后再讲。方法区(或者叫元空间)中主要存放常量,静态变量,类元信息。常量和静态变量好理解,类元信息指的是每一个类(class)的信息都会被类装载子系统装载到方法区中。事实上每一个new出来的对象的对象头中都有一个头指针指向方法区中它所属的那个类。

到这里终于能来讲一讲动态链接啦,动态链接其实是个比较抽象的概念,我们先来思考一个问题,math.compute();我们都知道这行代码的意思是执行math对象的compute方法,并且我们通常觉得它能执行compute方法是理所应当的,但实际上Java程序是怎么找到compute这个方法指令码的地址并执行它的呢?其实这就是动态链接的作用啦,我们在上面说了对象头指针,在这个例子中程序运行的时候会根据math对象的头指针找到compute方法指令码的内存地址并存放在动态链接中,之后再执行compute方法的时候就能通过动态链接找到该方法在方法区中的地址啦!需要注意动态链接一定是在程序运行的过程中动态生成的。

最后来说说本地方法栈,什么叫本地方法呢?这就要从Java的历史讲起了,我们大家知道Java是1995年由SUN公司发布的,那个时代的编程语言几乎都是C的天下,这就导致了Java的很多底层都是由C语言实现的,在Java的源码中,用native关键字修饰的方法都表明它的底层是由C实现的,这种方法就叫做本地方法。事实上,这些由C语言实现的方法也是需要内存空间来存储局部变量的,而这就是每一个线程中的本地方法栈的作用啦!


堆
我们在上面说到了堆里主要存放new出来的对象,其实堆内还有更细节的区块细分,堆空间分为年轻代和老年代,通常老年代占堆空间的2/3,年轻代占1/3。年轻代中又分为Eden区和Survivor区,默认下Eden区占年轻代空间的8/10,Survivor区占年轻代的2/10。

当我们向堆中存在对象时首先会放在Eden区中,当Eden区空间被占满后,字节码执行引擎会做一个垃圾收集的操作,也就是minor gc(gc分为minor gc和full gc,minor gc相对full gc来说对性能的影响微乎其微),将Eden区中的无效对象清理掉。那什么叫做无效对象呢?我们之前说了,栈帧中的对象类型的局部变量都有一个指针指向堆中的对象,当方法执行完毕,栈帧的内存空间被释放掉后,该指针也不存在了,可是堆中的对象却依然存在,并且变成了一个游离对象,没有任何指针指向它,它也自然不能被其它任何线程调用了,这样的对象就是无效对象栈中的本地变量表又被成为GC Roots根,当一个对象到GC Roots根没有任何引用链相连的话,则证明该对象是不可用的
GC ROOTS根
完成minor gc后,幸存的有效对象会被转移到Surviror区,并且分代年龄+1(对象的分代年龄也是记录在对象头里的)。随着不断有新对象放入Eden区,Eden区会不断执行minor gc,而每次minor gc都会有幸存对象添加到Survivor区,直到Survivor区也被放满,于是Survivor区也会触发minor gc。事实上Survivor区也是分为两个区域的,我们姑且称为from区和to区,一开始Eden区的幸存者会放在from区,当from区放满后对from区进行gc,from区的幸存者转移到to区,之后Eden区的幸存者又会放在to区,直到to区放满再对to区进行gc,to区的幸存者转移到from区,之后Eden区的幸存者又会放在from区,直到from区放满……(禁止套娃!!!)。需要注意的是垃圾回收的算法是有很多,这里用到的是复制算法。Survivor区中gc幸存者的分代年龄也会+1,随着套娃的进行,一直没被gc收集掉的对象的分代年龄会不断+1,默认情况下当一个对象的分代年龄一直苟到了15还没被销毁,Java虚拟机就会把这位长者直接移送到老年代,从此可以永续下去了。

当老年代也放满后会触发full gc,也就是对老年代进行垃圾收集,一旦full gc也无法释放老年代中的任何空间时则会发生内存溢出。另外,full gc的时候Java虚拟机会暂停应用线程的执行,直观来说就是程序会卡住,所以JVM调优的主要目的就在于减少full gc的次数,以及一次full gc消耗的时间

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值