深入理解java虚拟机—内存管理机制

子健儿      2022-04-09     430

关键词:

前面说过了类的加载机制,里面讲到了类的初始化中时用到了一部分内存管理的知识,这里让我们来看下Java虚拟机是如何管理内存的。

先让我们来看张图

有些文章中对线程隔离区还称之为线程独占区,其实是一个意思了。下面让我们来详细介绍下这五部分;

运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都拥有自己的用途,并随着JVM进程的启动或者用户线程的启动和结束建立和销毁。

先让我们了解下进程和线程的区别:

进程是资源分配的最小单位,线程是程序执行的最小单位。

进程有自己的独立地址空间,每启动一个进程,系统就会为它分配一个地址空间、建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费要比进程小很多,同时创建一个线程的开销也要比进程小很多。

同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包含一个线程。线程之间的通信更加方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信则需要以通信的方式(IPC)进行。

这里引用在知乎中某位同学的解释,

进程的颗粒度太大,每次都要有上下的调入,保存,调出。如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。那么这里具体的执行就可能变成:

程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。

这里a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。

看到这里是不是对线程共享和线程隔离区有了一个更深次的理解。可以理解为方法区和堆是分配给进程的,也就是线程共享区,而栈和程序计数器则是分配给每个独立线程的。

在SUN公司的HotSpot虚拟机中将java虚拟机栈和本地方法栈合二为一了

程序计数器(Program Counter Register)

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看成是当前线程所执行字节码的行号指示器。在计算机中,其实程序计数器就是一个寄存器,依据不同计算机细节的差异,它可以存放当前正在被执行的指令,也可以放下一个被执行的指令。

在虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令的

由于Java虚拟机的多线程是通过线程轮询切换并分配处理器执行时间的方式来实现的,在任何一个确定的时候,一个处理器都只会执行一条线程中的指令,因此为了线程切换之后能过恢复到正确的执行位置,每条线程都需要拥有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,所以程序计数器是线程私有的内存,也就是它属于线程隔离区的。

如果线程执行的是一个Java方法,这个计数器记录的就是正在执行的虚拟机字节码指令地址;如果正在执行的是Native方法,那么这个计数器的值就是(Undefined)。

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,即他的生命周期和线程相同。

在Java中,JVM中的栈记录了线程的方法调用,每个线程拥有一个栈,在某个线程的运行过程中,如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即栈针(Stack Frame)。

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈针(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法从调用至完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

当被调用方法运行结束时,该方法对应的帧将被删除,参数和局部变量所占据的空间也随之释放。线程回到原方法,继续执行。当所有的栈都清空时,程序也随之运行结束。

我们经常说的栈内存其实就是现在讲的虚拟机栈,或者说是虚拟机栈中局部变量表部分。

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是指向对象起始位置的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置,引用所指向的对象保存在堆中(引用可能为Null,即不指向任何对象))和returnAddress类型(指向了一条字节码指令的地址)。

其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。局部变量表所需要的内存空间在编译时期完成分配。当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

异常有两种

1,线程请求的栈深度大于虚拟机所允许的深度将抛出StackOverflowError异常 (递归调用)

2,如果虚拟机可以动态扩展,如果扩展时已经无法申请到足够的内存就会抛出OutOfMemeoryError异常。

List list=new ArrayList();
        for(;;){
            int[] tmp=new int[1000000];
            list.add(tmp);
        }

 

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的。他们之间的区别就是Java虚拟机栈是位虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为位虚拟机使用到的Native方法服务。

其实虚拟机规范中对本地方发栈中方法所使用的语言、使用方式以及数据结构都没有强制规定,因此具体的虚拟机可以自由地实现它。甚至在有的虚拟机(如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemory异常。

Java堆

对于大多数应用来说,Java堆(Java Heap)是Java虚拟机管理的内存中最大的一块。Java堆是被所有线程共享的一块数据区域,在虚拟机启动时创建,这一内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也逐渐变得不是那么“绝对”
堆中可细分为新生代和老年代,在细分可以分为Eden空间、Form Survivor空间、to Survivor空间。
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
根据Java虚拟机规范规定,Java堆可以处于物理上不连续的内存中,即只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,可以固定大小也是可扩展的。主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms来控制)。如果在堆中没有内存可分配,并且堆也无法继续扩展时,将会抛出OutOfMemortError异常。

Java的普通对象存活在堆中,与栈不同,堆的空间不会随着方法调用结束而清空。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存控件将会最终消耗殆尽。

方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译之后的代码等数据。虽然Java虚拟机将其描述为堆的一个逻辑部分,但它却有一个别名叫做Non-Heap(非堆)。目的是与Java堆区分开来。(以前很多人把方法区称为永久代,现在JDK1.8中已经用元数据区域取代了永久代)。

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入到方法区的运行时常量池中存放。并非预置入Class文件中常量池的内容才进入方法运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。

复制代码
public class Test
{
    public static void main(String[] args)
    {
        String s1="hello china";//字节码常量
        String s2="hello china";
        String s3=new String("hello china");

        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
        System.out.println(s1 == s3.intern());//运行时常量  intern 是个native方法
    }
}
复制代码

当方法区无法满足内存分配需求时,抛出OutOfMemoryError

注:JDK8之前,方法区由永久代实现,主要存放类的信息、常量池、方法数据、方法代码等;JDK8之后,取消了永久代,提出了元空间,并且常量池、静态成员变量等迁移到了堆中;元空间不在虚拟机内存中,而是放在本地内存中。

直接内存

由于直接内存(Direct Memory)并不是java虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分也被频繁使用,而且也可能导致内存溢出异常出现,所以也放到这一部分进行简介。

先让,本机直接内存的分配不会受到Java堆大小的限制,但是肯定还是会受到本机总内存大小以及处理器寻址空间的限制。管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常会忽略直接内存,使得各个内存区域总和大于物理内存限制(包含物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

JDK1.4加入了NIO,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。因此避免了在Java堆和Native堆中来回复制数据,提高了性能。

 

Object obj = new Object();

Object obj将会反映到虚拟机栈中(reference类型)
new Object()将会反映到Java堆中
此类的对象类型、父类、实现的接口、方法等信息数据,将反映到方法区中 

深入理解java虚拟机第二部分.内存自动管理机制.5.调优实战

高性能硬件上的程序部署策略在高性能硬件上部署程序,目前主要有两种方式:  通过64位JDK来使用大内存。    -- 缺点:GC停顿时间长  使用若干个32位虚拟机建立逻辑集群来利用硬件资源。  --思想:分治... 查看详情

深入理解java虚拟机类加载机制

本文内容来源于《深入理解Java虚拟机》一书,非常推荐大家去看一下这本书。本系列其他文章:【深入理解Java虚拟机】Java内存区域模型、对象创建过程、常见OOM【深入理解Java虚拟机】垃圾回收机制1、类加载机制概述虚拟机把... 查看详情

深入理解java虚拟机垃圾回收机制

本文内容来源于《深入理解Java虚拟机》一书,非常推荐大家去看一下这本书。本系列其他文章:【深入理解Java虚拟机】Java内存区域模型、对象创建过程、常见OOM1、垃圾回收要解决的问题垃圾收集(GarbageCollection,GC),要设计... 查看详情

深入理解java虚拟机第二部分.内存自动管理机制.3.垃圾收集器与内存分配策略

1、学习目的当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术实施必要的监控和调节。Java内存运行时区域的各个部分,其中程序计数器、虚... 查看详情

深入理解java虚拟机走进java

1.JDK:java程序设计语言、java虚拟机、javaAPI二、自动内存管理机制-----------------------------------------------------  1.运行时数据区域:    (1)java虚拟机在执行java程序的过程中会把所管理的内存划分为若干个不同的数据区域。这些... 查看详情

深入理解jvm虚拟机:java运行时数据区域

概述JVM是Java语言的精髓所在,因为它Java语言实现了跨平台运行,以及自动内存管理机制等,本文将从概念上介绍JVM内存的各个区域,说明个区域的作用。JVM运行时数据区模型Java虚拟机在执行Java程序的过程中会把它所管理的内... 查看详情

深入理解java虚拟机-垃圾回收机制(gc)

垃圾回收机制(GC)是java常重要特性之一。它让开发者无需关注内存的创建和释放,而是通过GC自动回收垃圾(无用对象)。哪些内存需要回收java堆和方法区是垃圾回收的主要内存区域,程序计数器、虚拟机栈、本地方法栈这几个内... 查看详情

深入理解java虚拟机-java内存区域,垃圾回收机制和内存分配策略(代码片段)

本篇主要参考周志明老师的《深入理解Java虚拟机》第三版一个Java程序,首先要经过javac编译成.class文件,.class文件是给JVM进行识别的,JVM将.class文件加载到方法区,执行引擎会执行这些字节码,执行时,... 查看详情

深入理解java虚拟机---4虚拟机类加载机制

类加载的整个生命周期:   加载、连接(验证、准备、解析)、初始化、使用、卸载。加载:    class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态数据结构转化为方法区中运行的数据结构,... 查看详情

深入理解jvm自动内存管理机制

2.1C、C++内存管理是由开发人员管理,而Java则交给了JVM进行自动管理2.2JVM运行时数据区:方法区、堆(运行时线程共享),虚拟机栈、本地方法栈、程序计数器(运行时线程隔离,私有)  2.2.1程序计数器(ProgramCounterRegister):每一... 查看详情

深入理解java虚拟机:jvm内存管理与垃圾收集理论(代码片段)

文章目录阅读的疑问???第二部分自动内存管理第2章Java内存区域与内存溢出异常1.程序计数器2.Java虚拟机栈3.本地方法栈4.Java堆5.方法区6.直接内存(我理解就是堆外内存吧)HotSpot虚拟机对象探秘1.对象的创... 查看详情

深入理解java虚拟机——类加载机制(代码片段)

文章目录类加载机制类的生命周期类的加载过程1、加载2、验证3、准备4、解析5、初始化类的初始化时机类加载器类与类加载器类加载器分类双亲委派模型工作过程源码分析双亲委派机制的好处类加载机制类的生命周期一个类型... 查看详情

深入理解java虚拟机

java内存区域Java虚拟机执行java程序时会将管理的内存划分为若干个区域:   1. 程序计数器    程序计数器是一个”线程私有“的内存区域,用于获取下一条需要执行的字节码指令,如分支、循环、跳转等。  2.Ja... 查看详情

jvm|第2部分:虚拟机执行子系统《深入理解java虚拟机》#yyds干货盘点#

...第2部分:虚拟机执行子系统)</font>前言参考资料:《深入理解Java虚拟机-JVM高级特性与最佳实践》第1部分主题为自动内存管理,以此延伸出Java内存区域与内存溢出、垃圾收集器与内存分配策略、参数配置与性能调优等相关... 查看详情

深入理解java虚拟机——java内存区域与内存溢出异常

Java虚拟机全称:javavirtualmachine;是Java开发语言中,用来运行Java字节码文件的平台;通俗的讲,就是一个程序。它提供对Java字节码的解释及运行,从而使Java语言能独立于各个系统平台。Java虚拟机在执行Java程序的过程中会把它... 查看详情

深入理解java虚拟机——类加载机制概述

一、类加载机制概述虚拟机把描述类的数据从Class文件加载加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在Java语言里面,类型的加... 查看详情

深入理解java虚拟机——类加载机制概述

一、类加载机制概述虚拟机把描述类的数据从Class文件加载加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。在Java语言里面,类型的加... 查看详情

《深入理解java虚拟机》一些笔记

大三下学期刚考完,把一个商城项目匆匆做完了,开始捧起这本书,但是感觉内容繁多,为了便于以后复习,这里总结一些笔记,大部分都是直接copy书上的,这本书还没看完也不知道自己能看到哪里,慢慢看吧第一章 走进java... 查看详情