深入理解JAVA虚拟机学习笔记
深入理解JAVA虚拟机学习笔记
1、对象的创建过程
这里以new关键字来创建对象。
在虚拟机的常量池区域定位对应类的符号引用。
判断这个符号引用代表的类是否被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在堆中分配对象内存,内存的大小在类加载后便已完全确定。分配方式有两种:指针碰撞、空闲列表。具体采用哪一种取决于堆内存是否规整。
关于分配时的并发安全:虚拟机采用CAS+失败重试的方式保证并发分配内存,如果开启了TLAB参数(本地线程分配缓冲区),则哪个线程要分配内存,就在哪个线程的TLAB上进行分配,只有当TLAB区域用完时,才对堆内存进行同步锁定。
内存分配完成后,对分配到的内存空间进行初始化(例如相关数据类型的默认值),然后对对象头中的描述信息进行必要的设置(例如对象的哈希码、GC分代年龄、类的元数据信息等等)。
在上面的工作都完成后,从虚拟机的视角来看,一个新对象已经产生了,但站在JAVA程序的角度,对象创建才刚刚开始,接下来需要把对象按照程序员的意愿进行初始化(比如执行一些构造方法,init方法等等),这样一个真正可用的对象才算完全产生出来。
2、对象的内存布局
对象的内存布局分为三块:对象头、实例数据区、对齐填充数据区。
对象头:对象头存储分为两部分数据,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
如果对象是一个Java数组,那在对象头中还有一块用于记录数组长度的数据。
实例数据区:实例数据区是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
对齐填充数据区:并不是必然存在的,也没有特别的含义,它仅仅起到占位符的作用。HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,当对象实例数据部分不是8字节的整数倍时,就需要通过对齐填充来补全。
3、对象的访问定位
Java程序需要通过栈上的reference数据来操作堆上的具体对象。reference类型在Java虚拟机规范中只规定了一个指向对象的引用,
因此最终的对象访问方式是取决于虚拟机实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
使用句柄
使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。这种方式最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。
使用直接指针
使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。这种方式最大好处就是速度更快,它节省了一次指针定位的时间开销,
4、虚拟机中有哪些异常
在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(下文称OOM)异常的可能。
如果堆中的对象分配足够多,会导致堆内存溢出OutOfMemoryError异常。
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那么可能是本机直接内存溢出。
方法区和运行时常量池也可能会发生溢出。
5、判断对象是否存活的方法
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。
可达性分析法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
可以作为GCROOT的对象有以下几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
6、垃圾收集器和算法
垃圾收集器:
Serial收集器:单线程收集器,发展历史最悠久的收集器,用于新生代区域垃圾收集。是虚拟机运行在Client模式下的默认新生代收集器
ParNew收集器:其实就是Serial收集器的多线程版本。是许多运行在Server模式下的虚拟机中首选的新生代收集器。目前只有它能与CMS收集器组合使用。
Parallel Scavenge收集器:新生代收集器,它也是使用复制算法的收集器。特点是它的关注点与其他收集器不同,它的目标则是达到
一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Serial Old收集器:是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供。
CMS收集器:是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。基于标记-清除算法,步骤分为:初始标记-并发标记-重新标记-并发清除。 这个收集器存在的问题就是:产生空间碎片、浮动垃圾
G1收集器:是当今收集器技术发展的最前沿成果之一,是一款面向服务端应用的垃圾收集器。特点是分代收集、内存化整为零(把Java堆分为多个Region后)。 步骤分为:初始标记、并发标记、最终标记、筛选回收。
垃圾收集算法:
- 复制算法
- 标记-整理算法
- 标记清除算法
- 分代收集算法
7、安全点和安全区域
何时发起GC就涉及到安全点和安全区域的概念了。
安全点:在HotSpot的解决方案中,是使用一组称为OopMap的数据结构来存放这些对象的引用(OopMap在类加载动作完成时生成),而JVM就是从这些引用中筛选出GC ROOT的。但是HotSpot并没有让每条指令都生成OopMap,而是只在特定的位置生成OopMap,这个位置就被称为安全点(safe point)。
使用Safepoint似乎已经完美地解决了如何进入GC的问题,但实际情况却并不一定。Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域:安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint。
8、介绍新生代GC和老年代GC
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
9、对象的内存分配策略
对象优先在Eden分配
当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代GC)。若GC期间虚拟机又发现已有的对象全部无法放入Survivor空间,就会通过分配担保机制提前转移到老年代去
大对象直接进入老年代
大对象是指,需要大量连续内存空间的Java对象,例如很长的字符串以及数组。大对象对虚拟机
的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(复习一下:新生代采用复制算法收集内存)。
长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
总结就是:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。
10、监控和性能分析工具
1)jdk自带工具(命令行和可视化)
JDK的bin目录中提供了很多命令行工具,大多数是jdk/lib/tools.jar类库的一层薄包装,它们主要的功能代码是
在tools类库中实现的。
- jps:JVM Process Status Tool,命令行工具,显示指定系统内所有的HotSpot虚拟机进程
- jstat:JVM Statistics Monitoring Tool,命令行工具,用于收集HotSpot虚拟机各方面的运行数据
- jinfo:Configuration Info for Java,命令行工具,显示虚拟机配置信息
- jmap:Memory Map for Java,命令行工具,生成虚拟机的内存转储快照(heapdump文件)
- jhat:JVM Heap Dump Browser,用于分析heapdump文件,它会启动一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果
- jstack:Stack Trace for Java,命令行工具,显示虚拟机的线程快照。
- jconsole:可视化工具,Java监视与管理控制台
- VisualVM:多合一故障处理工具,JDK发布的功能最强大的运行监视和故障处理程序,并且可以预见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。