JVM学习(Java内存区域)

JVM学习(Java内存区域)

随着Java学习的越来越深入,对框架了解过后,决定从Java底层入手,一步一步的更加了解Java,熟悉Java,争取能做到优化Java程序以及了解Java整体的一个架构,所以博主我读了 《深入理解Java虚拟机》这本书,所以有关JVM(Java虚拟机,下文皆用JVM代替)开头的博客都是我在学习这本书中整理出来的内容。提及的知识点也都来自于这本书,摘抄也是。我只是为了巩固我的印象以及帮助那些能看到这篇文的更容易理解书中的内容。

对于Java程序员来说,JVM是再熟悉不过的东西了,在我们进行编程的同时,我们也将内存控制的权力交给了JVM,与此同睡一旦出现内容泄漏和溢出方面的问题,如果我们对JVM不熟悉,不了解其中的问题以及原理,排查错误以及找出错误优化,这对我们来说将会是很艰难的工作。

1、运行时数据区域

JVM会在Java程序运行过程中,把它所管理的内存划分为若干个不同的区域,区域各自有各自的用途,以及创建销毁的时间,有的区域跟着虚拟机进程而存在,有的区域则依赖用户线程的启动结束建立销毁。
下图是运行时数据区的结构,接下来我将为大家一一讲解每个结构的内容以及我的理解
JVM学习(Java内存区域)

1.1 程序计数器

虽然程序计数器在图中看上去很大,但其实程序计数器是非常小的一部分内存空间,如其名,程序计数器,可以看做是给线程执行的字节码的一个行号指示器,你程序运行总得有个顺序吧,但运行时很大可能不会是一个线程维持所有java程序的运行,那么这时线程交替执行,执行了那个再来执行你这个,这个时候程序计数器的作用就出来了,记录了你上次运行截止的位置,在下次时间片切换到你这里运行时,依旧知道从哪里接着上次的运行。各个程序计数器之间互不影响,程序计数器是线程私有的内存,每个线程都拥有自己单独的一个线程计数器,他也能通过改变这个计算器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要它

1.2 Java虚拟机栈

Java虚拟机栈和程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期也跟线程相同。虚拟机栈描述的是Java方法执行时的内存模型。每个方法在执行的同时会创建到一个栈帧,这栈帧中对应的就是这个方法中的操作数栈局部变量表方法出口动态链接等信息,虚拟机栈是个后进先出栈,排在后面的方法会先加载完成。其中
局部变量表,它是存放了编译器可知的各种基本数据类型、以及对象引用,以及操作数栈操作完后和方法中的变量也存在这,它的内存申请的大小在刚开始编译就是已经确定下来的。
操作数栈,这个理解起来就很简单,我是认为它是内置在栈帧中用来给其中的运算或者其他操作执行的一片区域,比如变量之间的加减乘除都会压入到这个操作数栈中进行操作,之后将得到的值再存入表中。
方法出口,众所周知,方法返回结果一是在执行过程中遇到报错终止,二就是通过return值返回到固定位置,无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态,这就是方法出口的作用。
动态链接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。常量池中一般存在有大量的符号引用,这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(final、static等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态链接。
在JVM规范中,对这个虚拟机栈区域规定了两种异常状况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error异常,如果虚拟机栈进行动态扩展,但扩展的同时虚拟机栈没申请到足够的内存,就会抛出OutOfMemoryError的异常

1.3 本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,他们的区别不过是虚拟机栈执行的是Java方法服务,本地方法执行的是Native方法服务,因为虚拟机规范中没有具体要求,所以具体的虚拟机可以*实现它,甚至有的虚拟机将本地方法栈与虚拟机栈合二为一。

1.4 Java堆

事实上,堆确实应该被广大Java程序员所熟知,因为它也是JVM管理的内存中最大的一块,它也是被所有Java线程共享的一块区域,在虚拟机启动时创建。Java堆的唯一目的就是存放对象实例,几乎所有的对象都在这里分配。近来因为JIT(Just In Time ,Java即时编译器会将Java编译中出现频率次数高的热点代码用另一个线程二次编译,当下次需要这段代码时,直接替换掉需要的部分,也是另一种方式的优化)发展,所有的对象在堆上的分配也不是那么绝对了。
Java堆中可以细分为新生代和老年代,新生代又分为Eden空间、From Survivor空间、To Survivor空间,按照默认Eden:from:to内存区域是8:1:1大小。其中的回收机制我们在垃圾回收章节再做介绍,当然这三个区域都是针对内存的回收和分配来更好的存在的。他们也能进行调整与规划,这也是一定程度上所说的JVM调优。

1.5 方法区

方法区和Java堆一样,也是每个线程共享的内存区域,它虽然叫做方法区但它并不是用来存储方法,方法上文我们说过通过线程运行创建栈帧实现,但这个方法区里面存储的是已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据,虽有有些开发者喜欢叫方法区叫为永久代,但其实他们并不相等。为什么叫方法区呢又被称为永久代呢,因为里面存在的内存大多数是垃圾回收不了的,存在的时间就像永久一样,这个区域的垃圾回收主要是针对常量池的回收和对类型的写在,一般来说取得不到什么成效,所以这部分区域回收很少但是却还是有必要的,因为当方法区无法满足内存分配的需求时候会抛出OutOfMemoryError异常。

2 其他知识补充

2.1 运行时常量池

运行时常量池上文我们有提及过,它确实是属于方法区的一部分,属于其中的常量池,用来存放编译期生成的各种字面量和符号引用。运行时常量池最大的特征就是具备它的动态性,不要求常量一定只有编译期才能产生,运行期间也能将新的产生的常量放入常量池,这种特性被开发人员使用的表较多的便是String类中的intern()方法。在某些时刻,创建的相同内容字符串可能也是指向相同的地址,可能这也是运用了常量池的结果。

2.2 直接内存

直接内存并不属于JVM运行时数据区的一部分,也不是JVM定义的内存区域,但是这一块内存区域也有被频繁使用。在JDK 1.4中加入了NIO类,引入了一种基于通道与缓冲区的IO方式,运用本机自身直接的内存分配,来直接分配堆外内存,通过一个存储在Java堆中的对象作为这块内存的引用,这样能在某些场景通过这个对象直接到本机的内存,能够在某些时候显著的提高性能,也避免了在Java堆中和Native堆中来回复制数据。

2.3 对象创建过程

对象创建总的来说就五步
JVM学习(Java内存区域)
①类加载
在虚拟机遇到一条new命令时,首先会去检查这个指令的参数是否能在常量池中定位到类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析初始化过,如果没有则要先进去类加载过程,如果有就照搬再进行下一步。
②分配内存
类加载过程后,虚拟机将为新生对象分配内存,对象需要的大小在类加载后即可完全确定,为对象在Java堆中划出同样大小的内存区域。
内存分配的方式有“指针碰撞”和“空闲列表”两种,与他们名字相似,指针便是指向内存的指针,指针碰撞得假设内存区域是规整的而不是交错混乱的,指针向内存空闲区域移动所需的对应内存大小,接着将那一块内存区域分配给它。空闲列表则是在分配的时候找到一块足够大的空闲的内存区域划分给它,并且更新列表上的记录。
③初始化零值
众所周知,int的默认零值是0,Integer的默认零值是null,初始化零值的作用就是用来干这一方面事情的,在对象没有被赋予初始值的情况下就能被使用。
④设置对象头
对象头,对象头包含的是这对象的相关信息,比如这对象是哪个类的实例、如何找到类的元数据信息,对象的哈希码、对象的GC年龄等信息,这些都存储在对象头中,在虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方法。
⑤执行init方法
最重要的一步也是最关键不可或缺的一步,便是执行init方法,在上面的步骤都完成了之后从JVM的角度来看,一个新的对象已经产生。但从Java程序来看,对象创建才刚刚开始,init方法没有执行,所有字段都还为0,一般来说执行new指令后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才真正诞生了出来。


这次的JVM内存介绍就到这了,大部分是摘抄自《深入理解Java虚拟机》的内容,如果我提出的内容或者哪里不符有错误,请大家指出私信我更正,希望在Java学习道路上越走越远。