JVM内存模型尝试剖析

2 篇文章 0 订阅
1 篇文章 0 订阅

JDK体系结构

在这里插入图片描述

Java语言的跨平台特性

在这里插入图片描述

JVM整体结构及内存模型

Java 虚拟机分为三部分组成:类装载子系统、运行时内存区域即 Java 内存模型、字节码执行引擎。

在这里插入图片描述

线程共享

:存储 new 出来的对象
方法区(元空间、永久代):常量池、静态变量、类元信息

线程独享

(线程栈、虚拟机栈):存储每个线程的局部变量
本地方法栈:存储 Java 本地方法
程序计数器:记录线程方法运行位置

栈、又称之为虚拟机栈或者线程栈,我觉得称为线程栈可能更明了一些。
我们先看一段简单的代码:
public class JvmDemoTest {

private  void calculation(){
    int a=2;
    int b=3;
    int c=(a+b)*10;
}

public static void main(String[] args) {
    JvmDemoTest jvmDemoTest=new JvmDemoTest();
    jvmDemoTest.calculation();
    System.out.println("hello jvm");
}

}

上述代码在运行时首先会开启一个线程运行 main 方法 ,JVM 会为其在栈中分配一块内存空间用来存储数据,那么实际上 JVM 会为每一个运行的线程在栈上分配一块栈内存空间,同时在每执行到一个方法的时候会在该内存空间中给该方法分配一块栈帧内存空间 用来存储每个方法的局部变量其结构就如同我们数据结构中的栈,先入栈的栈帧后释放。栈帧内存空间中分为:局部变量表、操作数栈、动态链接、方法出口。同时每个线程都有一个独有的程序计数器来记录当前代码执行的位置,及一个存储本地方法的本地方法栈

使用 javap 命令对该类进行反汇编,可以得到一个较 Java 字节码更为可读的命令文件

javap -c -p JvmDemoTest.class > JvmDemoTest.txt
1
Compiled from “JvmDemoTest.java”
public class think_in_jvm.JvmDemoTest {
public think_in_jvm.JvmDemoTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.""😦)V
4: return

private int calculation();
Code:
0: iconst_2
1: istore_1
2: iconst_3
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: bipush 10
9: imul
10: istore_3
11: iload_3
12: ireturn

public static void main(java.lang.String[]);
Code:
0: new #2 // class think_in_jvm/JvmDemoTest
3: dup
4: invokespecial #3 // Method “”😦)V
7: astore_1
8: aload_1
9: invokespecial #4 // Method calculation:()I
12: istore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: new #6 // class java/lang/StringBuilder
19: dup
20: invokespecial #7 // Method java/lang/StringBuilder.""😦)V
23: ldc #8 // String hello jvm
25: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: iload_2
29: invokevirtual #10 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
32: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
35: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
38: return
}

操作数栈和局部变量表通过上述对 calculation 方法的执行指令解析已经很清晰了。我们所有对数的操作都会压入操作数栈最后出栈到 cpu 寄存器中执行,而局部变量表会存储方法的所有局部变量,而对于我们 new 出来的对象它时存储在堆中的,类似栈中的局部变量存储的是它在堆中的地址或者说引用,而元空间中的静态成员变量也是如此。

那么所谓的动态链接即在运行时将符号引用转变为直接引用。可以理解为直接指向代码所存地址的内存指针,静态链接是在类加载时将符号引用转变为直接引用,也就时那些静态方法类似于 main() 方法,而非静态方法就需要在运行时去动态链接了。如下代码中的 calculation() 方法就需要动态链接

public static void main(String[] args) {
JvmDemoTest jvmDemoTest=new JvmDemoTest();
int s=jvmDemoTest.calculation();
System.out.println(“hello jvm”+s);
}

而方法出口会记录这个方法最终执行完成返回主方法的位置。
做一个总结:

局部变量表:存储方法局部变量
操作数栈:存储需要进行操作的数据
动态链接:运行时将符号引用转变为直接引用
方法出口:存储记录方法出口,即方法结束后代码运行位置。

堆这边主要分三部分来说明

堆内存区域的划分
堆内存流转模型
GC ROOT 和 STW 机制
1.堆内存区域划分

堆中又划分为:Eden 区 、Survivor 区 及 老年代。
Eden 区和 Survivor 区 又称为年轻代,这里就有一个分代的概念。其中 年轻代默认占整个堆内存的 1/3 ,老年代占 2/3 。Eden 区和 Survivor 区占比为 8:2 ,而 Survivor 区又分为 S0 和 S1 这个会有一个对象流转的过程,其占比为 1:1。
年轻代的对象是属于那种朝生夕死的对象,而老年代的对象是长久存在的,同时垃圾回收时也会分为 minor gc 或者叫 young gc 其主要回收年轻代的对象以及 full gc 其会对整个堆进行垃圾回收包括年轻代和老年代的对象。

在这里插入图片描述

2.堆内存流转模型

一般来说我们 new 出来的对象是在堆中存储的,一般情况下会先存入 Eden 区,当 Eden 区满时会触发 minor gc 也叫做 young gc,这个时候会对年轻代的对象进行垃圾回收,字节码执行引擎开启的垃圾回收线程会首先到堆、栈、元空间、本地方法栈中寻找 GC ROOT 并去标记被 GC ROOT 引用的所有对象,剩余的对象即为垃圾对象,垃圾对象直接清除掉,存活下来的对象会移动到 survivor 区,那么已经在 survivor 区的对象仍然存活,他会增加分代年龄,并在 s0 区和 s1 区流转,当分代年龄达到 15 时这个对象会被放到老年代进行较为长久的保存。当老年代空间存满时会触发 full gc 对堆内存及元空间进行全面的垃圾回收,这个过程时较为耗时的会进行相对 minor gc 更长的 STW 。当 full gc 后老年代。

在这里插入图片描述
使用 Java 自带的 jvisualvm 工具可以较为值观的看到整个堆内存的流转过程。 OOM 示例代码如下

package think_in_jvm;

import java.util.ArrayList;
import java.util.List;

//堆内存流转测试
public class JvmHeapCirculation {

private static Integer count=0;

private byte[][] a=new byte[10][1024];



public static void main(String[] args) throws InterruptedException {
    List<JvmHeapCirculation> list=new ArrayList<>(16);
    while (true){
        list.add(new JvmHeapCirculation());
        Thread.sleep(10);
    }
}

}

运行时设置:-Xms150M -Xmx150M 设置堆空间大小为 150 M
示例代码中的:List list=new ArrayList<>(16);
就是一个 GC ROOT 在循环中增加的对象会一直被引用所以不会被回收掉,直至 OOM。

这里简单说一下 STW ,Stop The World,是指在垃圾回收的过程中,gc 会停止用户线程的运行,简单的理解就是所有非垃圾回收的线程都会被挂起停止运行,对于 Web 应用来说比较直观的感受就是系统突然卡了一下。

二、JVM内存参数设置

在这里插入图片描述
Spring Boot程序的JVM参数设置格式(Tomcat启动直接加在bin目录下catalina.sh文件里):
1 java ‐Xms2048M ‐Xmx2048M ‐Xmn1024M ‐Xss512K ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐jar microservice‐eurek
a‐server.jar
关于元空间的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。

JVM内存参数大小该如何设置?

JVM参数大小设置并没有固定标准,需要根据实际项目情况分析,给大家举个例子
日均百万级订单交易系统如何设置JVM参数

在这里插入图片描述
在这里插入图片描述

结论:通过上面这些内容介绍,大家应该对JVM优化有些概念了,就是尽可能让对象都在新生代里分配和回收,尽量别
让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免新生代频繁的进行垃
圾回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值