写点什么

最简单的 JVM 内存结构图

用户头像
叫练
关注
发布于: 2021 年 03 月 11 日
最简单的JVM内存结构图

JVM 内存结构图



image.png


大家好,好几天没有更新了,今天的内容有点多,我们详细介绍下 JVM 内部结构图,还是和之前一样,案例先行,方便大家理解记忆。

/** * @author :jiaolian * @date :Created in 2021-03-10 21:28 * @description:helloworld测试jvm内存区域 * @modified By: * 公众号:叫练 */public class HelloWorldTest {
public static void main(String[] args) { //新建HelloWorldTest对象; HelloWorldTest helloWorldTest = new HelloWorldTest(); //新建2个线程调用sayHello for (int i=0; i<2; i++) { new Thread(()->helloWorldTest.sayHello("world")).start(); } }
/** * 对某人说hello * @param who */ public void sayHello(String who) { System.out.println(Thread.currentThread().getName()+"hello!"+who); }}
复制代码

如上代码:在主线程中 for 循环新建 2 个线程调用 sayHello,最后两个线程分别对世界问好!这段代码比较好理解,就不贴输出结果了。我们编写并运行了这段代码,我们主要看看这段代码在 JVM 中是怎么运作的。

首先,我们编写一个 HelloWorldTest.Java 文件,经过 javac 编译会转化成字节码 HelloWorldTest.class,为什么要转化成字节码呢?因为 Java 虚拟机能识别!最后由类加载子系统 ClassLoader 将字节码装载到内存。每块内存各有自己的作用,最后由执行引擎来执行字节码。下面我们重点介绍下各块内存发挥的作用!


image.png


方法区


方法区主要装一些静态信息,比如:类元数据,常量池,方法信息,类变量等。如上代码 HelloWorldTest.class 是类元数据,sayHello,main 都是方法信息等都是放在方法区存储的。方法区中还需要注意两点:

  1. 如果方法区太大,超过设置,会报 OutOfMemoryError:PermGen space 错误。gclib 工具可以动态生成类测试该错误。

  2. 在 JDK1.7 以前,方法区叫永久代,而 1.8 之后叫元空间。原因是 JDK1.8 为了释放管理压力,把运行时常量池交给堆去管理。



堆中主要存放实例对象。你可以这么理解,只要看到用关键字 new 的对象,数据都放在堆中。如上代码 HelloWorldTest helloWorldTest = new HelloWorldTest();helloWorldTest 是 HelloWorldTest 对象的引用,指向 new 出来的 HelloWorldTest 对象实例,helloWorldTest 引用是放在栈中的,也叫局部变量方法内申明的对象类型或普通类型),我们简单画图来表示下堆,栈,方法区关系。当 JVM 执行了 HelloWorldTest helloWorldTest = new HelloWorldTest();这句话,JVM 内存结构看起来是这样的。如果指向对象引用消失,对象会被 GC 回收。


image.png


在堆内存中,内存需要划分成两块区域,新生代老年代。如下图所示。

  1. 新生代:在堆内存中,新生代又分为三块,eden(伊甸园创建新生命,对应 new 对象),from,to,这三块内存区域都属于新生代,默认比例是 8:1:1,每次 new 对象都会先存储到 eden 中,如果 eden 区域内存满了,会触发 monitor gc 回收该区域,还未回收的对象会放入 from 或者 to,from,to 内存其中一块是空的,方便对象在内存中整理标记,每 GC 一次,from,to 两块空间对象每移动一次,还未回收的对象年纪也会增加 1,到达一定年纪(默认是 15 岁),就会进入老年代了。

  2. 老年代:当老年代满了,会触发 Full GC 回收,如果系统太大,Full GC 都回收不了,程序会出现类似 java.lang.OutOfMemoryError: Java heap space,我们可以通过配置 JVM 参数:如-Xmx32m 设置最大堆内存为 32M。

对堆分块原因是方便 JVM 自动处理垃圾回收堆内存是 GC 回收的主要区域


image.png



栈内存空间相对于堆空间比较小,也属于线程私有,栈中主要是一堆栈帧,是先进后出的,理解起来栈帧对应就是一个方法,方法中包含局部变量,方法参数,还有方法出口,访问常量指针,和异常信息表,其中异常信息表和常量指针信息我们在方法体中可能看不出来,但通过工具 Jclasslib 工具类在反编译 class 文件可以体现出来,异常信息表可以处理当程序执行报错,会跳转到具体哪行代码执行,JVM 中就是通过异常表反馈的。我们还是结合例子和图来详细分析下。当程序运行时,JVM 中栈可能如下图呈现状态。


image.png

    

一个线程可能对应多个栈帧,栈帧都是从上往下压入,先进后出,如下图所示,在方法 A 中调用方法 B,在方法 B 中调用 C,在方法 C 中调用方法 D,主线程对应栈帧的压栈情况,出栈顺序是 D->C->B->A,最终程序结束。另外还需注意:操作数栈的意思是存储局部变量计算的中间结果,比如在方法 A 中定义 int x = 1;在 JVM 中会将局部变量入操作数栈用来之后的计算。栈也是有空间大小的,如果栈太大,超过栈深度,会类似报错,java.lang.OutOfMemoryError: Java stack space,最常见的例子就是递归了。你会写 demo 测试递归例子吗?


image.png


程序计数器


程序计数器也是线程独享的,多线程执行程序依赖于 CPU 分配时间片执行,画个简单的图,看看多线程怎么利用 CPU 时间片的。如下图,线程 0 和线程 1 分配 cpu 时间片交替执行程序,假设此时线程 0 先获取到了时间片,时间片用完后 CPU 会将时间片再分配给线程 1,线程 1 执行完毕后,此时,时间片又回到线程 0 来执行,那么问题来了,线程 0 上次执行到哪儿了呢?具体是代码的多少行了呢,该行代码有没有执行完毕?此时程序计数器就发挥作用了,程序计数器保存了线程的执行现场,方便下次恢复运行。这也是为什么程序计数器是线程独享的原因。


image.png


本地方法栈


本地方法栈就不过多介绍了,和栈结构一样,是一块独立的区域,只是对应的是 native 方法。


直接内存


直接内存独立于 JVM 内存之外的内存,可以直接和 NIO 接口交互,NIO 接口会频繁操作内存,如果放在 JVM 管理,无疑会增加 JVM 开销,所以单独将这块提出来,而且直接内存操作数据相比较 JVM 更快,显而易见提升了程序性能。


内存分配性能优化-逃逸分析


我们之前说过,只要是看到关键字 new,对象分配肯定在堆上,下面我们来看一个案例。

/** * @author :jiaolian * @date :Created in 2021-03-10 16:10 * @description:逃逸分析测试 * @modified By: * 公众号:叫练 */public class EscapeTest {
//private static Object object; public static void alloc() { //一个对象相当于16k大小,非逃逸对象 //object = new Object(); Object object = new Object(); }
public static void main(String[] args) throws InterruptedException { //亿次内存 long begin=System.currentTimeMillis(); for (int i=0; i<10000000; i++) { alloc(); } long end=System.currentTimeMillis(); System.out.println("time:"+(end-begin)); }}
复制代码

如上代码,我们在主函数里面通过 for 循环 1 亿次来 new Object,一个 object 为 16k,大致估算下有 GB 数据了,此时我们手动配置 JVM 参数,-XX:+PrintGC -Xmx10M -XX:+DoEscapeAnalysis;设置打印 GC 信息,默认最大的堆内存是 10M。

  1. -XX:+PrintGC。表示控制台打印 GC 信息。

  2. -Xmx10M。设置最大的堆内存为 10M。

  3. -XX:+DoEscapeAnalysis。 开启逃逸分析(默认开启)。

执行程序,打印结果如下图所示。一共进行了 3 次 GC,你可能有疑问?10M 堆内存需要容纳 GB 数据冲击,怎么也需要 N 次 GC,为什么只有 3 次 GC?如果设置-XX:-DoEscapeAnalysis 关闭逃逸分析,GC 可能会出现上千次。运行时间也从 3 毫秒增至 1000 毫秒以上。说明了非逃逸对象没有新建的堆上,而是建在栈上了。这样做的好处:从程序 GC 执行次数和执行时间上来看,程序运行效率提高了。


image.png


  • 原因分析:

观察我们上述案例代码中 alloc()方法,方法中 Object object = new Object();object 是一个局部变量,每次新建后到下一次循环再新建,上一次新建的对象就会出栈,object 引用指向的对象就会失效,失效的对象就会被 GC 回收了。开启逃逸分析后,new Object()创建的对象就不在堆上分配空间了,而放到了栈上。这就是 JVM 通过逃逸分析对内存的优化。思考下,如果将 private static Object object;注释放开,object 还会是非逃逸对象吗?

注意:逃逸对象不能在栈上分配空间!

相信到这里你已经对逃逸分析应该有一个比较清晰的认识了。


总结


好了,写的有点累了,写的不全同时还有许多需要修正的地方,希望亲们加以指正和点评,喜欢的请点赞加关注哦。点关注,不迷路,我是叫练公众号,微信号【jiaolian123abc】边叫边练。


image.png


发布于: 2021 年 03 月 11 日阅读数: 11
用户头像

叫练

关注

我是叫练,边叫边练 2020.06.11 加入

Java高级工程师,熟悉多线程,JVM

评论

发布
暂无评论
最简单的JVM内存结构图