JVM总结

JVM内存模型

image.png
  • JVM内存模型中栈就是方法栈,入栈出栈就是方法的入栈出栈,每个方法中包含4部分,局部变量,动态链接,返回地址,操作数栈

  • TLAB是每个线程都会分配一块空间,是为了避免多线程创建对象的时候竞争CAS效率慢。默认为开启

  • 《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。在不同的 JVM 上方法区的实现是不同的。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。换句话说:方法区是一种规范,永久代是Hotspot针对这一规范的一种实现。而永久代本身也在迭代中:
    (在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间(Metaspace),‑XX:MaxPermSize 参数失去了意义,取而代之的是-XX:MaxMetaspaceSize

  • 直接内存为内核区域的内存,可以减少内核态和用户态的数据拷贝,可以通过:unsafe.allocateMemory(xxx);来申请,不会占用堆内存空间

栈内存模型

栈模型.png

局部变量表

  • 局部变量表用于存放方法参数和方法内部定义的局部变量
  • 大小由class文件中,Code属性的max_locals数据项中来确认最大容量
  • 容量以slot为最小单位,可以使用32位或更小来实现
  • slot是可以复用的,如有些变量作用域不一定是整个方法(比如在方法内部定义代码块),超出作用域后局部变量slot不会释放只有重复使用读取或负值的时候才会清除
    例子:
    public static void main(String[] args){
    {
    byte[] test = new byte[64*1024];
    }
    //int a = 0;
    System.gc();
    }
    当int = 0是注释的时候,执行到System.gc的时候是不会释放空间的,打开的话会释放空间,我们可以在使用完后把test设置为null就可以释放
  • 上面例子中,test的引用值(reference)和0都会存入局部变量表中

操作数栈

  • 栈的最大深度也是在Code属性的max_locals数据项中来确认最大容量
  • 运算时会使用栈来进行压栈和出栈,如:iadd就是出栈两个int进行相加压栈结果
  • 大多数虚拟机里下面栈帧部分操作数栈和上面栈帧的部分局部变量表重叠在一起

动态链接

  • 指向常量池中栈帧所属的方法的引用

方法返回地址

  • 记录方法执行完成后的返回地址,不管是正常return还是抛出异常都需要返回到方法被调用的位置
    为什么还需要记录,如果调用者的栈帧中记录调用时的PC指针不是可以确定吗?
    正常退出的时候可以使用,但是异常退出的时候是通过异常处理器的表来确定的

附加信息

  • 取决于具体虚拟机的实现,如调试相关信息

其他

通过逃逸分析如果确认没有逃逸就可以直接将对象分配到栈中,对象的生命周期就和栈一样避免垃圾回收。java1.7后默认开启

  • -XX:+DoEscapeAnalysis:开启逃逸分析,
  • -XX:-DoEscapeAnalysis:关闭逃逸分析
    如:
    第一段代码就逃逸了,因为sb的作用域已经超出方法,而第二段代码sb没有逃逸可以分配到栈中
public static StringBuffer craeteStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb;
}
 
public static String createStringBuffer(String s1, String s2) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

栈总结演示

public int calculate(){  
    int a = 100;  
    int b = 200;  
    int c = 300;  
    return (a + b) * c;  
}  

javap之后看到,Stack=2, Locals=4代表需要2个操作数栈和4个slot局部变量表:

public int calculate();  
  Code:  
   Stack=2, Locals=4, Args_size=1  
   0:   bipush  100  
   2:   istore_1  
   3:   sipush  200  
   6:   istore_2  
   7:   sipush  300  
   10:  istore_3  
   11:  iload_1  
   12:  iload_2  
   13:  iadd  
   14:  iload_3  
   15:  imul  
   16:  ireturn  
}  

执行过程,中栈的使用:


image.png

image.png

image.png
image.png

image.png

image.png

堆内存模型

image.png
  • 根据创建对象的特性大部分都是生命周期很短,java堆可以分为新生代和老年代,
  • 不同的代根据存储对象的特性不同可以使用不同的垃圾回收算法,新:Minor GC,老:Full GC
  • 默认老年代和新生代空间比为:2:1,新生代eden和s0,s1的空间比8:1:1

新生代

  • 新生代分为eden,s0,s1区
  • 新生代的回收算法一般为复制算法
  • 创建对象的时候会在eden区中分配内存空
  • 由于多线程分配内存有并发问题,可以通过-XX:UseTLAB卡其TLAB,每个线程在Eden区都会有属于自己的空间默认每个TLAB只占1%的内存空间
  • TLAB设置了最大浪费空间,要分配的对象在TLAB中空间不够,如果TLAB剩余空间大于最大浪费空间就直接区eden区创建,小于的话就重新申请一个TLAB
  • s0和s1是内存垃圾回收(Minor GC)时会用到,将eden区和s0活着的对象复制到s1中,然后请求eden和s0,下次GC的时候将eden和s1活着的对象复制到s0中,每次GC会增加对象的年龄
  • 默认达到15岁时就会将对象送入老年代,进过Minor GC,s0或s1放不下就直接进入老年代
  • 空间担保,创建对象时如果Minor GC后,eden的空间不够并且要创建的对象如果小于eden的一半空间则在老年代担保内存空间,将eden中的对象存入老年代然后将新创建的对象存入eden中,如果大于eden的一半则直接存入老年代
  • 为什么要有Survivor区?由于新生代使用复制算法进行回收所以需要空间来存储活着的对象,如果直接进老年代会频繁full GC(STW)
  • 为什么要有Survivor区是两个?第一次Minor GC就会有对象进入Survivor区,下次在Minor GC就需要令一个来进行替换,如果只有一个不管是清除还是复制到eden区都会有性能下降

老年代

  • 老年代存储的是经过多次Minor GC年龄超过15岁的对象,还有一些大对象,大于eden空间的一半的对象
  • 老年代的回收算法为标记清除法(CMS)

方法区内存

  • 方法区也是所有线程共享。主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。
  • 方法区是JVM的定义,具体的实现有虚拟机来定义,在jdk1.7之前为永久代来实现,在1.8后为Metaspace空间来实现(hotspot)
  • 类文件中常量池 ---- 存在于Class文件中
  • 运行时常量池 ---- 存在于内存的元空间中
    主要用于存放编译期生成的各种字面常量以及符号引用,这部分的内容将在类加载后进入常量池中存放。
  • 字符串常量池 ---- 存在于堆中

程序内存分配演示

image.png


image.png
  • 句柄指针最大的好处是reference中存储的是稳定的地址,在对象被移动(垃圾回收)时只需要改变句柄中的指针而不需要改变所有栈局部变量表中的指针
  • 直接指针最大的优点访问速度快,减少一次指针的定位

HotSpot默认使用直接指针

垃圾回收

见:https://www.jianshu.com/p/30c36d2b914f

JVM命令查看

见:https://www.jianshu.com/p/f461d3582790

你可能感兴趣的:(JVM总结)