世界上没有完美的程序,但写程序是不断追求完美的过程。
Devices(设备、装置)、GlassFish(商业兼容应用服务器)
目录
Java虚拟机栈(Java Virtual Machine Stacks“栈”)
1. Java技术体系包括:
- Java程序设计语言
- 各种硬件平台上的JVM
- Class文件格式
- Java API 类库
- 第三方类库
JDK = Java程序设计语言+JVM+Java API
JRE = Java API 类库中Java SE API子集 + JVM
ERP(Enterprise Resource Planning企业资源计划 )& CRM(Customer Relationship Management 客户关系管理)
Java技术体系的4个平台
- Java Card:Java小程序(Applets)运行于小内存设备上的平台
- Java ME(Micro Edition):支持Java程序运行在移动终端上的平台
- Java SE(Standard Edition):支持面向桌面级应用(如Window下的应用程序)
- Java EE(Enterprice Edition):支持使用多层架构应用的企业应用(ERP、CRM)
虚拟机分类
Classic VM:内置JIT(Just In Time)编译器,“世界上第一款商用的Java虚拟机”,使用纯解释器方式执行Java代码。由于解释器不能和编译器配合工作。JDK1.4时不使用了,与Exact VM进入 Sun Labs Research VM之中。
Exact VM:Solaris平台出现,解释器和编译器混合工作模式,准确式内存管理
HotSpot VM:内置JIT(Just In Time)编译器,热点代码探测技术。通过计数器找到最具编译价值的代码,通知JIT编译器以方法为单位进行编译。
JRockit:BEA公司
KVM(Kilobyte):强调简单、轻量、高度可移植,在Andriod、ios等操作系统出现前被广泛应用
J9:IBM
Azul VM:特定硬件平台专有的虚拟机,在HotSpot
“元循环”:Meta-Circular,使用语言自身实现其运行环境。
通过TCK(Technology Compatibility Kit)的兼容性测试
HotSpot VM
如果一个方法被频繁调用或方法中有效循环次数多,分别触发标准编译和OSR(栈上替换)编译动作,可在最优化的程序响应时间与最佳执行性能取得平衡
动态语言支持(通过内置Mozilla JavaScript Rhono引擎实现)、提供编译API和微型HTTP服务器API等。JVM改进 锁与同步、·垃圾手机、 类加载等
JDK1.7计划改进:Lambda项目(Lambda表达式、函数式编程)、Jigsaw项目(虚拟机模块化支持)、动态语言支持、GarbageFirst收集器和Coin项目(语言细节进化)
JDK1.7实际改进:新的G1收集器、加强对非Java语言的调用支持、升级 类加载架构等
模块化、混合编程
开放服务网关协议OSGi技术、Clojure、JRuby、Groovy运行在JVM上
多核并行
Fork/Join模式是处理并行编程的一个经典方法。
OpenJDK的子项目Sumatra提供使用GPU(Graphics Processing Units图形处理器)和APU(Accelerated Processing Units)运算能力的工具
进一丰富语法
Java5添加:自动装箱、泛型、动态注解、枚举、可变长参数、遍历循环
Java7-8:在Coin项目中二进制的原生支持、在switch语句中支持字符串、“<>”操作符、异常处理的改进、简化变长参数方法调用、面向资源的try-catch-finally语句等
64位虚拟机
内存问题:指针膨胀和各种数据类型对齐补白的原因,比32位系统额外增加10%~30%的内存消耗。
JDK底层方法都是本地化(Native)
获取JDK源码
OpenJDK是Sun把Java开源而形成的项目
自动内存管理机制
Java和C++之间差异:内存动态分配和垃圾收集技术
Java不需要为每个new操作写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题,由虚拟机管理内存
运行时数据区域
JVM在执行Java程序的过程将内存分成若干个不同的数据区域。随虚拟机进程的启动而存在,有些区域依赖用户线程的启动和结束建立和销毁。
JVM管理的内存包括如下几个运行时数据区域:
- 方法区:Method Area
- 虚拟机栈:VM Stack
- 本地方法栈:Native Method Stack
- 堆:Heap
- 程序计数器:Program Counter Engister
程序计数器
当前线程执行的字节码的行号指示器,字节码解释器是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理
线程恢复的基础功能依赖计数器实现。
JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。一个处理器执行一条线程的指令。各个线程之间计数器互不影响。即线程私有。
计数器记录正在执行的虚拟机字节码指令的地址;如果在正在执行的Native方法,计数器值为空(Undefined)。唯一在JVM规范中没规定任何OutOfMemoryError情况的区域。
Java虚拟机栈(Java Virtual Machine Stacks“栈”)
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
虚拟机栈中局部变量表存放:基础数据类型(boolean、byte、int、double、char、short、long、float)、对象引用(reference类型,不等同于对象本身,可能是指向对象起始地址的引用指针或指向一个代表对象的句柄或其他与对象相关的位置)、returnAddress类型(指向一条字节码指令的地址)
64位长度的long和double类型的数据占用2个局部变量空间(Slot),其余数据类型占用1个。局部变量表所需的内存空间在编译期间分配,运行期间不改变局部变量表的大小。
两种异常情况:1.线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常。2.虚拟机战可以动态空战(也允许固定长度的虚拟机栈)如果扩展是无法申请到足够的内存,抛出OutOfMemoryError异常
本地方法栈(Native Method Stack)
虚拟机栈和本地方法栈区别:虚拟机栈为虚拟机执行Java方法(即字节码)服务,本地方法栈为虚拟机使用的Native方法服务
本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
Java堆(Heap)
JVM所管理的内存中最大一块。Java堆被所有线程共享的一块内存区域,虚拟机启动时创建。
用来存放对象实例,几乎所有的对象实例在这分配内存。
Java堆是垃圾收集器管理的主要区域,有时叫“GC堆”(Garbage Collected Heap)
采用分代收集算法,Java堆分为:新生代和老年代。(Eden、From Survivor、To survivor)
方法区(Method Area)
各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。别名:Non-Heap(非堆)
“永久代”(Permanent Generation),使用永久代实现方法区,Hotspot的垃圾收集器可以像管理Java堆一样管理这部分内存,但是容易遇到内存溢出问题。和Java堆一样不需要连续的内存和可选择固定大小或可扩展、也可不实现垃圾收集。
并非数据进入方法区就能“永久”存在,这区域的内存回收目标主要是针对常量池的回收和对类型的卸载
运行时常量池(Runtime Constant Pool)
方法区的一部分,存放编译期生成的各种字面量和符号引用,在类加载后加入方法区的运行时常量池。
运行时常量池相对于Class文件常量池的特征:动态性。运行期也可将常量放入池中,如String类的intern()方法
直接内存(Direct Memory)
JDK1.4新加入NOI(New Input/Output)类,引入一个基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象堆这块内存的引用进行操作。避免Java堆和Native堆来回复制数据。
受本机总内存(RAM和SWAP区或分页文件)大小和处理器寻址空间的限制。
Java堆中对象的创建
创建对象(例如克隆、反序列化)通常仅仅是一个new关键字,在虚拟机中,对象(Java对象,不包括数组和Class对象)的创建过程:
JVM遇到一条new指令
1.检查这个指令的参数是否能在常量池中定位到一个类的符号引用,检查符号引用代表的类是否已被加载、解析和初始化。
2.类加载检查通过后,虚拟机为新生成的对象分配内存。对象所需内存的大小在类加载完成后可完全确定。
根据Java堆是否规整决定使用下面哪种分配方式:
“指针碰撞”(Bump the Pointer)就是将用过的内存放一边,空闲的内存放一边,中间指针作为分界点的指示器,分配内存就是指针向空闲空间挪动一段与对象大小相等的距离。
“空闲列表”是虚拟机必须维护一个列表,记录哪些内存块可用。
使用Serial、ParNew等带Compact过程的收集器是,系统采用指针碰撞;使用CMS这种基于Mark-Sweep算法的收集器时,采用空闲列表。
对象创建都在JVM是频繁的行为没在并发情况下并不是线程安全,解决方案如下:
1.采用CAS配上失败重试的方式保证更新操作的原子性
2.“本地线程分配缓冲”(Thread Local Allocation Buffer):按照线程划分不同的空间之中进行,每个线程在Java堆中预先分配一小块内存。哪个线程需要分配就在哪个TLAB上分配,只有TLAB用完了再分配新的TLAB,才需同步锁定。
内存分配完成后,虚拟机需要分配到的内存空间初始化为零值(不包括对象头Object Header)。
JVM对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局分3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
对象头包括两部分信息:1.用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程锁持有的锁、偏向线程ID、偏向时间戳等。Mark Word分为32bit和64bit,设计成一个非固定的数据结构一遍在极小的空间存储尽量多的信息。
2.类型指针,即对象指向它的类元数据的指针,。JVM通过这个指针确定这个对象是哪个类的实例。如果对象是一个Java数组,对象头中必须有一块用于记录数组长度的数据。
HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary Object Pointers)
对齐填充起着占位符的作用,对象的大小必须是8字节的整数倍。当对象实例数据部分没有对齐时,需要通过对齐填充来补全。
对象的访问定位
建立对象是为了使用对象,通过栈上reference数据来操作堆上的具体对象。访问方式分为使用句柄和直接指针
- 句柄访问:Java堆划分出一块内存来作为句柄池。reference中存储的是对象的句柄地址。句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 直接指针访问,reference中储存的是对象地址。
句柄访问(常见)和直接指针访问对象的区别:1.使用句柄来访问最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要改变。2.使用直接指针访问的好处是速度更快。
Java堆溢出
Java堆用于存储对象实例,只要不断创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除对象。对象数量到达最大堆的容量限制后就会产生内存溢出异常。
解决方法:先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析。重点是确认内存中的对象是否是必要的,弄清是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。
如果不存在泄漏,即内存中的对象存活着,应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,减少程序运行期的内存销耗。
虚拟机栈和本地方法栈溢出
-Xoss参数(设置本地方法栈大小)存在、实际无效,栈容量只由-Xss参数设定。Java虚拟机规范描述异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出StackOverflowError异常
- 如果虚拟机在扩展栈是无法申请到足够的内存空间,OutOfMemoryError异常
使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常,异常出现是输出堆栈升读相应缩小。
定义大量的本地变量,增大其方法帧中本地变量表的长度
方法区和运行时常量池溢出
运行时常量池是方法区的一部分,两个区域的溢出测试一起进行。
String.intern()是Native方法,作用是字符串常量池已经包含一个等于此String对象的字符串,返回代表池中这个字符串的String对象;否则将此String对象包含的字符串添加到常量池中,返回此String对象的引用。通过-XX:PermSize和-XX:MaxPermSize限制方法区大小
本机直接内存溢出
DirectMemory容量可通过-XX:MaxDirectMemorySIze指定。如果不指定,默认与Java堆最大值(-Xmx指定)一样。申请分配内存的方法是unsafe.allocateMemory()
第三章 垃圾收集器与内存分配策略
判断对象是否存活
- 引用计数算法(Reference Counting):给对象添加一个引用计数器,每当有一个地方使用它是,计数器值就加一;当引用失效时,计数器减一。JVM没有使用是因为很难解决对象之间相互循环引用的问题。
- 可达性分析算法(Reachability Analysis):通过一系列“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连,此对象就是不可用。作为GC Roots的对象包括虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(即Native方法)引用的对象。
引用
强引用(Strong Reference)、弱引用(Weak Reference)、软引用(Soft Reference)、虚引用(Phantom Reference)
- 强引用(Strong Reference):类似“Object obj = new Object()”
- 软引用:用来描述一些还有用但并非必须的对象,在系统将要发生内存溢出前将这些对象进行第二次回收。SoftReference类实现
- 弱引用:描述非必需对象的,强度比软引用更弱。弱引用关联的对象只能生存在下一次垃圾收集发生之前,利用WeakReference类实现。
- 虚引用::一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,无法通过虚引用来取得一个对象实例。
生存还是死亡
经历2次标记过程后背回收:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将被第一次标记并进行筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”,防止到F-Queue的队列中。在F-Queue队列中进行第二次标记。
任何一个对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量+无用的类。回收废弃常量与回收Java堆中的对象相似。
判断一个常量是否是“无用的类”条件:1.该类所有的实例都已经被回收,Java堆中不存在该类的任何实例。2.加载该类的ClassLoader已经被回收3.该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
使用反射、动态代理、CGlib等ByteCode框架、动态申城JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备卸载的功能,以保证永久代不会溢出。
标记-清除算法(Mark-Sweep)
最基础的收集算法。
1.标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,
不足有两个:一个是效率问题,标记和清除两个过程的效率都不高,
另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致不发找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这块内存用完就将还存活的对象复制到另一块上。再把已使用的内存空间清理掉。只要移动堆顶指针,按顺序分配内存即可。
HotSpot虚拟机将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间。Eden和Survivor的大小比例是8:1,当Survivor空间不够,需要依赖其他内存(如老年代)进行分配担保。
标记-整理算法(Mark-Compact)
与“标记-清除”算法类似,但是不直接堆可回收对象进行清理,而是所有存活的对象都想一端移动,清除端边界以外的内存。
分代收集算法(Generation Collection)
根据对象存活周期的不同将内存分成几块,根据各个年代的特点采用最适当的收集算法。新生代采用复制算法,老年代采用“标记-清理”或“标记-整理”算法进行回收。
HotSpot的算法实现
枚举根节点:从可达性分析中从GC Roots节点找引用链这个操作为例,作为GC Roos的节点主要在全局性的引用与执行上下文中。
安全点
HotSpot没有为每条指令生成OopMap。在特定的位置记录某些信息,这些位置称为安全点。
只有到达安全点才能暂停。选定标准:是否具有让程序长时间执行的特征,例如方法调用、循环跳出、异常跳转才产生Safepoint
另一个问题:如何在GC发生时让所有线程都到达最近安全点上再停顿下来。两种方案:
- 抢先式中断(Preemptive Suspension)不需要线程的代码主动去配合。在GC发生时,所有线程全部中断,如果的、发现线程中断的地方不在安全点上就恢复线程,让它跑到安全点上。比较少用。
- 主动式中断(Voluntary Suspension):当GC需要中断线程时,不直接对线程操作,只是设置一个标签。各个线程执行时主动轮询这个标志发现为真就中断挂起。
安全区域(Safe Region)
程序不执行时(没有分配CPU)或线程处于Sleep状态或Blocked状态,线程不法响应JVM的中断请求。利用安全区域解决。
安全区域是指一段代码片段,引用关系不会发生变化。在这个区域内任何地方开始GC都是安全的
1.首先标识自己已经进入Safe Region,在这段时间里JVM发起GC时,不用管标识自己为Safe Region状态的线程。线程离开Safe Region时,检查系统是否已经完成根节点枚举,如果完成线程继续执行,否则必须等待直到收到可以安全离开Safe Region的信号为止。
3.5 垃圾收集器
Serial收集器:单线程的收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,它进行垃圾收集时,必须暂停其他工作线程。
Concurrent Mark Sweep(CMS)收集器:并发收集器,第一次实现让垃圾收集线程与用户线程同时工作。是一种以获取最短回收停顿时间为目标的收集器。应用场景:Java应用集中在互联网或B/S系统的服务端上,重视服务的响应速度。运作过程如下:
- 初始标记(CMS initial mark):stop the world,标记GC Roots能直接关联到的对象,速度很快
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程
- 重新标记(CMS remark):stop the world,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。停顿时间比初始标记长, 比并发标记时间短
- 并发清除(CMS concurrent sweep)
缺点:对CPU资源敏感,无法处理浮动垃圾(Floating Garbage),空间碎片多(解决方法设置+UseCMSCompactAtFullCollection开关参数)
浮动垃圾:由于CMS并发清理阶段用户线程还在运行这,伴随程序运行不断产生新垃圾,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉他们,只能等待下次GC时再清理。
ParNew收集器:Serial收集器的多线程版本,所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等于Serial收集器完全一样。使用-XX:+UserConcMarkSweepGC选项后的默认新生代收集器,也可使用-XX:+UserParNewGC选项来强制指定
Parallel Scavenger收集器:新生代收集器,使用复制算法的收集器,并行的多线程收集器。达到一个可控制的吞吐量(Throughput)
吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
Parallel Scavenger收集器无法与CMS收集器配合工作。
控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数(大于0的毫秒数)设置吞吐量大小的-XX:GCTimeRatio参数(大于0小于100)
GC自适应的调节策略:-XX:+UserAdaptiveSizePolicy根据当前系统的运行情况收集性能监控信息,动态调整参数提供最适合的停顿时间和最大吞吐量
Serial Old收集器:Serial收集器的老年代,单线程收集器+“标记-整理”算法。应用场景:在于给Client模式下的虚拟机使用。与Parallel Scavenger收集器搭配
Parallel Old收集器:Parallel Scavenger收集器的老年代,使用多线程和“标记-整理”算法。应用场景:使用Parallel Scavenger收集器+Parallel Old收集器,注重吞吐量以及CPU资源敏感的场合
Garbage First(G1)收集器:面向服务端应用的垃圾收集器,特点如下:
- 并行与并发
- 分代收集
- 空间整合,基于“标记-整理”算法,从局部(两个Region之间)上看来是基于“复制”算法
- 可预测的停顿
将Java堆分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离,而都是一部分Region(不需要连续)的集合。优先回收价值最大的Region。
虚拟机使用Remembered Set避免全堆扫描。每个Region都有一个与之对应的Remembered Set。虚拟机发现程序对Reference类型的数据进行写操作时,产生一个Write Barrier暂时中断写操作。检查Reference引用的对象是否处于不同Region之中
如果不计算维护Remembered Set操作,G1收集器的运作大致可划分为如下步骤:
- 初始标记(initial marking)
- 并发标记(concurrent mark):对堆中对象进行可达性分析
- 最终标记(Final marking)
- 筛选回收(Live Data Counting and Evacuation)对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
Full GC 触发条件
老年代空间不足
永久代空间满OOM:PermGen Space(Java8 之前)
CMS GC时出现Promotion Faield和Concurrent Model Failure
统计到升到老年代的对象的大小大大于老年代剩余的空间大小
并行与并发的区别
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态
并发(Concurrent):指用户线程与垃圾收集线程同时执行(不一定并行,可能交替执行),用户程序在继续运行,垃圾收集程序运行在另一个CPU上
GC日志
阅读GC日志是处理JVM内存问题的基础技能
垃圾收集相关参数
内存分配与回收策略
自动内存管理解决的问题:给对象分配内存以及回收分给对象的内存
- 对象的内存分配:堆上分配,对象主要分配在新生代的Eden区上。如果启动本地线程分配缓冲,将按线程优先在TLAB上分配。少数分配在老年代中。取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置
对象优先在Eden分配
当Eden区没有足够空间分配时,虚拟机发起Minor GC。
大对象直接进入老年代
大对象指需要大量连续内存空间的Java对象,如很长的字符串以及数组,byte[]数组。虚拟机提供一个-XX:PretenureizeThreshold参数,令大于这个设置值的对象直接在老年代分配。
长期存回的对象将进入老年代
虚拟机给那个对象定义了一个对象年龄(Age)计数器。
如果对象在Eden出生并经过第一次Minor GC 后仍然存活,并能被Survivor容纳的话,移动到Survivor空间,对象年龄设为1.
对象在Survivor区中在经过一次Minor GC,年龄增加1岁
当年龄增加到一定程度(默认15岁)晋升到老年代。通过-XX:MaxTenuringThreshold设置
-Xms20M -Xmx20M -Xmn10M
动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以进入老年代,无需等到MaxTenuringThreshold中要求的年龄
空间分配担保
Minor GC确保安全条件:老年代最大可用连续空间大于新生代所有对象总空间
新生代使用复制收集算法,只使用其中一个Survivor空间作为轮换备份,当出现大量对象在Minor GC后仍然存活,就需要老年代进行分配担保。
担保失败后(HandlePromotionFailure)重新发起一次Full GC。
虚拟机提供多种不同的收集器以及大量的调节参数。
目标:了解每个具体收集器的行为、优势、劣势、调节参数。