深入理解java:1.3.1 JVM内存区域的划分(运行时数据区)
学习Java GC机制,可以帮助我们在日常工作中
排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。
我们将从4个方面学习Java GC机制,
1,内存是如何分配的;
2,哪些内存需要回收;
3,在什么情况下执行GC以及执行GC的方式;
4,如何监控和优化GC机制。
先来看看[1,内存是如何分配的;],JVM内存区域的划分(运行时数据区)。
JVM的体系结构如下:JVM的类加载系统、执行引擎系统、垃圾回收器都是去访问 运行时数据区(即 JVM虚拟内存)。
Java内存区域
在Java运行时的数据区里,由JVM管理的内存区域分为下图几个模块:
其中:
1,程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。
每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。
如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;
如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined。
由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。
2,虚拟机栈(JVM Stack):它是线程私有的。一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作数栈、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。
局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已 经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);
不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。
3,本地方法栈(Native Method Statck):本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。
本地方法栈也是线程私有的。
4,堆区(Heap):堆区是理解Java GC机制最重要的区域,没有之一。
在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。
堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。
一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。
关于堆区的内容还有很多,将在下节“Java内存分配机制”中详细介绍。
5,方法区(Method Area):所有线程共享。在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);
另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。
不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。本文中,由于主要使用JDK6.0,因此仍将使用永久代一词。
方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。
方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。
一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。
在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。
在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。
运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码时用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);
运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。
6,直接内存(Direct Memory):直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存。
JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。
由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。
Java对象的访问方式
一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。
以最简单的本地变量引用:Object obj = new Object()为例:
- Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
- new Object()作为实例对象数据存储在堆中;
- 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;
接下来来看看[2,哪些内存需要回收;],JVM堆内存的分配机制。
一般的,对象的内存分配都是在堆上进行,JVM堆就是主要的垃圾回收对象。
Java内存分配和回收的机制概括的说,就是:分代分配,分代回收。
对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区,不在JVM堆)。
年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。
注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。
年轻代上的内存分配是这样的,年轻代可以分为3个区域:
Eden区(伊甸园,亚当和夏娃偷吃禁果生娃娃的地方,用来表示内存首次分配的区域,再贴切不过)和两个存活区(Survivor 0 、Survivor 1)。内存分配过程为:
- 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
- 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
- 下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;
- 将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;
- 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。
从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。
经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空。
到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高。
这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。
年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代。
年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。
如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。
用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。
可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。
解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。
最后来看看[3,在什么情况下执行GC以及执行GC的方式;],每个分代的收集方法。
年轻代:
事实上,在上一节,已经介绍了新生代的主要垃圾回收方法。
在新生代中,使用“停止-复制”算法进行清理,将新生代内存分为2部分,
1部分 Eden区较大,1部分Survivor比较小,并被划分为两个等量的部分。
每次当Eden区满时触发Young GC进行清理时,将Eden区和一个Survivor中仍然存活的对象,拷贝到 另一个Survivor中,然后清理掉Eden和刚才的Survivor。
由于绝大部分的对象都是短命的,甚至存活不到Survivor中,所以,Eden区与Survivor的比例较大,HotSpot默认是 8:1,即分别占新生代的80%,10%,10%。
如果一次回收中,Survivor+Eden中存活下来的内存超过了10%,则需要将一部分对象分配到 老年代。
用-XX:SurvivorRatio参数来配置Eden区域Survivor区的容量比值,默认是8,代表Eden:Survivor1:Survivor2=8:1:1.
老年代:
方法区(永久代):
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:
- 类的所有实例都已经被回收
- 加载类的ClassLoader已经被回收
- 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)
永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收。
深入理解java:1.3.1 JVM内存区域的划分(运行时数据区)的更多相关文章
- jvm系列(一)运行时数据区
C++程序员肩负着每一个对象生命周期开始到终结的维护责任.Java程序员则可以借助自动内存管理机制,不需要自己手动去释放内存.由虚拟机进行内存管理,不容易出现内存泄漏和内存溢出的问题,但是一旦出现这些 ...
- JVM笔记【1】-- 运行时数据区
目录 (一)java内存区域管理 1.1 程序计数器 1.2 虚拟机栈 1.3 本地方法栈 1.4 java堆 1.5 方法区 1.5.1 运行时常量池 (二)直接内存 (一)java内存区域管理 C ...
- JVM 专题十二:运行时数据区(七)对象的实例化内存布局与访问定位
1. 对象的实例化 1.1 创建对象的方式 new 最常见的方式 变形1 : Xxx的静态方法 变形2 : XxBuilder/XxoxFactory的静态方法 Class的newInstance() ...
- Java虚拟机所管理的内存,包含的运行时数据区域?
运行时数据区域 线程私有(随用户线程的启动和结束而建立和销毁)或所有线程共享(随虚拟机进程的启动而存在) 抛出的异常 备注 程序计数器(Program Counter Register) 线程私有 唯 ...
- 《深入理解 Java 虚拟机》学习笔记 -- 内存区域
<深入理解 Java 虚拟机>学习笔记 -- 内存区域 运行时数据区域 主要分为 6 部分: 程序计数器 虚拟机栈 本地方法栈 Java 堆 方法区 如图所示: 1. 程序计数器(线程私有 ...
- Jvm基础(1)-Java运行时数据区
最近在看<深入理解Java虚拟机>,里面讲到了Java运行时数据区,这是Jvm基本知识,把读书笔记记录在此.这些知识属于常识,都能查到的,如果我有理解不对的地方,还请指出. 首先把图贴上来 ...
- JVM内存区域(运行时数据区)划分
前言: 我们每天都在编写Java代码,编译,执行.很多人已经知道Java源代码文件(.java后缀)会被Java编译器编译为字节码文件(.class后缀),然后由JVM中的类加载器加载各个类的字节码文 ...
- JVM 专题十三:运行时数据区(八)直接内存
1. 直接内存 不是虚拟机运行时数据区的一部分,也不是<Java虚拟机规范>中定义的内存区域. 直接内存是Java堆外的.直接向系统申请的内存区间. 来源于NIO,通过存在堆中的Direc ...
- jvm内存模型(运行时数据区)
运行时数据区(runtime data area) jvm定义了几个运行时数据区,这些运行时数据区存储的数据,供开发者的应用或者jvm本身使用.按线程共享与否可以分为线程间共享和线程间独立. 线程间独 ...
- 《深入理解Java虚拟机》笔记01 -- 运行时数据区
运行时数据区示意图 1. 程序计数器 占用一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器.主要用来记录线程执行到哪条语句了,分支.循环.跳转.异常处理.线程恢复等功能都需要依赖这个 ...
随机推荐
- Spring + Mybatis 企业应用实战 第3章 Sping MVC的常用注解
注解(annotation) @Controller @Controller是扩展的@Component的,可以说基本一样,就是作为一种标志. @RequestMapping value: 指 ...
- mac下phpize编译提示Cannot find autoconf解决办法
mac下phpize编译如下报错: /usr/bin/phpizeConfiguring for:PHP Api Version: 20121113Zend Module Api No: 201212 ...
- 【HDU2089】不要62
题目大意:求区间 [n,m] 中数位不包含 4 和 62 的数字的个数. 题解:数位dp. 预处理出 \(f[i][j]\) 表示 i 位数字中以 j 为第一位的满足条件的数字个数. 对于统计答案来说 ...
- ZROI 19.07.31 AB班ACM
写在前面:非常感谢cjc和djh两位神仙带我,非常感谢他们给了我一次躺赢的机会. 虽然我被硬点成了代码手,但我写的基本每次都有一堆罚时.然而djh爷全部1A,tql. 题目按照一血时间升序,大致符合难 ...
- 用Chrome 浏览器调试移动端网页 chrome://inspect/#devices
谷歌输入(chrome://inspect/#devices) 我使用的是小米(红米NOTE2 ),电脑是win 10 系统,以下几步就可以轻松使用浏览器内置的功能调试移动端网页了: 1. 手机开启调 ...
- java 实现 图片与byte 数组互相转换
package webgate; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import ja ...
- 可持久化Trie模板
如果你了解过 01 Trie 和 可持久化线段树(例如 : 主席树 ).那么就比较好去可持久化 Trie 可持久化 Trie 当 01 Trie 用的时候能很方便解决一些原本 01 Trie 不能解决 ...
- POJ 3260 The Fewest Coins(完全背包+多重背包=混合背包)
题目代号:POJ 3260 题目链接:http://poj.org/problem?id=3260 The Fewest Coins Time Limit: 2000MS Memory Limit: ...
- DVWA--upload
文件上传漏洞 0x01了解文件上传漏洞的用处是用来干什么的 什么是文件上传漏洞? 文件上传漏洞是指由于程序员在对用户文件上传部分的控制不足或者处理缺陷,而导致的用户可以越过其本身权限向服务器上上传可执 ...
- wannafly 练习赛11 B 假的字符串(字典树+建边找环)
链接:https://www.nowcoder.com/acm/contest/59/B 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 32768K,其他语言65536K 64bit ...