5.JVM内存管理
JAVA虚拟机在执行java程序的过程中,会把它管理的内存分成若干个不同的数据区域。
------------------------------------------------------------------------------------—
| 运行时数据区 |
| ----------- -------- ----------------- |
| | 方法区 | | 栈 | | 本地方法栈 | |
| | | | | | | |
| ----------- -------- ----------------- |
| |
| --------------------------- ----------------- |
| | 堆 | | 程序计数器 | |
| | | | | |
| --------------------------- ----------------- |
| ⬇️ ⬆️ ⬇️ ⬆️ |
| ---------------------------- ------------------ ----------------- |
| | 执行引擎 | | 本地库接口 | ➡️ | 本地方法库 | |
| | | | | | | |
| ---------------------------- ------------------ ----------------— |
|
| 其中,堆和方法区,是所有线程共有区;
| 栈,本地方法栈,程序计数器,是线程私有区。
|------------------------------------------------------------------------------------
(1) 内存区域
a.程序计数器(Program Counter Register),线程私有,不会抛出任何内存异常
I.可以这么理解,当前线程所执行字节码的行号指示器。字节码解释器,就是通过程序计数器的值,来选取下一条要执行的字节码指令(分支、循环、跳转等基础功能都需要依赖计数器)。
II.java虚拟机的多线程是通过,各个线程之间轮流切换并分配内存来实现的。在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换回来之后能够
恢复到正确的执行位置。每条线程都需要一个独立的程序计数器。
III.如果线程正在执行的是一个java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是native方法,计数器的值为空。
b.java虚拟机栈 (Java Virtual Machine Stack) , 线程私有,会有 StackOverFlow 和 OutOfMemoryError异常 ,通过-Xss分配内存大小
I.每个方法在执行时,都会创建一个栈帧(Stack Frame),用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
每一个方法从调用到执行完成的过程,就对应一个 栈帧 在虚拟机栈中,入栈到出栈的过程。
II.栈中的局部变量表,所需的内存空间,在编译器间完成分配。在进入一个方法时,这个方法需要在帧(Stack Frame) 中分配多大的内存空间是确定的,在这个方法运行期间,
不会 改变 局部变量表 所占用 内存空间 的大小。
III.当java启动一个线程时,虚拟机会计算出这个线程所需要的栈深度,(比如10),当线程请求的栈深度(每调一个方法压一个栈帧,用掉一个栈深度),大于虚拟机给Stack分配的
栈深度,会抛出StackOverFlow异常。(用javap javap -verbose Test 查看程序的字节码,Code 属性, stack=2 , 可以查看运行的详细过程,包括栈深度,和每个栈帧需要多少个slot)
当一个可扩展栈(栈有可扩展的有固定长度的,由使用的JAVA虚拟决定的),动态扩展时,无法请求到足够的内存(比如我需要10M内存,但是JVM只给我5M),会抛出,
OutOfMemoryError异常。
c.本地方法栈 (Native Method Stack) ,线程私有,会有 StackOverFlow 和 OutOfMemoryError异常,通过-Xss分配内存大小
I.和java虚拟机栈基本一样。区别不过是,
JVM Stack 为 虚拟机 执行java方法 服务;
Native Method Stack 为 虚拟机 执行本地方法服务
II.也会抛出StackOverFlow 和 OutOfMemoryError异常
d.java堆 , 线程共享 , 会抛出OutOfMemoryError异常。,通过-Xms分配内存最小值,-Xmx分配内存最大值
I.存放 对象实例 和 数组。
II.是垃圾回收的主要区域。
III. java堆,可以处于物理上的不连续空间,逻辑上连续即可。可动态扩展,通过-Xms控制大小。
IV.如果堆中没有完成内存分配,并且堆也无法扩展是,将会抛出OutOfMemoryError异常。
e.方法区 , 线程共享 , 会抛出OutOfMemoryError异常。
I.用于存储,已经被虚拟机加载的,类的信息、常量、静态变量、编译后的代码等数据
II.不需要连续的内存,可以选择固定大小和可扩展
III.当方法区无法完成内存分配需求时,会抛出OutOfMemoryError异常。
e-slave. 运行时常量池,会抛出OutOfMemoryError异常。
是方法区的一部分。
I.Class文件中,除了有类的 版本、 字段、方法、接口、等描述信息外,还有一项就是常量池(Constant Pool Table),用于存放编译期生成的,
各种字面变量和符号引用(),这部分将在类加载后进入方法区的常量池。
II.并非预置在Class文件中常量池中的内容,才能进入方法区;运行期间也可能将新的常量放入池中。
f.直接内存,并不是java虚拟机内存的一部分,而是机器内存的一部分,会抛出OutOfMemoryError异常
I.NIO引入了一种类似于 通道(Channel) 和 缓冲区(Buffer) ,可以使用Native函数库直接分配堆外内存。
然后,通过一个存储在java堆中的,DirectByteBuffer对象作为这块内存的引用进行操作。
这样避免了在java堆和native堆中来回复制数据,提高了性能。
II.当直接内存和JVM内存之和大于机器内存时,抛出OutOfMemoryError内存。
(2)对象创建细节
a.内存分配方式
I.指针碰撞, 如果java堆中内存是绝对规整的,为对象分配空间的任务,等同于把一块确定大小的内存从java堆中划分出来。
如果java堆中内存是绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为临界点,那么分配内存就是,
把指针向空闲空间那边挪动等同于对象大小的距离,这种分配方式成为 指针碰撞。
II.空闲列表, 如果内存是不规则的,虚拟机就必须维护一个表,记录那些内存块是可用的,分配的时候找到一块足够大的内存块分给对象实例,
并更新列表的记录,这种方式称为 空闲列表(Free List)
b.选择哪种分配方式是java堆是否规整决定的,java堆是否规整,是由采用的垃圾收集器是否带有压缩功能决定的。因此,
使用Serial、ParNew灯光带有压缩(Compact)过程的收集器时,系统采用的分配算法是指针碰撞。
使用CMS这种基于 Mark-Sweep(标记-移除) 算法的收集器,系统采用的分配算法是空闲列表。
c.空闲列表问题,及解决方案
问题:
首先,堆是线程共有的,所以,当多线程创建对象是,有这样一个问题,当Thread A分配一块内存完成后,还没更新列表,这时Thread B给
自己的对象分配了同一块内存,这就造成了冲突。
方案:
I.对分配内存的动作,进行同步,这种造成性能下降。
II.每个线程在堆中,预先分配一小块内存作为缓冲区,称为(Thread Local Allocation Buffer , TLAB) ,哪个线程需要给自己的对象分配
内存,就在自己的TLAB上分配,只有自己的TLAB上分配完了,才需要同步锁定。通过-XX:+/-UseTLAB参数来设定。(性能调优)
d.内存分配完成后,虚拟机将分配到的内存空间初始化为零值。这一步操作保证了Java代码中可以不赋初始值就可以使用
e.接下来,虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、对象的hash-code、对象的GC分代年龄,等信息。存在对象头中。
这样从JVM的角度来说,对象创建完成。
f.对象的内存布局,对象在内存中的存储可以分为3块区域:
I.对象头 (Header) : 包括两部分信息,
第一部分,存储对象自身运行时数据,如HashCode,GC分代年龄,锁定状态标志,线程持有的锁。
第二部分,类型指针,即对象指向它的 类 元数据的指针,虚拟机通常用这个指针确定对象属于哪个类。
还有记录数组长度的信息。
II.实例数据 (Instance Data) : 程序定义的个字段的内容。包括父类和子类。
III.对齐填充 (Padding) : 占位符,换句话说,就是保证对象大小必须是 8字节(byte) 的整数倍
g.对象的访问定位
java程序需要使用栈上面的reference,引用数据来操作堆上的具体对象。
I. 句柄
这种方式,Java堆中会分配一块内存,作为句柄池,reference存储的就是对象句柄池地址。
句柄中包含了对象实例数据 (在堆上),和类型数据(类数据,在常量池)具体地址信息。
好处:GC后reference不需要修改
II.直接指针
reference存储的就是对象地址。
好处:速度快,节省了一次指针定位开销。
Sun HotSpot使用直接指针
(3)堆溢出,OutOfMemoryError 后面跟 Heap
-Xms 和 -Xmx 设置堆的最大和最小内存
堆的最小参数 -Xms 和 最大参数 -Xmx 设置为一样,就可以避免堆扩展。
a.解决思路:
I.用内存映像分析工具(如,Eclipse Memory Analyzer) 堆Dump出来的堆转储快照进行分析。
II.分析的重点是确认内存中的对象是否是必要的,即先确认是否有 内存泄漏(Memory Leak,当创建
的对象没有使用,又无法被GC回收,就是内存泄漏)
III.如果是内存泄漏,查看泄漏对象到GC Roots的引用链信息,就能找到泄漏对象是通过怎样的路径与GC Roots相关联,
并导致GC无法自动回收他们的。通过引用链信息,定位到泄漏代码的位置,review代码。
IV.如果没有内存泄漏,即,内存中的对象都必须存活。那就看虚拟机堆参数(-Xms和-Mmx)和内存相比,看是否还可以调大。
从代码上检查是否有,某些对象生命周期过长,持有时间过长的情况,尝试优化这些代码,从而减少运行期的内存消耗。
(4)栈溢出 StackOverFlow
-Xss设置栈占用内存大小。默认1024K,也就是1M
a.虚拟机启动时,有栈大小的默认参数,当所有的栈帧(Stack Frame),内存加起来超过栈内存大小时,就会抛出StackOverFlow异常。
在栈深度,默认情况下,大多数栈深度达到1000-2000帧没有问题,对于普通递归是够用了(但是栈帧大小是不确定的,所以,只能是大多数情况下。)
b.建立线程数量过多,导致内存溢出
I.操作系统,分配给每个进程的内存是有限制的。如果给一个java分配了1G内存,
虚拟机提供了参数,来控制堆和方法区所占用内存大小,如果没有指定栈占用的内存大小,忽略其它,剩余的内存 1G - 堆内存 - 方法区内存,被本地方法栈和虚拟机栈
瓜分,栈是线程私有的,栈分配的内存越大,可以建立的线程数就越少,建立新线程时候,容易把剩下的内存耗尽。这种情况,可以减少最大堆,和减少栈容量,换取更多
的线程,避免内存溢出。
(5)方法区,内存溢出 。OutOfMemoryError后面跟随PermGen
-XX:PerSize 和 -XX:MaxPermSize限制方法区大小
String.intern()是一个native方法,作用:如果字符串常量池中,已经包含一个等于此String 对象的字符串,则返回常量池中,代表此字符串的对象。
否则,将此String对象添加到常量池中。
a.Spring Hibernate在对类进行增强时,都会使用到CGLib这类字节码技术,增强的类越多,就需要越大的方法区,容易导致方法区的内存溢出。
b.JSP第一次运行时,要编译成java类,大量的jsp也有可能导致方法区内存溢出。
(6) 本机直接内存溢出 OutOfMemoryError Unsafe.allocateMemory
DirectMemory 容量可以通过:-XX:MaxDirectMemorySize指定,如果不指定,则默认与java堆最大值 (-Xmx)一样。
如果内存溢出,在堆的Dump文件很小,或者没有明显的异常,又或者程序中使用了NIO,可以考虑是 本机直接内存溢出。