深入理解JVM:内存区域

一、运行时数据区域

Java虚拟机在运行时java程序的时候,会把它所管理的内存划分成若干个不同的数据区域。其中jdk1.8前后版本有差别。

jdk1.8之前:                                                                        jdk1.8之后:

内存可分为:程序计数器、栈、堆、方法区和直接内存。

整个内存数据区域是属于当前进程的,当前进程拥有所有的资源和数据。而直接内存是所有进程共享的。

其中栈和程序计数器是线程私有的,也就是每一个线程拥有自己独立的区域。互相不干扰。

二、程序计数器

我们的java代码在程序执行之前就被编译成字节码。而这个程序计数器不是我们计算机组成原理的程序计数器(存放的计算机指令地址),而我们的jvm的pc是字节码解释器的指示器。存放的是字节码的地址。如果执行的是java方法,这里存储的就是正在执行的字节码的地址,如果执行的是本地方法存储的就是undefined。

字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

在多线程的环境下,pc还能保证恢复到原来线程的位置。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

三、Java虚拟机栈

描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

栈帧:操作数栈

          动态链接

          方法出口信息

          局部变量表:rerturnAdress类型(指向了一个字节码指令的地址)

                               各种基本数据类型和引用类型

Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。在java方法中y有两钟返回方法:抛出异常和return语句。两种方式都回将栈帧弹出。

四、Java堆内存

Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。这段区域也是垃圾回收器经常光顾的地方。

Java 堆还可以细分为:新生代和老年代、永久代(方法区):再细致一点有:Eden 空间、From Survivor、To Survivor 空间(这三个都是新生代)

jdk1.8之前                                                                               之后

JVMå åå­ç»æ-JDK7JVMå åå­ç»æ-JDK8

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。

五、方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。在jdk1.8之后方法区(永生带)移入直接内存(元空间)。

六、运行时常量池

JDK1.8 版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。

七、对象的创建过程

Javaå建对象çè¿ç¨

(1)首先jvm遇到一个new的指令时,去咱们的常量池去查询是否有这个类的符号引用(全限定类名),如果没有表示未加载解析过这个类,就需要加载解析,同时在常量池中添加符号引用,在方法区添加类的信息。

(2)然后分配内存,分配的方式有两种(指针碰撞和空闲列表)

(3)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

(4)设置对象头,这个对象头哦存储的是:这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息和否启用偏向锁等。

(5)执行init方法,把对象按照程序员的意愿进行初始化(也就是构造函数之类的开始初始化)。

八、对象访问定位

(1)句柄方式:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

对象ç访é®å®ä½-使ç¨å¥æ

(2)直接指针: 如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。

对象ç访é®å®ä½-ç´æ¥æé

这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

九、内存溢出

StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。

OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 错误。

参考:https://snailclimb.gitee.io/javaguide/#/docs/java/jvm/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F?id=%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2-%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章