JVM一个在Java程序背后默默工作的人,任劳任怨,有实际使用情况中有时需要针对程序对JVM进行一些设置,进行一些处理,而这也是本文要说的如果需要对JVM进行调优工作,我们首先需要了解JVM内存机制以及垃圾回收的机制
jvm的内存图如下
内存空间小,线程私有。改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成
栈,数据暂时存储的位置,先进后出的原则存储数据,线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表
、操作数栈
、动态链接
、返回地址
,个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表:主要保存函数的参数以及局部的变量信息
操作数栈:一个后入先出栈,属于方法执行时的计算区域, 方法计算时字节码指令往操作数栈中执行入栈和出栈操作
动态链接:指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
返回地址:保存当前帧栈,方便恢复上层方法执行状态
为本地方法提供服务,其中本地方法指的是操作计算机底层的一些代码,这部分代码并非使用Java编写,存储在本地
JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。 由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
线程共享区域,存储包括类信息,类型常量池,字段信息,方法信息,类变量(也就是静态变量),类加载器信息,指向Class实例的引用,方法表
这里的类型常量池指的是类里面的常量,在类加载的时候会把把里面的内容放到方法区中的运行时常量池中
在jdk7之前,HotSpot虚拟机使用的是永久代,方法区和永久代有本质上的不同,方法区是JVM标准,而永久代是JVM规范中的具体实现,并且只有HotSpot才拥有永久代,其他虚拟机没有。而在1.8版本中,移除了永久代,具体点说就是
JDK8 HotSpot JVM 移除永久代,使用本地内存来存储类元数据信息并称之为:元空间(Metaspace)
两者最大区别在于:元空间使用的是本地内存而不是JVM虚拟机。
另外注意在原来的JDK7这个版本中字符串常量池原本在永久代中,但是在后面JDK8中永久代被移除,所以这个时候常量池这个东西也就是从方法区移动到了Java堆中,这里注意注意:这里说的是字符串常量池,运行常量池包含字符串常量池,但是还有其他部分,1.8后字符串常量池移动到堆中,其他的还是在原地
在我们了解到JVM后,接下来我们说下是怎么运行这些内存的,创建一个对象步骤如下
1、当虚拟机收到一条new指令时,首先将检查当前new的类是否在常量池被加载过(在常量池找到需要new的类的符号,检查其是否被初始化过)。如果没有,说明类没有加载过,则执行相应的类加载过程;如果有则直接准备为新的对象分配内存。
2、把计算后的内存大小从内存中划取出来,内存的分配有两种
指针碰撞:
想象一个内存空间:左侧是已使用的内存区域,右侧是空闲内存区域,中间是指针来作为分界点的指示器。当我们分配内存时,只需要指针向右移动与对象所需内存相同大小的距离即可,所以称为“指针碰撞”
空闲列表:
当空闲内存区域和已使用内存区域相互交错时,虚拟机就需要维护一个列表,用来记录空闲内存块。当需要分配内存的时候,则需要找到一块足够大的内存区域分给对象实例,并更新表中的记录
3、线程安全问题,当在多线程情况下创建对象可能造成意外情况,所以这时候使用的不是上面的两种,而是新的方法,
4、初始化内存空间,把分配后的内存空间中的值初始化为0
5、设置对象必要参数,类的元数据信息,对象的hash码,GC年龄,锁相关
6、最后执行对象构造函数
简单点说流程就是:
检查有没有 ---》 开始分配内存 ----》 初始化值为零 ----》 添加对象必要参数 ----》 执行构造函数
垃圾回收(GC)机制,JVM中一个面试以及工作中常看到的值,垃圾回收机制回收的就是不用的对象,以此来释放内存,增加软件的健壮性
垃圾回收机制主要的操作区域就是堆和方法区,这里面如果要进行处理的话就需要先找到需要回收的对象,这里面主要使用发方法有
引用计数算法:堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1
可达性算法:程序把所有的引用关系看作一张图,从一个节点GC ROOT(根目录)开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。在Java中,虚拟机栈引用的对象,方法区中静态属性引用的变量,常量引用的对象,本地方法引用的对象不能作为GC ROOT
这里面的引用共分为强引用,软引用,弱引用,虚引用,引用的强度逐渐减弱
强引用:普遍存在的,直接通过new创建的,GC不会回收被引用的对象
软引用:还有用但并非必须的对象,在内存溢出异常之前会把这些对象进行回收
弱引用:描述非必须对象,只生存到下次垃圾收集发生前
虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。
标记-清除算法
先扫描,对存活的对象进行标记,标记完成后扫描整个空间,然后回收没有标记的对象。好处是回收方便,坏处是会造成内存碎片
复制算法
为解决内存碎片的问题,把堆分成一个对象面或者多个 空闲面, 程序从对象面为对象分配空间,当对象满了,收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存
标记-整理算法
类似于标记清除算法,不同的是回收后会把剩余存活的对象那个向空闲空间移动,并更新指针,以此来解决内存碎片问题,但是会提高回收成本
分代回收算法
根据对象存活的声明周期把内存分为若干个区域,在JVM中堆分为两个区域:新生代,老年代。
其中新生代分为三块区域:Eden、From Survivor、To Survivor ,之间的比例是8:1:1 。
大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
老年代,可以理解经过多次垃圾回收仍然存在的对象,当老年代存满的时候出发Full GC
这里的Full GC 指的是对整个堆进行处理回收,共有四种情况导致Full GC
a) 年老代(Tenured)被写满;
b) 持久代(Perm)被写满;
c) System.gc()被显示调用;
d) 上一次GC之后Heap的各域分配策略动态变化;
除了Full GC出发之外还有一种执行,就是 Scavenge GC , Scavenge GC 是在Eden申请空间失败时触发,对Eden进行GC,清除非存活对象,并且把存活的对象移动到Survivor区。然后整理Survivor的两个区