内存池与JVM内存模型

一、内存池概览

其实JVM中的内存池并不是真实存在的,它是OS堆中划分出来的一部分,会通过chunk来记录内存的使用。
内存池与JVM内存模型_第1张图片

JVM内存模型其实就是JVM在启动的时候从操作系统内存中要了一块大内存,然后将这个大内存分成五个区域:方法区、堆区、虚拟机栈、本地方法栈、程序计数器。
内存池与JVM内存模型_第2张图片

二、结合实例代码分析

public class Test {
    public static Test test4 = new Test();

    public static void main(String[] args) {
        Test demo = new Test();
        System.out.println(demo.add());
    }

    public int add() {
        int a = 10;
        int b = 20;

        return a + b;
    }
}

当我们执行这段代码的时候,在JVM中到底发生了什么呢?

  1. 调用javac命令,将.java文件编译成.class文件(字节码文件);
  2. 调用java命令,运行.class文件

我们再来细分.class的加载:

2.1、类加载子系统

我们先来了解一下下面四个概念:

class文件:指的是硬盘上的.class文件
class content :指的是类加载器将.class文件加载到内存,存储字节码文件数据的那块内存区域。 在源码中是使用stream字节流来读取的

 ClassFilesStream* cfs = stream()

然后会对读取到的文件进行解析,会解析成以下几部分:

  • InstanceKlass:存放类的元信息,包括字面量(类名、属性名、属性签名、方法名…). 因为我们自己写的测试类是AppClassLoader加载的,所以说会在元空间中划分出一块属于AppClassLoader的内存空间,然后将生成的instanceKlass文件存储到该AllClassLoader的内存中。

在方法区中,实际上会按照不同的类加载器分配出对应的内存空间,用来存储该类加载加载的类:

  • InstanceMirrorKlass: 镜像类,镜像类存储在堆区,用于表示java.lang.Class,Java代码中获取到的Class对象。这里我们有必要明确一下,静态变量是存储在哪里的?其实,静态变量时存储在镜像类中的,那么就意味着,静态变量也是存储在堆区的。
static int i = 1; //the value 1 is stored in the PermGen sectionstatic 
Object o = new SomeObject(); //the reference(pointer/memory address) is stored in the PermGen section, the object itself is not.

class对象 :Class clazz = Test.class;clazz对象就是一个class 对象。在JVM中,真正获取到的是InstanceMirrorKlass实例
对象 :Test obj = new Test() 或者通过反射实例化的对象;

然后我们再来理解一下方法区这个概念:

2.2、方法区

  • 永久代:1.8之前方法区的实现,存储在堆区
  • 元空间:1.8及之后方法区的实现,存储在直接内存上,即OS内存上

方法区是规范,永久代和元空间是具体的实现。

这里需要思考一下,为什么使用元空间取代了永久代?
1、GC算法,分开存储有利于GC算法的实现;
2、OOM
3、硬件的发展:之前的32bit中,内核层占了2G,应用层之占2G,所以为了限制JVM的占用内存就放到一起管理
4、字符串也会OOM

2.3、元空间

如果不做调优的话,元空间的大小是多少呢?

我们可以使用命令“java -XX:+PrintFlagsInitial”来查看:

uintx MetaspaceSize = 21810376  // 大约20.80 Mb

元空间的调优参数是:

-XX:MetaspaceSize
-XX:MaxMetaspaceSize

元空间如何调优

1、其实和堆区的调优一样,最大和最小值要设置成一样,防止内存抖动;内存忽大忽小,或者需要提前判断是否需要扩容,这些都会带来性能开销;

2、调成多少:物理内存的1/32

3、如何查看服务运行之后元空间的大小:visualVM(需要额外开端口)、arthas(阿里)


2.4、本地方法栈

Java调用C、C++的动态链接库、运行里面的函数需要的栈(JVM内存很多都是通过JNI实现的,但是现在随着socket的发展,JNI的使用越来越少)

2.5、虚拟机栈

虚拟机栈构成:
内存池与JVM内存模型_第3张图片

问题:JVM中有多少个虚拟机栈?一个线程一个!虚拟机栈是线程私有的。

问题:虚拟机栈中最小的是栈帧,那么虚拟机栈中有几个栈帧?方法调用次数个!

问题:为什么需要栈帧这个数据结构?为了处理方法的运行更清晰。

PS:方法是静态的,只有方法运行的时候才会产生栈帧!

栈帧组成

  • 局部变量表
  • 操作数栈
  • 动态链接 :main方法对应的JVM对象在元空间中的内存地址

内存池与JVM内存模型_第4张图片
方法区中的klass对象会存储方法对象集合,包括了main方法

  • 返回地址 :准确的表达叫恢复现场

JVM内部执行main方法的过程

1、创建运行main方法需要的栈帧
2、将main方法的操作数栈指针赋值给线程的属性:操作数栈
3、将main方法的局部变量表指针赋值给线程的属性:局部表指针

JVM内部执行add方法的过程:

public int add() {
        int a = 10;
        int b = 20;

        return a + b;
    }

1、首先创建add方法的栈帧;
2、在add方法的栈帧中保存main方法的字节码的下一行的程序计数器
内存池与JVM内存模型_第5张图片
我们可以看到,在main方法的程序计数器的第12行调用了add方法,那么调用完成之后应该紧接着调用下一行,也就是程序计数器的第15行。

3、线程的局部开始指针(main的)保存至add方法的栈帧
4、线程的操作数栈开始指针(main的)保存至add方法的栈帧
5、将add方法的局部表指针赋值给线程的局部表指针
6、将add方法的操作数栈指针赋值给线程的操作数栈指针

PS:每个线程都维护有两个指针:局部变量表和操作数栈当前指针。最开始的时候,有一个线程执行首先执行main方法,那么他所维护的这两个指针应当是和main方法相关的,即mian方法的局部变量表和main方法的操作数栈。但是当main方法中调用add方法的时候,这两个值会动态的执行add方法的局部变量表和add方法的操作数栈。而当add方法执行完成后,这两个指针应当又指向main方法的局部变量和操作数栈。所以,这里有恢复现场的说法。
内存池与JVM内存模型_第6张图片

2.6、程序计数器

因为当前的JVM是虚拟出来的,所以这里的程序计数器实际上是字节码的索引。即字节码执行的行号。

真正的程序计数器是EIP(32bit)、RIP(64bit),是OS级别的。
内存池与JVM内存模型_第7张图片
2.7、局部变量表

LocalVariableTable, 存放局部变量的表。每个方法都维护有一份自己的局部变量表。
内存池与JVM内存模型_第8张图片
2.8、操作数栈

push
pop

内存池与JVM内存模型_第9张图片

Test obj = new Test();

 0 new #2 <com/jihu/test/jvm/Test> (第一步)
 	1、在堆区生成了一个对象(不完全对象,未执行构造方法)InstanceOopDesc
 	2、将不完全对象的指针压入操作数栈,此时这个不完全对象指向堆区
 3 dup (dup:duplcate 第二步)
 	1、复制栈顶元素
 	2、压入栈
 		这里我们可能会有一个疑问,为什么要复制呢?
 		答:将对象指针作为this传参
 		这里有一个面试题,this指针是何时复制的?
 		答:非静态方法,第一个参数就是this指针
 4 invokespecial #3 <com/jihu/test/jvm/Test.<init>> (第三步)
 	非静态方法,第一个参数就是this指针

	1、执行invokespecial字节码指令,完成运行方法的环境构建
		this指针赋值,此时方法的第一个变量就是this指针
	2、有了this指针之后,就会执行默认的构造方法

	这一段字节码执行完成后,这个对象就是一个完全对象了。	
 7 astore_1 (第四步)
 	1、将栈顶元素pop
 	2、将完全对象的地址复制给局部变量表index=1的位置,即复制给测试代码中的demo局部变量。(即完成对象的赋值)

第一步:将不完全对象的指针压入操作数栈,此时这个不完全对象指向堆区
内存池与JVM内存模型_第10张图片
第二步:复制栈顶元素并压入栈,此时只有一个不完全对象,会复制该对象
内存池与JVM内存模型_第11张图片
第三步:赋值this指针,执行默认构造,实行之后该对象便是一个完全对象

这里首先会将复制到栈顶的那个不完全对象出栈,然后在将index=0复制为this指针。

即,每个方法的局部变量表中,index=0都是this指针。
内存池与JVM内存模型_第12张图片
第四步:将完全对象的地址赋值给局部变量表index=1的位置,即赋值给测试代码中的demo局部变量。(即完成对象的赋值)

Test demo = new Test();

此时,我们是不是对这一个简单的new对象的一行代码有了更充分的认识呢?

内存池与JVM内存模型_第13张图片

三、堆

内存池与JVM内存模型_第14张图片
最小大小:默认为物理内存的1 / 64
最大大小:默认为物理内存的1 / 4

调优参数:
-Xms
-Xmx

需要调成一样大

参考这篇文章理解更详细的GC过程:https://blog.csdn.net/wy5612087/article/details/52369677
为什么对象会进入老年代:
1、15次gc后仍然存活的对象
(4bit,0000~11111)最大15次
2、大对象

	何为大对象?大小超过Eden区的一半(源码中解释的)
	大对象的计算标准不是固定的,Eden区的大小在GC之后是会发生变化的。

3、空间担保
内存池与JVM内存模型_第15张图片

  • 在进行Minor GC之前,JVM首先会检查【老年代最大连续空闲空间】是否大于【当前新生代所有对象占用的总空间】
  • 如果是,那么说明此次的Minor GC是安全的,可以放心的进行Minor GC
  • 如果不是,则JVM会去查看HandlePromotionFailure参数的值是否为true(表示是否允许担保失败)
  • 如果不允许担保失败,则此时就会进行一次Full GC 以腾出老年代更多的空间
  • 如果允许担保失败,则此时JVM会去检查【老年代最大连续空闲空间】是否大于【历次晋升到老年代的对象的平均大小】
  • 如果小于,则JVM此时会进行一次Full GC以便于腾出更多的老年代空间
  • 如果大于,则JVM会冒险进行一次Minor GC(为什么说是冒险呢? 因为有可能Minor
    GC后新生代所有的对象都还存活,并且survivor区无法容纳下这些对象,那么这些对象就会被晋升到老年代,导致老年代空间被填满)

内存池与JVM内存模型_第16张图片

空间担保是针对Eden区的

1Eden区和2Survivor区(分别叫from和to),默认比例为81.

如果此时Eden总共是80M,第一次young GC之后还剩20M, 此时survivor区存不下20M,所以这部分对象会直接进入老年代

调优的目的是最好每一次youngGC之后,都能回收所有垃圾,竟可能的减少full GC

4、动态年龄判断

jvm动态对象年龄判定,是根据Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold要求的年龄。

四、总结

1、虚拟机栈指向方法区:动态链接

2、虚拟机栈指向堆区:对象的引用(局部变量表)

3、方法区指向堆区:引用类型的静态属性

4、堆区指向方法区:Klass pointer 对象的内存布局
Klass pointer 指向该对象的instanceKlass实例

内存池与JVM内存模型_第17张图片

你可能感兴趣的:(#,[LB-子牙],性能调优专题:JVM,java,jvm)