深入理解Java虚拟机 读书笔记

时间:2022-12-27 17:00:23

深入理解Java虚拟机 读书笔记

深入理解Java虚拟机 读书笔记

Java的内存区域

  • 程序计数器,就是操作系统里面的PC指针,指向当前执行的代码的地址,线程私有。
  • Java虚拟机栈,线程私有,每个线程对应一个栈,每个方法对应一个栈帧,一个方法的执行就是出栈入栈的过程。
  • Java堆,线程共享的,用于存放对象的实例,垃圾收集管理的主要区域,大部分GC采用分代收集法,所以GC区分为新生代和老年代。(更详细的垃圾回收部分后面会讲)
  • 方法区,所有线程共享的,存储已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等。
  • 运行时常量池,方法区的一部分,编译期生成的各种字面量和符号引用。
  • 直接内存,NIO,基于通道与缓冲区的IO方式,使用native函数库直接分配堆外内存,通过Java堆中的DirectByteBuffer作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回的复制数据。

Java对象的创建

根据new关键字后面的参数定位到常量池中的符号引用,然后检查类加载没,没加载就加载了,加载后就知道对象要分配的空间大小了,然后根据要分配的空间大小从Java堆上划出来一块。如果Java堆是规整的,就是使用“指针碰撞”法,分配内存就把指针往后移一下。如果Java堆是不规整的,空闲的和使用的交错放置的,就使用“空闲列表”法。使用哪种方法取决于GC,如果GC带压缩整理,垃圾回收的时候能顺便把内存归拢起来就指针碰撞法,否则就空闲列表。
同时,对象创建的过程在并发情况下并不是线程安全的,如果一个指针还没来得及挪,另一个就挪好了,然后这个指针记录了错误的位置,这种情况就GG了。所以有两种解决的办法,其实本质上都是一种,就是同步锁定(CAS和失败重试详见http://blog.csdn.net/hsuxu/article/details/9467651),第一种是每一次new的时候都进行同步锁定。第二种是搞了个本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),每个线程有一小块缓冲,这样一般情况下分配内存的时候可以直接从缓冲取缓冲一下,只有在TLAB用完并重新分配的时候同步一下。其实本质上第二种方法也是同步,只不过减少了同步使用的次数。
内存划出去以后,再执行设置对象的类的元信息,哈希码,GC分代年龄等信息,放到对象头(Object Header)中。最后执行对象的构造函数,香喷喷热腾腾的对象就做好了。

对象的内存布局

对象头,实例数据,对齐补充
对象头两部分,一部分是对象自身运行的数据,Hash码、GC分代年龄、锁状态标志。。。“Mark Word”。另一部分是指向类元数据的指针,表明它是哪个类的实例,如果是数组,还会有一个数组长度的数据。
实例数据,就是干货,就是java里面写的那些东西,无论是父类继承下来的,还是子类定义的,按照虚拟机分配策略参数和java代码里面的定义顺序排序。
对齐补充,HotSpot对象的起始地址规定是8字节的整数倍,所以对象的大小必须是8字节的整数倍,所以需要对齐补充。

对象的访问定位

Java程序是通过栈上的reference数据来操作堆上的具体对象,由reference定位访问到堆上对象的方法取决于虚拟机的设定,主流的访问方式有使用句柄和直接指针两种。
深入理解Java虚拟机 读书笔记
使用句柄需要在Java堆中划出来一块内存放句柄池,然后通过句柄池访问实例和方法区中的对象类型数据。
深入理解Java虚拟机 读书笔记
直接指针访问就是直接指到Java堆对象实例所在的位置,然后对象实例所在的位置会有个指针指向方法区的对象类型数据。
使用句柄的好处是方便归拢,比如垃圾回收时经常会移动对象,只需要改变句柄中的实例数据的指针即可,reference本身不需要修改。
使用直接指针的好处就是一个字“快”,速度更快,没有那么多屁事,不需要绕弯子,reference直接怼到对象实际的位置上,对象访问那么频繁,这样可以大大的提速。Sun HotSpot就是用的这种方式。

实战之——OutOfMemoryError异常(传说中的OOM)

除了程序计数器以外,其它的几个虚拟机内存运行时区域都有可能发生OOM

Java堆溢出

只要不断的创建对象,并且保证GC Roots到对象之间有可达路径避免垃圾回收,那么对象数量到达最大堆的容量限制之后就会产生内存溢出异常。
示例代码:

/**
*
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author DC
*
*/

public class HeapOOM {

static class OOMObject{

}

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

使用内存映像分析工具,首先分析内存中的是否是必要的,也就是分清楚到底是内存泄露(Memory Leak)还是内存溢出(Memory Overflow)
如果是内存泄露,可以通过工具查看泄露对象到GC Roots的引用链。
如果不是内存泄露,也就是内存中的对象都必须活着,那么就是虚拟机参数设置小了,那么就修改参数(-Xms和-Xmx)

JVM对那些没有根引用的对象进行来及回收,也就是无法从根对象中追述的对象。

JVM垃圾回收的根对象的范围有以下几种:

1、栈中引用的对象,引用是在栈帧中的本地变量表中的,真正的对象在堆中

2、方法区perm中的类静态属性引用的对象,以及常量引用的对象

3、本地方法栈中JNI(Native方法)的引用的对象

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

虚拟机请求的栈深度大于虚拟机所允许的最大深度,抛出*Error异常。
扩展栈时没有申请到足够大的内存空间,则抛出OutOfMemoryError异常。
单线程模式下,无论是栈帧太大还是虚拟机栈容量太小,一般都只会抛出*Error异常。
多线程模式下,会产生OutOfMemoryError异常,为每个线程分配的栈越大越容易异常,因为除去堆区和方法区的部分如果不够线程瓜分,就会内存溢出,跟栈没关系。

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

方法区是用来存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
CGLIB(Code Generation Library)是一个代码生成的库,Spring的AOP和Hibernate的OR Mapping都是通过它来实现的。它可以实现动态的代理,比如AOP就是通过它来实现的。JDK自带的代理需要代理的对象实现一个接口,而使用了CGLIB的AOP就不需要这些东西,因为它的底层是一个小而快的字节码处理框架ASM,使用它可以把原来的class封装起来实现方法的拦截。