Java自动内存管理详解

最近找了两本Java虚拟机方面的书,看了看其中对于Java自动内存管理的章节,写的都大同小异,在此总结一下,主要是三个方面:内存划分、内存分配、内存回收。

内存划分(运行时数据区)


JVM运行时数据区

从线程的角度来分,可分为线程私有和线程共享的,上图中左边的灰色区域就是线程共享的区域,包括堆、方法区、运行时常量池。而右边的区域则是线程私有的,包括程序计数器、虚拟机栈。

堆是虚拟机管理的内存中最大的一块,是被线程共享的一块区域,主要用于存放对象实例,但并不是所有对象都是在堆上分配的。同时堆也是垃圾收集器管理的主要区域。

方法区

方法区与堆一样,也是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区虽然逻辑上与堆独立,但物理上属于堆。

运行时常量池

运行时常量池属于方法区的一部分,用于存放class文件中的常量池信息,主要是各种字面值和符号引用。另外,运行时常量池并不要求常量一定只能在编译期产生,运行期间也可能将新的常量放入池中,例如String类的intern()方法。

程序计数器

类似于操作系统中的程序计数器,不过这里的程序计数器指示的是正在执行的字节码指令的地址。字节码解释器的执行完一条指令后,会改变程序计数器的值,指向下一条需要执行的指令地址。之所以需要每个线程都使用一个独立的程序计数器,是因为能够让多线程程序正确执行,各条线程之间的计数器互不影响。

虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型,其基本单位是栈帧,每个方法执行的时候都会创建一个栈帧。虚拟机一直在执行栈顶的栈帧所对应的方法,当一个方法中调用另一个方法时,就会新建一个被调用方法的栈帧,push进虚拟机栈,被调用方法执行结束,会将返回值写入调用他的栈帧,并将自己的栈帧从栈中弹出。
而方法的栈帧中,存放了局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表所需的内存空间都是在编译器就能够确定的,用于存放方法内部的本地变量;操作数栈则是用来进行运算操作,将两个操作数从栈顶弹出,计算结果,压入栈。

还有一个没有提及的是本地方法栈,与虚拟机栈相似,不过是为本地方法服务的,虚拟机规范中对其没有强制规定,可由虚拟机具体实现。

内存分配


java堆分代

上面说到,堆是内存管理的主要区域,堆中存放了各种各样的对象,进一步可以划分为新生代和老年代。其中,新生代里有Eden空间、From Survivor空间、To Survivor空间。这样划分主要是为了方便内存回收。具体各个空间的用途,到内存回收就会知道。

从Java代码中new一个对象说起,JVM首先会检查这个new指令的参数能够在常量池中定位到一个类的符号引用,然后检查与这个符号引用相对应的类是否已经成功经历过加载、解析、初始化等步骤,当类完成装载之后,就可以完全确定创建该类实例所需要的空间大小。然后JVM就会为该实例进行内存分配。

下面就是分配在哪的问题。一般会分配在堆中的Eden空间,如果启动了本地线程分配缓冲,会优先在TLAB(Tread Local Allocation Buffer,即本地线程分配缓冲区)中分配,TLAB是Eden空间中线程私有的部分,大约占据Eden总空间的1%。 如果分配到Eden空间失败,就会进行一次新生代的垃圾收集工作。对于需要大量连续内存的大对象,会直接分配到老年代。

另外涉及到的一个概念是逃逸分析。上文也提到,并不是所有的对象都在堆中分配,其中有一部分对象是在栈上分配的,这里说的栈就是指虚拟机栈帧中的局部变量表部分。逃逸分析是JVM执行性能优化之前的一种分析技术,具体目标是分析出对象的作用域。如果一个对象的作用于仅限于方法体内部,就会在栈上为其分配内存,栈帧随着方法退出而销毁,不需要参与到垃圾收集中去。但一旦方法内部的对象被外部对象引用,这个对象就因此发生了逃逸,就不会在栈上分配。

内存回收

内存回收涉及到几个方面:哪些内存需要回收?什么时候回收?如何回收?

可回收对象判定

常用的有引用计数算法和根搜索算法。

引用计数就是为每一个对象添加一个引用计数器,每当有一个地方引用它时,就将计数器的值加1,当引用失效时,计数器的值减1。任何时刻计数器值为0,说明对象不再被使用。此方法的缺陷在于,很难解决对象之间相互循环引用,如果两个需要回收的对象分别引用彼此,就无法被垃圾收集器回收。

根搜索算法通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,就证明此对象是不可用的。GC Roots对象包括栈帧中本地变量表中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中引用的对象。

垃圾收集算法

主要有标记-清除算法、复制算法、标记-压缩算法。

标记-清楚算法:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。标记和清除过程的效率都不高,而且标记清除之后会产生大量不连续的内存碎片,碎片太多会导致需要分配大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集操作。空闲的内存碎片可以用空闲列表来表示,从而提供下一次分配对象的内存地址。

复制算法:将内存划分为大小相等的两块,每次只使用其中的一块,当一块内存用完了,就将还活着的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。运行高效,代价是损失了一般的内存空间。在堆中的新生代垃圾收集算法中,就使用了复制算法。将Eden空间、From Survivor空间中存活的对象复制到To Survivor空间,然后将From Survivor空间和To Survivor空间互换。(如果Eden空间和From Survivor空间的存活对象的分代年龄大于一定阈值或者To Survivor空间已满,会直接被分配到老年代)Eden空间和两个Survivor空间的缺省比例是8:1:1。之所以可以在新生代使用复制算法,是因为大多数新生代对象的生命周期都非常短暂。

标记-整理算法:与标记-清除算法差不多,不过此算法将所有存活对象向内存的一端移动,然后直接清理掉另一端的内存。此算法应用于老年代的垃圾收集。由于能够整理出一大块连续的空闲内存区域,所以用一个指针指向空闲内存区域的起点,用于指向下一次内存分配的位置。

垃圾收集器

垃圾收集器有很多,而且虚拟机里整合了很多种垃圾收集器,本文不再赘述,值得一提的是Stop-the-World机制,通俗来说,垃圾收集进行的时候,工作线程必须停止一段时间,无论以哪种收集器进行垃圾收集,都会有或多或少的Stop-the-World时间。

另外一个是程序吞吐量与低延迟的权衡。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。可通过-XX:MaxGcPauseMillis设置垃圾收集造成的Stop-the-World的时间,但为了低延迟而将该值调小之后,会导致相应的新生代内存空间变小,内存空间越小越容易被耗尽,会导致GC更加频繁,总的用于GC的时间可能反而会变多,导致程序吞吐量下降。

时间: 2024-11-03 12:15:01

Java自动内存管理详解的相关文章

php内存管理详解

php的内存管理 php和c最重要的区别就是是否控制内存指针. 内存 在php中, 设置一个字符串变量很简单: <?php $str = 'hello world'; ?>, 字符串可以自由的修改, 拷贝, 移动. 在C中, 则是另外一种方式, 虽然你可以简单的用静态字符串初始化: char *str = "hello world"; 但是这个字符串不能被修改, 因为它存在于代码段. 要创建一个可维护的字符串, 你需要分配一块内存, 并使用一个strdup()这样的函数将内

Apache Spark 内存管理详解

Spark 作为一个基于内存的分布式计算引擎,其内存管理模块在整个系统中扮演着非常重要的角色.理解 Spark 内存管理的基本原理,有助于更好地开发 Spark 应用程序和进行性能调优.本文旨在梳理出 Spark 内存管理的脉络,抛砖引玉,引出读者对这个话题的深入探讨.本文中阐述的原理基于 Spark 2.1 版本,阅读本文需要读者有一定的 Spark 和 Java 基础,了解 RDD.Shuffle.JVM 等相关概念. 在执行 Spark 的应用程序时,Spark 集群会启动 Driver

Python深入06——python的内存管理详解_python

语言的内存管理是语言设计的一个重要方面.它是决定语言性能的重要因素.无论是C语言的手工管理,还是Java的垃圾回收,都成为语言最重要的特征.这里以Python语言为例子,说明一门动态类型的.面向对象的语言的内存管理方式. 对象的内存使用 赋值语句是语言最常见的功能了.但即使是最简单的赋值语句,也可以很有内涵.Python的赋值语句就很值得研究. a = 1 整数1为一个对象.而a是一个引用.利用赋值语句,引用a指向对象1.Python是动态类型的语言(参考动态类型),对象与引用分离.Python

C++内存管理详解

伟大的Bill Gates 曾经失言: 640K ought to be enough for everybody - Bill Gates 1981 程序员们经常编写内存管理程序,往往提心吊胆.如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的.本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理. 1.内存分配方式 内存分配方式有三种: (1)从静态存储区域分配.内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在.例如全局变量

javascript的内存管理详解_基础知识

介绍 低层次的语言,如C,具有低级别的内存管理命令,如:malloc()和free(),需要开发者手工释放内存.然而像javascript这样的高级语言情况则不同,对象(objects, strings 等)创建的时候分配内存,当他们不在使用的时候内存会被自动回收,这个自动回收的过程被称为垃圾回收.因为垃圾回收的存在,让javascript等高级语言开发者产生了一个错误的认识,以为可以不用关心内存管理. 内存生命周期 不管什么样的编程语言,内存的生命周期基本上是一致的. 1.分配你需要的内存 2

Objective-C内存管理详解

手动管理内存 retain计数是一个相当简单的概念,Objective-C中的内一个对象都有一个retain计数.retain计数是一个整数.使用alloc函数创建一个对象时,该对象的retain计数设为1.当计数变为0的时候,对象被释放.一般通过发送retain消息给对象,从而增加对象的retain计数.发送release给对象,则减少retain计数的值. 手动管理内存前,先进入项目的"Build Settings" 里面,找到"Objective-C Automatic

Linux账户管理详解

当用户登陆Linux系统时,Linux将做如下检查: 1)在/etc/passwd文件里匹配输入的用户名,获取该用户名的UID和GID(其中GID和/etc/group关联) .Home目录和Shell设置 2)在/etc/shadow里核对该用户的密码 /etc/passwd文件结构 这个文件的每一行代表一个账号,如下所示: oracle:x:501:501::/home/oracle:/bin/bash 1. 用户名 2. 密码:早期的密码放在该字段,但如今的密码已单独放在/etc/shad

Java直接(堆外)内存使用详解

本篇主要讲解如何使用直接内存(堆外内存),并按照下面的步骤进行说明: 相关背景-->读写操作-->关键属性-->读写实践-->扩展-->参考说明 希望对想使用直接内存的朋友,提供点快捷的参考. 数据类型 下面这些,都是在使用DirectBuffer中必备的一些常识,暂作了解吧!如果想要深入理解,可以看看下面参考的那些博客. 基本类型长度 在Java中有很多的基本类型,比如: byte,一个字节是8位bit,也就是1B short,16位bit,也就是2B int,32位bit

Java虚拟机自动内存管理

生活规律告诉我们,在享受便利的同时一般都会付出巨大的代价,如果你在享受了便利的同时,还没有为此付出代价,不是说明没有,只是还没到付出的时候.试问,有哪个Java系统架构师不懂Java虚拟机?纵观Java程序员的发展历程,又有多少人是卡在了Java虚拟机之上.所以如果你还没有感觉到为此付出代价,说明你已经Java虚拟机的糖衣炮弹所击中,且被毒害之深.Java的自动内存管理就是这样,像毒药一样,一旦上瘾就很难戒掉,而且会沉迷于此.而正确的做法就是了解其原理,拿到尚方宝剑,当虚拟机不好好为你提供服务时