深入理解java虚拟机第二章笔记

时间:2022-12-28 12:58:32

第二章 java内存区域与内存溢出异常

一 运行时数据区域

1. 程序计数器:

是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。如果线程正在执行一个java方法,那这个计数器记录的是正在执行的虚拟机的字节码指令的地址(线程私有)。

2. java虚拟机栈(线程私有):

生命周期与线程相同。虚拟机栈描述的java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

局部变量表存放了编译器可知的各种基本数据类型,对象引用和returnAddress类型(指向了一条字节码指令的地址)。

3. 本地方法栈:

与虚拟机栈的区别是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机适用到的Native方法(用于调用非java代码)服务。

4.java堆(线程共享):

是被所有线程共享的一块内存区域,几乎所有的对象实例以及数组都要在堆上分配,也是垃圾收集器管理的主要区域,因此也被称为GC堆。

由于现在收集器都采用分代收集算法,所以java堆还可以分为新生代和老年代。再细致一点的有Eden空间,From Survivor空间,To Survivor空间等。

主流虚拟机的java堆是可扩展的,通过-Xmx和-Xms控制。

5.方法区(线程共享):

它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

6.运行时常量池:

是方法区的一部分,class文件中有一项信息是常量池,用于存放编译时生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

7.直接内存

二 HotSpot虚拟机对象

分配内存:

1.指针碰撞:堆内存规整,则只需要将指针从分配过的内存与未分配内存的交界处,向未分配内存方向移动与对象大小相等的距离

2.空闲列表:堆内存不规整,则需要记录分配与未分配的内存,分配时从未分配列表中找到一块足够大空间划分给对象实例,并更新列表

线程安全问题的解决方案:

1.对分配内存空间的动作进行同步处理(CAS比较转换配上失败重试)

2.把内存分配动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)

内存分配完成后,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。

对象的内存布局:对象头,实例数据,对齐填充

1.对象头:一部分存储对象自身运行时的数据(如Hash码,线程持有的锁等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是哪个类的实例。
2.实例数据:程序中所定义的各种类型的字段内容

3.对齐填充:对象大小必须是8字节整数倍,没对齐时需要补全

对象的访问定位:

1.句柄访问:java堆中划分出来一块内存作为句柄池,句柄中包含对象实例数据与类型数据各自的具体地址信息

2.直接指针访问:在java堆中存放对象实例数据以及到对象类型数据的指针,指向方法区中的对象类型数据。

三 OutOfMemoryError异常

1.Java堆溢出(例如,无限循环)

内存泄露是指你的应用使用资源之后没有及时释放,导致应用内存中持有了不需要的资源,这是一种状态描述而内存溢出是指你的应用的内存已经不能满足正常使用了,堆栈已经达到系统设置的最大值,进而导致崩溃,这事一种结果描述而且通常都是由于内存泄露导致堆栈内存不断增大,从而引发内存溢出

例如:限制java堆大小20M且不可扩展(最小值与最大值一样)

/**
 * -verbose:gc -Xms20M -Xmx20M -Xmn10M(新生代大小) -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * @author cream
 *
 */
public class HeapOOM {

	static class OOMObject{}
	
	public static void main(String[] args) {
		List<OOMObject> list = new ArrayList<>();
		
		while(true){
			list.add(new OOMObject());
		}
	}
}
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.Arrays.copyOf(Unknown Source)
	at java.util.ArrayList.grow(Unknown Source)
	at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
	at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
	at java.util.ArrayList.add(Unknown Source)
	at HeapOOM.main(HeapOOM.java:16)

2. 虚拟机栈和本地方法栈溢出

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error异常,如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

单个线程无论由于栈帧太大还是虚拟机栈容量太小,档内存无法分配时,虚拟机抛出的都是*Error异常,线程数与每个线程的栈分配的内存大小是相悖的,如果建立过多内存导致线程溢出,则只能通过减少最大堆和减少栈容量来换取更多线程

例如:

/**
 * VM Args: -Xss128K
 * @author cream
 *
 */
public class JavaVMStackSOF {
	private int stackLength=1;
	
	public void stackLeak(){
		stackLength++;
		stackLeak();
	}
	public static void main(String[] args) {
		JavaVMStackSOF oom = new JavaVMStackSOF();
		try {
			oom.stackLeak();
		} catch (Throwable e) {
			System.out.println("栈长度:"+oom.stackLength);
			throw e;
		}
	}
}

测试结果:

栈长度:988
Exception in thread "main" java.lang.*Error
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:6)
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)
	at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:7)...

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

Young(年轻代):主要是用来存放新生的对象。
Old(年老代):主要存放应用程序中生命周期长的内存对象。

Permanent(永久代):是指内存的永久保存区域,主要存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域. 

它和和存放Instance的Heap区域不同,GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS的话,就很可能出现PermGen space错误。

jdk1.8将永久代替换为元空间,元空间是直接存在内存中,不在java虚拟机中的,因此元空间依赖于内存大小。当然你也可以自定义元空间大小。为什么叫元空间,是因为这里面存储的是类的元数据信息

由于在jdk1.8中-XX:PermSize 和 -XX:MaxPermSize 已经失效,取而代之的是一个新的区域 —— Metaspace(元数据区)。参数替换为 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize

经过测试,书上的代码已经无法产生异常。1.8中的具体变化还需要深入研究。

4.本机直接内存溢出

参数设置:-XX:MaxDirectMemorySize