系列文章目录
1:JVM核心知识
1.3:内存模型
目录
前言
本系列主要针对想进一步进阶Java的开发者,本文介绍Java的内存模型。
一、线程栈与堆内存
在正式介绍内存模型之前,我们要先正确认识线程栈与堆内存。
线程栈的特点有:
1.每个线程都只能访问自己的线程栈。
2.每个线程都不能访问(看不见)其他线程的局部变 量。
3.所有原生类型的局部变量都存储在线程栈中,因此 对其他线程是不可见的
4.线程可以将一个原生变量值的副本传给另一个线程, 但不能共享原生局部变量本身
基于线程栈的特点,我们可以提出一下问题:
Q:要是想在线程之间共享变量怎么办?
一个线程可以将原生变量的副本传递给另一个线程,但是不能共享局部变量本身,如果想在不同线程之间共享局部变量,就需要我们下面的堆内存。
堆内存的特点有:
1.堆内存中包含了 Java 代码中创建的所有对象,不 管是哪个线程创建的。 其中也涵盖了包装类型 (例如 Byte,Integer,Long 等)。
2.不管是创建一个对象并将其赋值给局部变量, 还 是赋值给另一个对象的成员变量, 创建的对象都 会被保存到堆内存中。
总结说来:
线程栈保留
1.原生数据类型的局部变量
2.类对象的引用(真正的对象保存在堆内存之中)
堆内存保留
1.对象本身及对象的成员变量,而不管成员变量的类型是原生的还是对象的引用
2.类的静态变量和类定义(下面会详细介绍这一点)
只要线程能拿到堆中的对象地址,那么这个线程就能访问变量,也能访问对象的成员变量。
这里埋藏着一个小伏笔,如果两个线程同时调用某对象的同一方法,就会有并发相关的问题,关于并发的问题,这个系列后面也会介绍。
二、JVM 内存整体结构
JVM整体内存结构如上图:Java进程即所谓的Java虚拟机。
我们一部分一部分介绍
1.线程栈:
每启动一个线程,JVM 就会在栈空间栈分 配对应的 线程栈。
线程执行过程中,一般会有多个方法组成调 用栈(Stack Trace), 比如 A 调用 B,B 调用 C…每执行到一个方法,就会创建对应 的 栈帧(Frame)。
栈帧的作用和线程栈的作用类似,都起到了隔离变量的作用
栈帧用来保证方法内部局部变量隔离
线程栈用来保证线程之间变量隔离
Q:那么栈帧里面包含什么呢?
栈帧是一个逻辑上的概念,具体的大小在 一个方法编写完成后基本上就能确定。
我们可以回忆一下在编程语言中的函数,是不是要包含一个返回值,还要有局部变量,然后还有我们的具体逻辑。
返回值对应栈帧里的返回值,局部变量对应原生变量或者是对象引用,也就对应栈帧里面的局部变量表与class/method指针,由于JVM是基于栈的虚拟机,所以具体逻辑是由栈来完成的,即对应操作数栈。
2.堆内存结构
堆内存里面包含堆和非堆,非堆只是名字看来不是堆,实际上与堆类似
堆内存是所有线程共用的内存空间,JVM 将 Heap 内存分为年轻代(Young generation)和 老年代(Old generation, 也叫 Tenured)两部分。
年轻代还划分为 3 个内存池,新生代(Eden space)和存活区(Survivor space), 在大部分 GC 算法中有 2 个存活区(S0, S1),在我们可 以观察到的任何时刻,S0 和 S1 总有一个是空的, 但一般较小,也不浪费多少空间。关于GC算法的详细介绍会在之后的章节出现。
Non-Heap 本质上还是 Heap,只是一般不归 GC 管理,里面划分为 3 个内存池。
Metaspace, 以前叫持久代(永久代, Permanent generation), Java8 换了个名字叫 Metaspace,里面包含有常量池和方法区(定义类的一些描述数据还有JVM的描述数据)。
CCS, Compressed Class Space, 存放 class 信 息的,和 Metaspace 有交叉。
Code Cache存放 JIT 编译器编译后的本地机器代 码,即动态即时编译的代码缓存。
Q:观察堆内存的结构,我们肯定会有疑惑,为什么我们要对堆内存进行分代?
因为GC是基于分代假设的。
多说一句,其实这里把内存划分细致一点是我们GC算法优化的整体思路(后面会介绍GC算法的演变)
Q:堆内存和非堆内存有什么区别呢?
堆内存和非堆内存最大的区别就是非堆内存里面存放的不是我们代码创建的对象,但是里面有我们自己定义的类。
Q:CCS里面会存放压缩对象指针,为什么这样做?
很明显,JVM在运行过程中会产生大量对象,如果我们压缩指针,能节省巨大的内存。
总结
以上就是今天要讲的内容,本文大致介绍了Java的内存模型。下面我们将学习JVM的启动参数,敬请期待。