1.Java内存区域划分
- 常量
用final修饰的成员变量表示常量,静态变量、实例变量和局部变量 - Class文件中的常量池
即该文件内部描述的一些常量。主要存放字面量和符号引用常量。字面量类似Java中的常量,符号引用包括
1. 接口的全限定名
2. 字段名称和描述符
3. 方法名称和描述符 - 运行时常量池
存放编译期生成的各种字面量和符号引用。
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。简单来说,使用常量池主要带来了如下两个好处:
(1)节省内存空间: 常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间: 比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
- JDK8为什么用元空间替换永久代
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。 - JDK7为什么把字符串常量池从方法区放到堆
对于字符串常量这种创建完成用几次就不被使用的对象,是很容易被回收的。 而要进行频繁垃圾回收的地方是堆空间, 这样在JDK7就把字符串常量池移动到堆空间中就是很明智和有必要的选择了。 这样就避免了放到不频繁进行垃圾回收的元空间中应该被垃圾回收的对象而不能及时进行垃圾回收的浪费空间的现象出现。
2.栈帧
存放局部变量表、操作数栈、方法返回地址、常量池的引用
3.三种垃圾回收算法及优缺点
- 标记清除
标记阶段将不是垃圾的对象进行标记,清除阶段会清除标志位,并将可以回收的对象加入空闲链表(如果前面也是垃圾对象则会合并),下次分配对象时根据空闲链表找一块内存大于等于所需要的内存空间的内存,并把多余的内存返回给空闲链表
缺点:
标记和清楚的效率都不高,会产生内存碎片 - 复制算法
缺点:耗费内存
HotSpot的8:1:1 保证内存利用率达到90%,新生代由于存活的对象小,拷贝的对象也少,因此常用在新生代。 - 标记整理
缺点:
需要大量移动对象,效率低,老年代如果使用复制算法由于存活对象多,不仅拷贝效率低,而且一半浪费空间
4.垃圾回收器
- Serial(新生代)
单线程回收 Stop The World - ParNew
多线程的新生代版本 - Parallel Scavenge
和ParNew一样,但是为了尽可能缩短垃圾收集时用户的停顿时间,吞吐量优先(CPU用于用户程序的时间占比更多) - Serial Old(老年代)
老年代的单线程版本 - Parallel Old
老年代的多线程版本 - CMS(老年代)
- 初始标记:会标记GCRoots「直接关联」的对象以及「年轻代」指向「老年代」的对象
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 并发预处理
可能有些对象,从新生代晋升到了老年代。可能有些对象,直接分配到了老年代(大对象)。可能老年代或者新生代的对象引用发生了变化。
老年代的变化记录在卡表中,要找出Dirty Card中引用关系发生变化的老年代对象,遍历新生代(新生代可能发生minor gc使得新生代对象减少)中对老年代的引用关系发生变化的老年代对象. - 重新标记:对上个阶段找出的老年代的引用重新标记
- 并发清除:不需要停顿。这个过程产生的新的垃圾叫浮动垃圾。
缺点
由于和用户进程同时执行,因此要给用户内存留空间,如果预留的内存空间不足会采用serial old进行垃圾回收,增加卡顿时间。由于采用标记清除,因此会造成内存碎片,造成空间利用率低,
- G1
Region:
当分配的对象大于等于Region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝。通过如果发现堆内存容不下H对象的时候,会触发一次GC操作,如果没有引用指向该对象,那么进行一次minor gc会回收该对象。
Remembered Set
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
gc过程:
minor gc
Eden区满时,会触发minor gc,会stop the world
过程:
根扫描: 标记gc roots 直接可达的对象
处理Rset: Rset中存储了其他Region中引用当前Region的关系,使用Collection Set记录一次垃圾回收需要回收的Region,CSet中存活的对象会移动到其他区域。
复制对象: 将存活的对象放到Survivor区或老年代
处理引用:会处理下软引用、弱引用、JNI Weak等引用,结束收集
mixed gc
堆空间的占用率达到一定阈值后会触发Mixed GC(默认45%,由参数决定) 一定会回收年轻代
初始标记(STW):老年代和新生代都会扫
并发标记
重新标记(STW):
SATB算法,gc开始时记录所有对象的引用关系,把每次引用发生变化的引用值记录下来,这个阶段看发生引用关系变化的引用有没有对象存活,如果有设为gc roots 进行标记
清理(STW):回收所有的年轻代Region和垃圾多的老年代Region
浮动垃圾
快照时是存活的,之后不存活,因此产生浮动垃圾
G1会什么时候发生full GC?
如果在Mixed GC中无法跟上用户线程分配内存的速度,导致老年代填满无法继续进行Mixed GC,就又会降级到serial old GC来收集整个GC heap
特点:
Region 内部是复制算法,间是标记整理
可预测的停顿时间,可以让用户指定在一个固定时间段M毫秒内,gc停顿的时间不超过N毫秒
5.类加载
类加载之前的步骤:
- 使用Class.forName() 或 ClassLodar.loadClass进行类加载
- 判断内存中有没有对应的类对象,没有才选择加载
- 加载时请求父类加载器,再递归请求父类加载器,父类可以则加载,否则交给子类类加载器。
4.只要一个类加载器负责加载一个类,它就负责该类以及该类中使用到的其他未经加载的类的加载过程,加载即将.class文件加载到内存, - 加载
通过类的全限制名得到.class文件,加载进内存
在方法区生成instanceKClass对象
在堆中生成Class对象 - 链接
- 验证
校验加载.class字节流是否正确,确保不会损害虚拟机的安全。 - 准备
静态变量分配内存和赋默认值(final修饰的静态变量在编译是分配内存,此阶段会显式初始化) - 解析
常量池中的符号引用转换为直接引用。
- 初始化
执行静态代码块,为静态变量赋初始值。
6.双亲委派,Tomcat如何打破
Tomcat对每一个war包生成一个类加载器,专门用来加载该war包。最终原因还是重写了ClassLodar.loadClass()
7.HotSpot对象的创建以及定位
对象创建过程:
- 类加载检查
在常量池中能否找到该类对应的符号引用,如果没有要类加载 - 分配内存
指针碰撞:
适用于没有内存碎片,用过的内存放到宜宾啊,没用过的放到一边,中间一个指针,分配内存后挪动该指针。如Serial ParNew
空闲链表:
有内存碎片,见标记清除算法。 - 初始化零值
不包括对象头 - 设置对象头
普通对象
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
2.对象头的组成
2.1.Mark Word
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word
的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word
为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
其中各部分的含义如下:
- 执行init方法
8.minorgc fullgc
Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
9.什么情况会触发full gc
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
- 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。 - 老年代空间不足
10.gc roots
虚拟机栈中局部变量表中引用的对象
本地方法栈中 JNI 中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
被同步锁持有的对象
11.为什么需要自定义类加载器
- 隔离加载类,不同的war包里可能存在冲突的类
- 扩展加载源,比如从数据库中加载
- 防止源码泄漏,由于某些字节码文件可能会被反编译,因此以加密方式存储,自定义类加载器以实现解密。
12.栈帧里有什么
- 局部变量表(方法的参数 里面定义的变量)
- 操作数栈(一个方法执行过程中的字节码指令,他往操作数栈中进行入栈和出栈)
- 方法返回地址