JVM-内存区域

Java的内存交给JVM去管,快乐。一旦出现内存泄漏和溢出不知道原理,不会排查就GG了,所以要学。

1.运行时数据区域

(1)程序计数器

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

JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。每个线程有单独的程序计数器在“线程私有”内存里。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。此内存区域 是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

(2)Java虚拟机栈

线程私有,生命周期与线程相同。为Java方法服务,描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出 栈的过程。 

小白说的栈是指局部变量表部分,存放了基本类型、对象引用和returnAddress类型(指向字节码指令的地址)。编译期间存完,大小固定。异常情况是爆内存、爆栈深度。

(3)本地方法栈

线程私有,生命周期与线程相同。为Native方法服务。Native方法就是所谓的本地方法,用关键字native修饰,像接口一样定义,用其他语言实现,可以返回任何Java类型;使用的时候对其他类没什么影响,其他类甚至都不知道它们调用的Native方法;可以被继承然后用Java语言写。作用:与java环境外交互,与操作系统交互。

(4)Java堆

所有线程共享,存实例对象,是垃圾收集器管理的主要区域,所以也叫GC堆。可以处于物理不连续但逻辑连续的内存空间。

(5)方法区

所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。除了和Java堆一样不需要连续的内存和可以 选择固定大小或者可扩展外,还可以选择不实现垃圾收集。

(6)运行时常量池

方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。编译、运行时都可以产生,例如String类的intern()方法。

String.intern()是一个Native方法,它的作用是:如果字符串常量池中已经包含一个等 于此String对象的字符串,则返回代表池中这个字符串的String对象;否则,将此String对象包 含的字符串添加到常量池中,并且返回此String对象的引用。有就引用,没有就新建并加到池中。

(7)直接内存 

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓 冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储 在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著 提高性能,因为避免了在Java堆和Native堆中来回复制数据。 

2.HotSpot虚拟机对象探秘

(1)对象的创建

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一 个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没 有,那必须先执行相应的类加载过程。

如何在堆中划分内存?如果内存绝对规整(用过的在一边,没用过的在一边),则是用指针像空闲内存移动一段距离,这种方式叫“指针碰撞”。如果内存不规整(散乱),那就需要维护一个列表,上面记录哪些空闲块可以用,在分配时找一块足够的内存空间分给对象并更新列表,这种方式叫“空闲列表”。选择哪种方式由采用的垃圾收集器功能决定。

线程安全方面怎么办?创建对象很频繁,高并发不是线程安全的,例如正在给A分内存,指针没来及修改就被B拿去分内存。解决方案有2种。

一种是对分配内存空间的动作进行同步处理 ——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;CAS全称是Compare And Swap,比较并替换的意思。有3个基本操作数,内存地址V,旧值A,新值B。假设有个内存地址V,存着值为10的变量。现在线程1要对这个变量加1,对于线程1来说,A=10,B=11。线程1要更新前,线程2抢先一步,把内存地址的变量值率先改为11。此时线程1提交更新,用自己的A去和地址V的实际值比较,发现A不等于实际值,提交失败。那就再来一次,重新获取地址V的实际值,A=11,计算得B=12,这个重来的过程叫自旋。这一次没有其他线程来捣乱,线程1提交更新,比较自己的A和地址V的实际值,发现相等,就把地址V的实际值替换为B,也即是12。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。划分内存又不是什么深仇大恨,乐观点。)

另一种是把内存分 配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内 存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内 存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。 

内存分配后,默认值都是0(不包括对象头)。接下来就是对对象进行必要设置,信息存在对象头里。然后就是初始化,执行<init>方法,为变量赋予我们想赋予的值。

(2)对象的内存布局(对象头+实例数据+对齐填充)

对象头=MarkWord+类型指针。前者用于存储自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,如果是数组对象,还有一块用于记录数组长度的数据,所以大小不定。后者是指向它的类元数据的指针,通过这个指针确定这个对象是哪个类的实例。

(3)对象的访问定位

需要通过栈上的reference数据来操作堆上的 具体对象。目前主流的访问方式有使用句柄和直接指针两种。 

《深入理解Java虚拟机第二版》

原文地址:https://www.cnblogs.com/shoulinniao/p/12635488.html