《深入理解Java虚拟机:JVM高级特性与最佳实践》读书笔记(一)

JVM

Posted by 朱坤乾 on May 1, 2019

收获颇多的一本书,非常值得细细品味。读过此书后,发现以前看过的网上好多JVM五花八门的文章,都是源自此书。 所谓思而不学则殆,其实,做而不学也怠了。这些年,写了很多代码,做了很多项目,却很少系统的看一本书。

作为一名Java开发人员,不能局限于Java语言规范,更需要对Java虚拟机规范有所了解。Java虚拟机规范有多种实现,其中HotSpot VM是Oracle JDK和Open JDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。

第一部分:走进java

Java虚拟机:

Java制定了比较宽松的虚拟机规范,各个厂商都可以做自己的虚拟机。

默认的是已经被Oracle收购的Hotspot

但IBM等公司在专业硬件上有性能更加优秀的专业商业虚拟机。

64位虚拟机

优势:突破32位虚拟机最多4G内存的限制。

劣势:

  1、JVM优化不够好,导致性能一定下降,不过这是暂时的,早晚被解决。

  2、内存大,导致的GC时间长。

第二部分:内存自动管理机制.1.内存区域

程序计数器(线程私有内存)

是很小的一块内存空间,当前线程所执行的字节码的行号指示器(字节码解释器工作就是通过改变这个计数器的值,来选取下一个需要执行字节码的操作)

分支,循环跳转,异常处理,线程回复等都是程序计数器来做的

由于java虚拟机的多线程是通过线程轮流切换,并分配处理器执行时间来实现的,在任何时刻一个处理器只会执行一个线程 因此为了线程能切换到正确的执行位置,,每条线程都需要独立的程序计数器 各个线程之间的程序计数器互不影响,独立存储,因此称之为线程私有内存

唯一不会OutOfMemoryError的区域

java虚拟机栈(线程私有)

生命周期和线程相同. 描述的是java方法执行的时候内存模型:::每个方法被执行的时候都会创建一个栈帧,,用于存储局部变量表,操作栈,动态链接,方法出口.每一个方法调用至方法执行完的过程,就对应一个栈帧在虚拟机栈中从入栈到出栈的过程

经常有人把内存分为堆内存和栈内存,这样是粗糙的.java的内存分远远比这个负载的多了 这样分是因为程序员最关注的与对象分配关系最密切的就这两块. 其中的栈就是值得是虚拟的栈,或者栈中的局部变量表

局部变量表存放:::基本数据类型,对象的引用(不等同于对象本身) 64位的long和double会占用两个局部变量空间,其余数据只占用一个变量空间

这个区域有两种异常:::

1:stackOverflowError 如果线程请求的栈深度大于虚拟机所允许的深度 2:OutOfMemoryErroy 如果虚拟机的栈可以动态扩展(大部分虚拟机可以扩展,)当扩展到无法申请到内存就会抛出异常.

本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

归属:线程

会StackOverflowError,会OutOfMemoryError

Java堆

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

所有的对象实例以及数组都要在堆上分配 。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆” .

堆是垃圾收集器的主要管理区域. 很多时候被称为GC堆,基本采用分代收集算法

新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1

老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

java堆处于物理不连续的内存空间,只要是逻辑上是连续的就可以

归属:进程

会OutOfMemoryError

方法区(别名非堆,与堆区分开)

线程共享,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent Generation),主要存放java类定义信息,与垃圾回收关系不大,但不是没有垃圾回收,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。运行时常量池,方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池。

方法区无法分配除足够的内存时将抛出Outofmemory异常

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

垃圾处理机制同”永久代”

归属:进程

会OutOfMemoryError

直接内存

使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。(NIO的DirectByteBuffer)

本机直接内存的分配不会受到Java堆大小的限制。

归属堆外

会OutOfMemoryError

运行时常量池

是方法区的一部分 Class文件除了接口版本等之外还有一项就是常量池,存放编译期生成的字面量和符号引用,这部分内容将在类加载后存放到方法区运行时常量池中.

第二部分:内存自动管理机制.2.HotSpot虚拟机对象探秘

对象的创建过程

1、加载类

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

2、分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。 对象所需内存的大小在类加载完成后便可完全确定。 为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

分配方式:

1、指针碰撞。适用于连续内存,需要垃圾回收有整理功能。

2、空闲列表。不需要连续内存,可用于标记-清理的垃圾回收功能。

指针操作同步问题的解决方式,则也是一般的同步问题的解决方式:

1、CAS+失败重试来保证原子性

2、线程隔离,即为每个线程分配一块单独的区域,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。

虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

3、内存归零

不包括对象头

4、对象头设置

5、构造函数

虚拟机部分已经完成,开始对象自定义部分。

对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

1、对象头

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、 GC分代年龄、 锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。

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

如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。

2、实例数据

存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配策略为longs/doubles、 ints、 shorts/chars、bytes/booleans、 oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段 总是被分配到一起。

3、对齐填充

对象的定位访问

Java程序需要通过栈上的reference数据来操作堆上的具体对象。 由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、 访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现而定的。

目前主流的访问方式有使用句柄和直接指针两种。

1、句柄

使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改

2、指针

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。

HotSpot是使用第指针方式进行对象访问的

内存溢出

Java堆溢出

java.lang.OutOfMemoryError:Java heap space

不断创建对象,保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,达到最大堆的容量限制后就会产生内存溢出异常。
-Xms20m 堆的最小值;-Xmx20m 堆的最大值;-XX:+HeapDumpOnOutOfMemoryError  内存溢出异常时Dump出当前的内存堆转储快照以便日后分析

通过查看引用链确定溢出原因:

内存合理的大:内存溢出 调大内存,或调小业务内存模型

内存不合理的大:内存泄漏 解决问题

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

关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:

如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

线程过多,会导致内存溢出。

Xss 栈容量

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

Exception in thread”main”java.lang.OutOfMemoryError:PermGen space

方法区溢出也是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。

多次调用String.intern()方法可以产生内存溢出异常。JDK 1.6之间,可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制永久代大小,从而达到限制方法区大小的目的

本机直接内存溢出

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值(-Xmx指定)一样

由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果读者发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查一下是不是这方面的原因

可以通过 -XX:MaxDirectMemorySize 指定。如果不指定,则默认和Java堆最大值(-Xmx 指定)一样

总结

通过这些学习明白虚拟机是如何划分的,那部分的区域,什么样的代码和操作可能导致内存溢出.虽然java有内存溢出机制,但内存溢出异常离我们并不遥远. 这些知识讲解了各个区域内存溢出的原因,之后将讲解java垃圾回收机制为了避免内存溢出做了哪些贡献