《深入理解Java虚拟机》读书笔记:第二章Java内存区域与内存溢出异常

时间:2021-08-14 00:42:24

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域:方法区、虚拟机栈、本地方法栈、堆、程序计数器

程序计数器(ProgramCounterRegister):一块较小的内存空间,看作当前线程所执行的字节码的行号指示器;字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储(线程私有)

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的

 

Java虚拟机栈(JavaVirtual Machine Stack):描述Java方法执行的内存模型 -- 每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息

局部变量表存放了编译器可知的各种基本数据类型,对象引用和returnAddress类型,局部变量表所需的内存空间在编译期间完成分配

  • 基本数据类型(boolean,byte,char,int,long,double,float其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余1个)
  • 对象引用(reference,不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是一个代表对象的句柄或其他与此对象相关的位置)
  • returnAddress类型(指向了一条字节码指令的指令) 

本地方法栈(Native Method Stack):虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。两者都会*Error(内存溢出,)和OutOfMenoryError(内存不足)

*有的虚拟机把本地方法栈和虚拟机栈合二为一  

 

Java(JavaHeap):堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,唯一目的是存放对象实例(分配对象实例以及数组),是垃圾收集器管理的主要区域

堆中细分: 新生代,老年代 ;新生代再细分 Eden,From Survivor,To Survivor空间;默认 Eden:Survivor=8:1

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将抛出OutOfMenoryError

方法调用嵌套层数过大引起 *Error

 

方法区(Method Area):各个线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,即编译器编译后的代码等数据.如类名,访问修饰符,常量池,字段描述等

方法区无法满足内存分配需求时,将抛出OutOfMemoryError

运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

 

直接内存(Direct Memory):并不是虚拟机运行时数据区的一部分,而是本机直接内存.在NIO中,通过Native(本地)函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native堆(Java堆之外的本机内存)中来回复制数据

  

对象的创建(HotSpot为例)

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载,解析和初始化过.如果没有,那必须先执行相应的类加载过程

  • 符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法

类加载检查通过后,接下来虚拟机将为新生对象分配内存

内存分配的方式:

  1. 指针碰撞(堆中的内存规整,中间一个指针作为分界点两边是用的和空闲的内存);
  2. 空闲列表(虚拟机维护一个列表记录内存可用,从列表中查找)

*Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

 

对象的创建十分频繁,并发下非线程安全 .解决方法: 1.同步处理分配内存空间的动作;2.把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

 

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,接下来对虚拟机对对象进行必要的设置,将类的元数据信息,对象的哈希码,对象的GC分代年龄等存放在对象的对象头.此时一个新的对象已经产生,但<init>方法(初始化)还没执行,所有字段都是零.执行new 指令后接着执行init方法进行初始化

 

对象在内存中存储的布局分为3块区域:对象头(Header),实例数据(InstanceData)和对齐填充(Padding)

对象头一部分存储对象自身的运行时数据(称为Mark Word),如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向锁ID,偏向时间戳;

另一部分是类型指针,即对象指向它的类元数据的指针.虚拟机通过这个指针来确定这个对象是哪个类的实例

虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小;如果对象是一个Java数组,对象头中还必须有一块用于记录数组长度的数据

 

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承的还是在子类定义的

这部分存储顺序受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响

HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oops    相同宽度的字段总被分配到一起

 

对齐填充并不是必然存在的,仅仅起着占位符的作用HotSpotVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,而对象头部分是8字节的倍数,当实例数据部分没有对齐时就需要对齐填充部分来补全

 

Java程序通过栈上的reference数据来操作堆上的具体对象.reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位,访问堆中的对象的具体位置.所以对象的访问方式取决于虚拟机实现,主流的访问方式有使用句柄和直接指针两种

  • 句柄:Java堆中划分一块内存作为句柄,reference中存储的就是对象的句柄地址.句柄中包含了对象实例数据与类型数据各自具体的地址信息(可以把句柄看作是对象的索引)
  • 直接指针:reference中存储的直接就是对象地址(比上面少了一层,索引只在reference中)

《深入理解Java虚拟机》读书笔记:第二章Java内存区域与内存溢出异常

两者各有优势:使用句柄在对象被移动时,不改变reference而只改变句柄中的实例数据指针;而使用直接指针可以节省一次指针定位的时间开销 

 

Java堆内存溢出异常时,异常堆栈信息 java.lang.OutOfMemoryError: Java heap space,会进一步提示堆空间

解决:通过内存映像分析工具(eclipse插件安装http://download.eclipse.org/mat/1.6.1/update-site/,或程序http://www.eclipse.org/mat/downloads.php ) 对Dump(备份)出来的堆转储快照分析,确认是内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)

使用MemoryAnalyzer tool(MAT)分析内存泄漏可参考http://www.blogjava.net/rosen/archive/2010/06/13/323522.html

一些JVM参数

-Xmx (指定JVM堆的最大内存,在JVM启动以后,会分配-Xmx参数指定大小的内存给JVM,但是不一定全部使用,JVM会根据-Xms参数来调节真正用于JVM的内存

-Xms (指定了JVM初始启动以后初始化最小堆内存);例如 -Xmx5m

-XX:+HeapDumpOnoutOfMemoryError(让虚拟机在出现内存溢出异常时备份出当前的内存堆转储快照)

 

虚拟机栈和本地方法栈溢出:线程请求的栈深度大于虚拟机所允许的最大深度,将抛出*Error

虚拟机在扩展栈时无法申请到足够的内存空间则抛出OutOfMemoryError,两者本质上都是内存太小

-Xss:栈内存大小

 

JDK1.6运行时常量池溢出:在OutOfMemoryError后面跟随的提示信息是"PermGen space",(运行时常量池属于方法区 永生代)

Java8 删除了Hotspot JVM中的永生代内存(PermGen,永生代内存主要存储一些需要常驻内存,不会被回收的信息),改为使用本地内存来存储类的元数据信息,并将之称为元空间(Metaspace),也就不会遇到java.lang.OutOfMemoryError:PermGen错误

参数: -XX:PermSize=64m  -XX:MaxPermSize=128m 需变成 -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m

关于常量池的一个有趣的例子

public static void main(String[] args) {

//intern返回常量池中记录首次出现的实例
//String.intern()方法 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1); //true

String str2 = new StringBuilder("ja").append("va").toString(); //其他像int,float,double,byte等也是将会返回false
System.out.println(str2.intern() == str2); //false StringBuilder.toString之前,字符串常量池里面已经有了java这个字符串,不是首次出现
}

本机直接内存溢出:直接内存(Direct Memory)通过 -XX:MaxDirectMemorySize指定,不指定则默认与Java堆最大值一样,明显特征是Heap Dump文件中不会看到明显的异常

Netty中的一个直接内存泄漏的例子  https://issues.jboss.org/browse/NETTY-424?_sscc=t