java程序员Java 杂谈JavaEE 学习专题

深入学习JVM 【1】运行时数据区域划分

2018-08-27  本文已影响9人  kk少年

前言

在使用c++进行编程时,我们通过new创建的每一个对象都需要有对应的delete操作去释放对象所占用的内存,对内存的掌控度比较高,但是程序员需要知道对象什么时候不需要使用了,并需要手动释放内存,如果忘记了delete释放,很容易出现内存泄漏(申请内存后,没有释放,会一直占用着)和内存溢出(因为过多的内存泄漏导致无法申请足够的内存,即out of memory)的问题。

相比之下,java虚拟机提供了自动内存管理机制,java程序员可以解放双手,不再需要去写delete等手动释放内存的代码,虚拟机会自动将内存中无用的对象占用的内存释放。

了解jvm的必要

虽然有自动内存管理机制的存在,但是不代表写的每个java程序都不存在内存泄漏和内存溢出问题,我们需要对虚拟机有足够的了解,才能在发生内存泄露和内存溢出的时候有效地排查问题。

本文将对jvm虚拟机运行时内存进行一个基本的介绍,后续的文章也会讲解jvm其他知识,大部分都是自己的读书总结加上自己的理解。希望将自己的所学进行总结的同时能惠及他人,如果有什么地方讲的不对,希望各位同学能够指出。

内存划分

java虚拟机将其管理的内存划分为以下几块:

运行时数据区域.png

各个区域都有其各自的特点和作用,以及不同的创建和销毁的时间

各个区域的介绍

程序计数器

扩展问题1:为什么需要程序计数器?

java虚拟机的多线程是通过线程轮转,分配CPU时间片来执行java程序,当线程切换时,为了能够回到原来的字节码执行位置继续程序的执行,所以每个线程会有一个程序计数器。

扩展问题2:Native方法是什么?

java程序执行的时候调用的方法,有些是用java语言实现的,有些是用其他语言编写实现的,用其他语言实现的方法称为Native方法本地方法,native方法会使用native关键字进行标注,如Object类的getClass()方法:

public class Object {
    
    public final native Class<?> getClass();
    ...
}

由于native方法不是java实现的,也就没有字节码行号之说,此时程序计数器的值应当为空(undefined)。

虚拟机栈

扩展1:局部变量表

局部变量表用于存放编译期可知的各种基本数据类型、对象引用、returnAddress类型(一条字节码指令的地址)

对于基本数据类型,存放的是变量的名和值;

对于引用类型,存放的是指向对象在堆中的起始地址。

ps: 对于64位的long或double类型的局部变量会占用两个局部变量表空间(Slot),其余的数据类型都是只占用一个局部变量表空间。

局部变量表所需要的空间在编译期间已经计算好了,在一个方法执行时,需要为栈帧分配多少局部变量表空间是完全确定的

本地方法栈

本地方法栈的特性和虚拟机栈几乎一样。

扩展1: 堆区细分

jvm为了垃圾回收的方便,将堆划分为新生代老年代,新创建的对象基本上都放在新生代中,而存活比较久的对象则会移到老年代中。新生代和老年代采用不同的垃圾收集算法,可以更高效地回收内存。采用复制算法的新生代还可以细分为EdenFrom SurvivorTo Survivor。具体的详情是怎样的,为了不偏离这篇文章的主旨,这里先打个问号,后序的文章将会详细介绍堆区的几个划分的用途。

堆区虽然是线程共享的,但是如果设定了启动参数-XX:+UseTLAB,则开启了本地线程分配缓冲(Thread local Allocation Buffer, TLAB),会为每个线程单独在堆中划分出一个TLAB,哪个线程需要分配内存,就先在该线程对应的TLAB中分配内存,当TLAB用完,才在堆区的Eden中继续申请一块TLAB

方法区

方法区是用于存放虚拟机加载的类信息、常量、静态变量、编译后的代码等数据。

方法区特点:

扩展1:运行时常量池:

class文件中有个常量池,运行时常量池就是class文件中常量池经过类加载后存放的内存区域。

常量池主要存放两类常量:字面量和符号引用。

字面量指字符串,声明为final的常量值等;而符号引用是java编译后生成的各种常量,其包括:

jdk1.8之前,方法区是用永久代实现的,
jdk1.7以下的版本,运行时常量池是方法区的一部分,而jdk1.7及之后的版本,运行时常量池中的字符串常量池已经不在方法区,而是在java堆中开辟了一块区域作为字符串常量池。

在jdk1.8开始,已经没有永久代的概念,譬如符号引用(Symbols)转移到了native 堆中的元空间;字面量也在 java heap;类的静态变量(class statics)转移到了java heap

扩展2:常量是否只能在编译期产生?
否,运行期也可能将新的常量放入运行时常量池中,比如Stringintern方法。在jdk1.7的表现如下:

// 如果运行时常量池中,存在"10"这个字符串常量
// 则将常量池中的字符串对象返回,
// 如果不存在,则直接在运行时常量池中创建“10"这个字符串,并将其返回。
String s = String.valueOf(10).intern();

直接内存

前面讲的几块都属于虚拟机管理的运行时数据区域,java程序中也有可能会用到不是虚拟机运行时内存区域的一部分。这块内存我们通常称为直接内存

直接内存的例子:
jdk 1.4 加入的NIO类,引入了一种基于通道Channel和缓冲区Buffer的IO方式。直接通过Native方法在java堆外的直接内存中分配内存, 通过存储在java堆中的DirectByteBuffer对象作为这块直接内存的引用。操作DirectByteBuffer即可操作直接内存,这样做的好处是避免了要使用直接内存的时候需要先复制到java堆中。直接操作直接内存更加高效。

点赞是对我最大的鼓励

上一篇 下一篇

猜你喜欢

热点阅读