学习笔记 2021.10.22
2021.10.22
JVM
常量池具体存在的位置在哪里?
是怎么通过常量池来减少内存消耗的?
字符引用在内存上又怎么体现?
运行时数据区
堆
YGC OGC FGC的一些简单理解
FGC是包括方法区在内的区域

上面注意到老年回收单独收集是一个很少见的行为,知道有这么个东西即可。

这里简单贴一张各个区转移的示意图

注意点:YGC触发的条件是伊甸区满的时候触发,而s区满的时候不会触发垃圾回收。这也是触发频率最高的垃圾回收。
至于FGC会在后面垃圾回收时具体再讲。

关于堆分代的理解

其实不分代也是完全可以的,但是分代了是对具体对象具体处理的策略,更多的是为了优化垃圾回收的性能。
内存分配策略(对象提升原则)

- 长期存活的对象可以理解为即age超过阈值的对象。
TLAB
堆空间中不一定都是共享的!

因此引入了TLAB这个概念:

- JVM确实是把TLAB设置为首选。
- 一般TLAB所占空间为伊甸区的百分之一
- 在TLAB分配失败的话,JVM会通过加锁机制让对象在伊甸区中分配内存。
基本的使用过程:

当然上图只是一个简图,也有直接到老年区的情况。
堆空间的常用参数设置

- 伊甸区相对s区大的话,会导致FGC的存在意义就没有了。
- 相对的当比例小的话,则会导致垃圾回收的频率变大了。
- 空间分配担保相关:

简单理解即是看情况要不要使用FGC。
一些小拓展:

目的就是为了看能否分配对象时不在堆上面进行。在栈上进行的话,就只有入栈出栈的操作,而不存在GC之类的,就优化了很多。
具体是否发生逃逸的判断:

如何快速的判断是否发生了逃逸,即看new的对象实体是否在外部被使用了。

像第二种本来就是使用外面的而不是自己的,自然也是发生了逃逸。

结论:能使用局部变量的,就不要在方法外定义了。
代码优化的小tips
栈上分配

同步省略

具体的例子示意:

标量替换:

此时看到由创建对象直接变成了基本数据类型的使用,就可以直接分配在栈中的局部变量表里面了。
总结

方法区
栈、堆、方法区的交互关系
局部变量表中会存储具体数值么?

简单理解就是,在虚拟机栈的局部变量表中,存储着具体对象的名称,如果不是基本数据类型,该栏的值是指向堆中实例化后的对象的引用。到了堆中,则是一个个具体的对象,然后至于对象中的具体信息,则又指向了方法区中的内容。方法区中就是.class文件本身。
突然有感:
具体交互关系以及我想这么实现的意义。
首先第一点是类定义好后,被加载后即会被放到方法区中,包含具体的函数以及一些变量?
类型声明的过程,只是在栈空间给了块slot放他的引用,在没有具体创建对象之前,这块引用是没有指向的位置的。所以此时对于栈中的其他基本数据类型,他们放的就是实际值,而栈只会放堆中对象的地址(创建好过后)。
然后才是对象创建,即new。这时引用会指向堆中的具体对象,而对象自己也会指向方法区中的类获取相关的其他东西,我认为在对象创建这,也有对象自己的数据放在了这里。
这么设计对象创建的过程,首先在局部变量表层面,不至于占用太多空间,因为这里是操作很频繁的区域。在堆层面,不必存储太多信息,因为基本都有一个模板,所以又才会指向方法区中的模板。
剩下的问题:
具体对象中的属性,是在方法区中还是堆中?public static这些定义体现在JVM中是怎么个情况?
方法区的理解

并且实际上设置堆的大小的时候也不会影响到方法区的大小。

即前面有提到的metaspace(元空间)就是指代的方法区。在hotspot的发展过程中,经历了从永久代和元空间的变化。

对这么一个演变过程有个印象就可以了。
方法区大小的设置
这里只记录jdk8之后的吧:

初步了解解决OOM的思路和方法:

这里具体在后面调优的学习中再去解决。
方法区的内部结构
大概示意图:

方法区中加载的信息包括:类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息:

域信息(也属于类型信息):

方法信息(也属于类型信息):

可以看到,这里对方法的记录也包括其操作数栈和局部变量表之类的。
这里也要注意,class文件被加载放入方法区后,对应的类加载器的信息也放在了方法区里面。
一些关键字的相关注意:

这里即是指用static修饰的变量。可以不用实例化对象就可以直接使用。

即可以看到final修饰的,在编译阶段就已经赋值了。
而只用static修饰的,则会现在前面所说的preapare先初始化,再在后面的阶段再赋值。
运行时常量池

常量池表:包含各种字面量和对类型、域和方法的符号引用。
常量池存在的意义:

这时,为了避免加载那么多的信息,不去直接加载这些类,而是存储他们的符号,在具体编译执行的时候实现引用从而达到目的。

运行时常量池:

方法区的具体执行过程:

这里就贴一张图,说下理解。
- 首先字节码指令中有很多去获取常量池中类的信息的字符引用。这里就要注意的是在编译执行的过过程中,会把字符引用转换为真正的地址索引,去看该类是否存在或已经被加载之类的。
- 调用方法即是在栈中创建新栈帧的过程,此时对应的符号引用也是指向的方法,然后再去找方法的具体位置从而执行。
方法区的演进细节:

特别注意就是下面JDK8中的元空间是属于本地内存的。

永久代被元空间替换的原因分析:

为什么对字符串常量池进行位置得到调整:

反正就是用的多,希望垃圾回收更频繁一点。
静态变量位置相关

这里反而想说一点,即是类的成员变量,就可以理解成前面有提到过的类的自己的信息,即成员变量是引用变量,在堆中也是以引用指向来存储的。这个就是直接存在于堆里面的,而方法中的局部变量,则自然就是在栈空间中的。
至于后面的static定义的变量,就硬记为在方法区中即可。
方法区的垃圾回收

常量的判断相对还好理解,主要是判断某个类是否需要回收:

总结
运行时数据区就是内存!! 所有涉及到内存的都以运行时数据区为指代来考虑。
对象实例化的布局
将对象创建的过程从内存层面上的分析,不论面试,至少也有助于对程序的理解。
常见的创建对象的方式:

对象创建的步骤
从字节码的角度看待:

- new这一步即是创建对象的过程,包括查看class是否被加载,开辟内存空间等等。
- invoke这部则是调用了构造器方法来初始化,上面是零值初始化,这里是显示初始化。
从执行步骤上来分析

- 第一步判断的具体方法即在于去看元空间的常量池中是否有该类的符号引用。有就好,没有的话则调用对应的类加载器去加载,没有的话就抛出异常
- 指针碰撞,其实就是按着指针的顺序放置对象即可。
- 不规整时维护的列表其实就可以理解为链表形式,在逻辑上有序,物理上无序那种。
- 而内存是否规整则由垃圾收集器去决定。
- 第四部即是属性的默认初始化,下附几个初始化的顺序。

- 对象头包括一些必要信息,后面再说。
- 最后一步即为显示初始化及其后面的,反正就是涉及到自己操作进行的初始化。上面的2 3 4都会在init方法中进行实现。
- 普遍还是认为init过后才算对象创建完成。
对象的内存布局

- 哈希值用来从栈到堆、GC分代年龄即前面所说的age,其他的有了解到再说。
- 类型指针指向了方法区中加载的class。
- 实例数据即是创建具体对象时自己定义的数据,注意如果对象中也有对象,同样是指向方法区中的类。
小结的一个图:

把这三部分一起来看再结合前面所说,也就很好理解了。
对象访问定位

两种主要方式的图示:
句柄池

直接指针

直接指针就偏向于常理解的形式。也就是hotspot采用的方式。
直接内存

有点相关NIO的常识

常见的读取内存的数据的过程

而直接内存读取的情况

即这就是对上面最后一点的阐述。
关于直接内存的其他一些注意点:

- 元空间也只是属于直接内存的一部分。

考虑到堆空间站运行时数据区的大头,因此这里是个大约的计算。
浙公网安备 33010602011771号