深入理解JAVA虚拟机读书笔记----Java内存区域与内存溢出异常

时间:2022-12-29 09:14:41

1、为什么需要了解它?

java源码是由C++和少量的C编写的。JAVA与C++之间很大的不同点之一是:C++开发人员在内存管理领域,即对没有对象拥有绝对的控制权,也必须对每个对象的每个整个声明周期负责;而JAVA开发人员在虚拟机的自动内存管理机制的帮助下,不再需要处理对象的销毁,不容易出现内存泄漏和内存溢出问题,但是,一旦出现了内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查将异常困难。

2、Java虚拟机所管理的内存会包括以下几个运行时的数据区域

深入理解JAVA虚拟机读书笔记----Java内存区域与内存溢出异常

2.1程序计数器

Java虚拟机的多线程是由线程轮换的方式来切换的。任何时刻,一个处理器(多核中的一个内核)只能执行一条指令。程序计数器可以记录每个线程的执行的位置,这些内存区域为“线程私有”的。

2.2Java虚拟机栈(此处指虚拟机栈中局部变量表的部分)

Java虚拟机栈也是“线程私有”的,声明周期与线程相同。虚拟机栈方的是Java方法执行的内存模型:每个方法执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表存放了编译器可知的各种基本数据类型、对象引用和returnAddress。局部变量表在编译期间内存空间的大小就分配完毕了。
此处有两个异常:*Error(请求的栈深度大于虚拟机允许的深度),OutOfMemoryError(虚拟机栈一般可以动态扩展,如果扩展无法申请足够的内存)

2.3本地方法栈

与Java虚拟机栈发挥作用基本一致。区别是:Java虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。

Java方法和Native方法区别:
1、JAVA方法是由JAVA编写的,编译成字节码,存储在class文件中
本地方法是由其它语言编写的,编译成和处理器相关的机器代码
2、本地方法保存在动态链接库中,即.dll(windows系统)文件中,格式是各个平台专有的
JAVA方法是与平台无关的,但是本地方法不是

2.4Java堆

一般来说,Java堆(Heap)是虚拟机管理的内存中最大的一块。Java堆为所有线程共享的一块内存区域,在虚拟机启动是创建,唯一目的就是用来存放对象示例,几乎所有对象实例都在这里分配内存。
Java堆是垃圾收集器管理的主要区域,因此很多时候被叫做“GC堆”(Garbage Collected Heap)。
主流虚拟机的堆大小可以通过(-Xmx -Xms)来配置,如果内存不够分配实例,而且堆不够扩展时,会抛出OutOfMemoryError

2.5方法区

也是线程共享的区域。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2.6运行时常量池(Constant Pool Table)

是方法区的一部分。Class文件除了类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
另外,兼备动态性,常量并非只能编译器产生,运行期间也可以将常量放入池中。
http://blog.sina.com.cn/s/blog_69dcd5ed0101171h.html

2.7直接内存

这并不属于Java虚拟机运行时数据区的一部分,但会频繁使用。

3、hotspot虚拟机对象探秘

3.1对象的创建

在语言层,就是一个new就创建成功,在虚拟机中会怎样?

a、类加载检查:遇到new命令,检查new指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经加载过了、解析、初始化过了。如果没有,则先执行相应的类加载过程。

b、分配内存:通过类加载检查后,虚拟机为其分配内存,对象所需内存大小在类加载完成后便可完全确定。

如何分配内存?第一种,java堆中的内存绝对规整,用过的内存在一边,没用过的在另一边,中间放一个指针来表示分界线,分配的时候挪动要分配的对象大小距离即可。第二种,如果java堆中的内存在乱放着,已使用和空闲内存相互交错,虚拟机就必须维护一个列表“空闲列表”,列表上记录哪些内存块是可以用的。 注:选择哪种方法取决于java堆是否规整,而java堆是否规整取决于垃圾收集器是否带有压缩整理功能来决定。
并发分配内存的安全问题?如果创建对象非常频繁,即使仅仅修改一个指针所指的位置,也不是线程安全的。两种解决方案:第一种,对分配空间的动作进行同步处理操作,实际上,虚拟机采用CAS+失败重试方法来保证。第二种,把内存分配的动作按照线程划分到不同的空间之中进行,哪个线程要分配内存,就在哪个线程的TLAB(本地线程分配缓冲Thread Local Allocation Buffer)上分配,只有TLAB用完需要分配新的TLAB时,才需要同步锁定,虚拟机是否使用TLAB可以使用-XX:+/-UseTLAB参数设定。

CAS(Compare and Swap)算法简介:

JDK1.5之前都是靠synchronized关键字保证同步的,线程的挂起和恢复比较频繁,开销比较大。
独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
乐观锁的核心算法是CAS(Compareand
Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
① 观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
② 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
③ ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。

c、初始化零值
如果使用TLAB,这一步可以提前到分配TLAB时进行。
d、对对象头进行必要的设置
例如:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的CG分代年龄等信息。不同虚拟机会存放其他不同的信息。
此时,从虚拟机的角度来说,对象已经创建完成,但从java程序中来看,对象创建才刚刚开始,init方法还得执行。

3.2、对象的内存布局

还对象在内存中的布局分为三个区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)

对象头包含两部分信息:一部分是用于存储对象自身的运行时数据,例如:哈希码/GC分代年龄/锁状态标志/线程持有的锁/偏向线程ID/偏向时间戳等,这部分数据长度虽然有规定但其实并未按照固定长度来,通常的JVM实现都是在极小的空间内存储更多的信息;另一部分是类型指针,即对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,有些虚拟机没有这个,他们用另外的方法确认类信息。另外,如果对象是一个java数组,通常会存一个数组的长度信息。
实例数据是对象真正存储的有效信息,也就是各种类型的字段的内容。
对齐填充就是用来占位的,因为HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,所以只能每个对象的大小也就是8自己的整数倍才行。

3.3、对象的定位访问

指栈上的reference数据如何找到堆上真正的对象内容。
一种方法是使用句柄:在java堆中划出一块内存来放句柄池,句柄池中放着对象的指针和对象的类型指针,所以reference只要指向句柄池的位置即可。采用这种方式的话,对象的头信息就不用存储类型信息了,句柄信息就包含了此信息;还有个好处是如果对象被移动的话,秩序更改句柄信息即可,reference信息不用变。
另一种是直接指针访问:reference指向堆中的对象地址,而对象的头信息包含了类型信息。这种方式的好处是速度快,少了一次指针定位的开销。

4、后面练习:

java堆溢出:

不断地创建对象并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,到达最大容量后就会溢出。

package com.leo.c02;

import java.util.ArrayList;
import java.util.List;

/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* -Xmx3550m:设置JVM最大可用内存为3550M.
-Xms3550m:设置JVM促使内存为3550m.此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
-XX:+HeapDumpOnOutOfMemoryError,JVM会在遇到OutOfMemoryError时拍摄一个“堆转储快照”,并将其保存在一个文件中。
-XX:HeapDumpPath=E:\Java\dump,设置导出对存储快照的路径,默认再项目目录下
* 会生成一个类似 java_pid15900.hprof 的文件,我用jdk安装目录下的 java visualVM 打开它,查看了他的结构
* @author xuexiaolei
* @version 2017年09月03日
*/

public class HeapOOM {
static class OOMObject {
}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();

while (true) {
list.add(new OOMObject());
}
}
}

虚拟机栈和本地方法栈溢出:

如果线程请求地栈深度岛屿虚拟机所允许地最大深度,就会抛出*Error,所以用一个无限递归的的方法来溢出

package com.leo.c02;

/**
* VM Args:-Xss128k
* Created by LEO on 2017/9/3.
*/

public class JavaVMStackSOF {
private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}

当扩展栈时无法申请到足够的内存空间,将抛出OutOfMemoryError

package com.leo.c02;

/**
* VM Args:-Xss2M (这时候不妨设大些)
* Windows会假死,谨慎运行
* Created by LEO on 2017/9/3.
*/

public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}

public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}

public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}

方法区和运行时常量池溢出:

借助CGLib字节码技术,增强的类越多,就需要越大的方法区来保证动态生成的class可以加载入内存

package com.leo.c02;


import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
*
* 引入以来的jar包
* <dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
* Created by LEO on 2017/9/4.
*/

public class JavaMethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj, args);
}
});
enhancer.create();
}
}
static class OOMObject {

}
}

本机直接内存溢出:

借助Unsage类的分配内存方法allocateMemory来分配直接内存,一直分配就出现了OOM

package com.leo.c02;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* Created by LEO on 2017/9/4.
*/

public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}

//这种类型的结果就要考虑直接内存的溢出了
//Exception in thread "main" java.lang.OutOfMemoryError
//at sun.misc.Unsafe.allocateMemory(Native Method)
// at com.leo.c02.DirectMemoryOOM.main(DirectMemoryOOM.java:19)