JVM整体结构及内存模型
JVM整体结构及内存模型


<font style="color:rgb(44, 62, 80);">方法区</font>
<font style="color:rgb(44, 62, 80);">堆</font>
<font style="color:rgb(44, 62, 80);">虚拟机栈</font>
<font style="color:rgb(44, 62, 80);">本地方法栈</font>
<font style="color:rgb(44, 62, 80);">程序计数器</font>
package com.junziln.study.maintest.jvm;
public class Math {
public static final int initData = 666;
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
}
(线程栈)
[](https://javabetter.cn/jvm/stack-frame.html)栈帧内存空间:
- 局部变量表:存放局部变量
- 操作数栈:存放需要操作的临时内存区域
- 动态链接 是在程序运行期间完成的将符号引用替换为直接引用,
- 方法出口 该方法执行完 返回到调用方法的时候的那个位置

main方法的栈帧的局部变量表
math放的是堆中math对象的地址

程序计数器
也是每个线程独有的:马上要运行的代码的那一行位置,(再内存里面的)
本地方法栈
[](https://javabetter.cn/oo/native-method.html)方法区
静态变量user 指向堆中的对象new user()
堆
new出来的对象绝大多数是放到eden区

GC Roots是根可达算法的基本,而在JVM运行过程中,可以被作为GC Roots的对象有如下四大类:
在minor gc过程中对象挪动后,引用如何修改?
对象在堆内部挪动的过程其实是复制,原有区域对象还在,一般不直接清理,JVM内部清理过程只是将对象分配指针移动到区域的头位置即可,比如扫描s0区域,扫到gcroot引用的非垃圾对象是将这些对象复制到s1或老年代,最后扫描完了将s0区域的对象分配指针移动到区域的起始位置即可,s0区域之前对象并不直接清理,当有新对象分配了,原有区域里的对象也就被清除了。
minor gc在根扫描过程中会记录所有被扫描到的对象引用(在年轻代这些引用很少,因为大部分都是垃圾对象不会扫描到),如果引用的对象被复制到新地址了,最后会一并更新引用指向新地址。
这里面内部算法比较复杂,感兴趣可以参考R大的这篇文章:
https://www.zhihu.com/question/42181722/answer/145085437
https://hllvm-group.iteye.com/group/topic/39376#post-257329
STW(stop the world)
GC算法一般在堆可用内存不足的情况下会被触发,通常来说,它们首先会先停止应用程序(也被称为
STW:StopTheWorld),也就是指将JVM中的所有用户线程暂停,这样可以保持堆内存在该时间段内不会发生新的变化,能够在最大程度上保证结果的准确性。
GC发生时为什么都必须要STW呢?这主要有两个原因,一个是尽量为了避免浮动垃圾产生,第二个则是为了确保一致性。
避免产生浮动垃圾
如果在GC发生时不停下用户线程,那么会导致这么一种情况出现,就是刚刚标记完成一块区域中的对象,但转眼用户线程又在该区域中产生了新的“垃圾”。举个例子:
好比你生日聚会,聚会搞得屋子很乱,所以有人需要过来打扫卫生,但保洁人员刚扫完这边,那边又被搞乱了,又跑过去扫完那边之后,这边又被搞乱了,最终导致房间永远无法打扫干净。为了避免在打扫过程中产生“浮动垃圾”,就只能选择让所有聚会人员全部停止活动,这样才能确保最终屋子能够打扫干净。
同时,如果在GC发生时不做全局停顿,带来的后果则是:会给GC线程造成很大的负担,GC算法的实现难度也会增加,因为GC机制很难去准确判断哪些是垃圾。
确保内存一致性
在GC发生时,可达性分析算法的工作必须要在一个能够确保一致性的内存快照中进行。也就是指:在整个分析期间,JVM看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性无法得到保证。这也是GC进行时,必须停止所有用户线程的其中一个重要原因。
STW会带来什么问题?
不过STW也会带来一些问题,比如导致客户端长时间无响应、HA(高可用)系统中的主从切换脑裂、上游系统宕机等。
JVM内存参数设置

java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
java -Xms2048M -Xmx2048M -Xmn1024M -Xss512K -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -jar microservice-eureka-server.jar
-Xss:每个线程的栈大小
-Xms:设置堆的初始可用大小,默认物理内存的1/64
-Xmx:设置堆的最大可用大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewRatio:默认2表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。

关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N
-XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。
-XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的**-XX:PermSize**参数意思不一样,-XX:PermSize代表永久代的初始容量。
由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。
对象创建与内存分配机制:

当我们使用 new 关键字创建一个对象的时候,JVM 首先会检查 new 指令的参数是否能在常量池中定位到一个类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,就先执行相应的类加载过程。
如果已经加载,JVM 会为新生对象分配内存,内存分配完成之后,JVM 将分配到的内存空间初始化为零值(成员变量,数值类型是 0,布尔类型是 false,对象类型是 null),接下来设置对象头,对象头里包含了对象是哪个类的实例、对象的哈希码、对象的 GC 分代年龄等信息。
最后,JVM 会执行构造方法(<init>
),将成员变量赋值为预期的值,这样一个对象就创建完成了。
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
2.在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录

解决并发问题的方法:
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存。通过**-XX:+/-参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+**),-XX:TLABSize 指定TLAB大小。
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
5.执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同,这是由程序员赋的值),和执行构造方法。
什么是java对象的指针压缩?
压缩了 是 4个字节,没有压缩 8个字节
对象内存分配

********
public User test1() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
return user;
}
public void test2() {
User user = new User();
user.setId(1);
user.setName("zhuge");
//TODO 保存到数据库
}
********
/**
* 栈上分配,标量替换
* 代码调用了1亿次alloc(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
*
* 使用如下参数不会发生GC
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* 使用如下参数都会发生大量GC
* -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class AllotOnStack {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
private static void alloc() {
User user = new User();
user.setId(1);
user.setName("zhuge");
}
}
Minor GC和Full GC 有什么不同呢?
Eden与Survivor区默认8:1:1
为什么要这样呢?
****
************
对象内存回收
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
public class ReferenceCountingGc {
Object instance = null;
public static void main(String[] args) {
ReferenceCountingGc objA = new ReferenceCountingGc();
ReferenceCountingGc objB = new ReferenceCountingGc();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
}
}
**“GC Roots”******
GC Roots

Class常量池与运行时常量池
********可以通过javap命令生成更可读的JVM字节码指令文件:
javap -v Math.class

字面量
符号引用