Note of «深入理解Java虚拟机»
走进JAVA
- 结构严谨,面向对象
- 一次编写,处处运行
- 相对安全的内存管理和访问机制
- 完善的API以及强大的第三方类库
- Java技术体系
- 程序设计语言
- 虚拟机
- Class文件格式
- API类库
- 广义上还包括运行在java虚拟机上的其他语言:Jython,JRuby,Groovy…
- JDK(Java Development Kit)
- JRE(Java Runtime Environment)
- 2006年JavaOne大会时,Sun宣布最终把Java开源(GPL v2),由OpenJDK组织对源码进行管理,除了极少数产权代码(Sun本身也无权开源的)外,OpenJDK几乎包含了SunJDK的所有源码。
- Java展望
- 模块化,随着OSGi技术的发展,通过模块化实现按需部署(热部署),降低复杂度和维护成本的需求越来越迫切
- 混合语言,Java平台上多语言混合变成正在成为主流,如并发部分由Clojure编写,展示层用JRuby/Rails,中间层用Java
- 多核并行,如Map/Reduce, Scala, Clojure…
- 丰富语法,如Lambda表达式,函数式编程
- 64位虚拟机
Java内存管理
- 程序计数器–线程私有–唯一不会出现内存溢出的地方
- Java虚拟机栈(局部变量表部分)–线程私有–描述Java方法执行的内存模型,可以看作是一个局部变量表,存放了编译期可知的各种基本数据类型,对象引用,returnAddress地址,其中64位的long和double占用两个局部变量空间(Slot),局部变量表所需的内存空间大小在编译时是已知的。
- 本地方法栈–线程私有–和虚拟机栈相似,只不过这里的方法是虚拟机用到的本地Native方法,HotSpot不区分Java虚拟机栈和本地方法栈
- Java 堆–线程共享–存放几乎所有对象实例(all class instance and arrays),例外不是字符串,而是即时编译(JIT)等技术允许栈上分配
- 方法区–线程共享–存放已被虚拟机加载的类信息,常量,静态变量,即时编译后的代码
- 运行时常量池,是方法区的一部分,由于比较特殊,如部分字符串放在里面,所以总拿出来讲,String.intern()方法调用多了就容易溢出
- 直接内存,非虚拟机运行时数据区的部分,使用Native函数库直接分配的堆外内存,在nio中有使用。
各种溢出
- 两种异常的原因
- StackOverFlow: 线程请求的栈深度大于虚拟机允许的最大深度
- OutOfMemoryError: 无法申请足够内存
- Java的线程是映射到OS的内核线程上的,所以无限制开线程,会导致
OutOfMemory:unable to create new **native** thread
- 方法区由于在HotSpot虚拟机的GC机制中认为属于永久代,所以用Perm简称,如设置大小
--XX:MaxPermSize --XX:PermSize
- 方法区的溢出也很常见,比如spring,hibernate等使用cglib技术自动增类,自动生成和加载过多的类导致方法区溢出
垃圾回收&内存分配
判断对象是否存活的方法
- 引用计数法,给每个对象添加引用计数器,实现简单,效率高,但无法很难处理循环引用,所以java没有采用 TODO 有对象的列表?
- 根搜索法,从一系列名为“GC Roots”的对象作为起始点,遍历引用链,判断对象是否可达 TODO 起始点如何确定?
回收方法区,或者HotSpot虚拟机中的永久代,回收效率较新生代低,其主要回收:
- 废弃常量
- 无用的类
垃圾收集算法
-
标记-清除算法,先标记所有要回收的对象,之后统一清除,效率低且产生大量内存碎片
-
复制算法,最原始的做法是将内存分为相等的两部分,只使用其中的一部分,当用完后,将存活的对象拷贝的另一块,然后清理使用过的内存。 研究发现,新生代中的对象98%是短命的,所以不用对等分配内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次只使用Eden和其中一个Survivor,当回收时,将存活的对象拷贝到另外一个survivor上即可。HotSpot上默认Eden:survivor=8:1,也就是每次新生代中可用内存为90%(80%+10%),当存活对象多余10%的空间时,需要依赖其他内存,如老年代,进行分配担保。
-
标记-整理算法,和‘标记-清除’相似,只是后续步骤不是清除死亡对象,而是,将存活对象向一端移动,compact空间
-
分代收集,根据对象的存活周期不同,将内存分为几块,一般,将Java堆分为新生代(Young Generation)和老年代(Tenured Generation),分别使用不同的收集算法,如新生代对象存活率低,使用复制算法,老年代中对象存活率高且没有额外空间,使用标记-清理或标记-整理算法
垃圾收集器,垃圾收集算法的具体实现。
- Serial收集器,采用复制算法,‘单线程’,只有单个回收线程,且需要暂定所有工作线程,是虚拟机在Client模式下的默认新生代收集器。
- ParNew收集器,采用复制算法,Serial收集器的多线程版本,仅仅是多个回收线程,是很多虚拟机在Server模式下的首选新生代收集器。
- Parallel Scavenge收集器
- Serial Old收集器,Serial收集器的老年代版本,使用标记-整理算法,也是主要用于client模式
- Parallel Old收集器,Parallel Scavenge收集器的老年代版本,使用标记-整理算法
- CMS收集器(Concurrent Mark Sweep),以获取最短回收停顿时间为目标,使用与Server模式,基于标记-清除算法实现
- G1收集器,最牛逼的,基于‘标记-整理’算法,且能精确控制停顿。这样是G1能够在基本不牺牲吞吐量的前提下,完成低停顿内存回收。其主要思路是极力避免全区域垃圾回收,而是将Java堆,包括新生代和老年代,划分为多个大小固定的独立区域,并跟踪这些区域中的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的垃圾回收时间,优先回收垃圾最多的区域。区域划分和优先级回收保证了G1在有限时间内获取最高的回收效率。
内存分配和回收策略
- 对象优先在Eden上分配
- 大对象(需要大量连续内存空间,如数组)直接进入老年代。如果大量大对象放在eden上,会导致内存频繁回收,Eden和两个survivor间大量拷贝,浪费时间,所以尽量避免大量短命大对象
- 长期存活的对象进入老年代,虚拟机给每个对象定义一个对象年龄计数器,每进入一次survivor年龄加1,一般到15岁左右转到老年代
- 动态对象年龄判定,即不一定要高于某个阈值才升级为老年代,而是相同年龄的所有对象空间之和大于survivor空间一半,则大于等于该年龄的所有对象
- 空间分配担保,一般使用老年代进行担保,但是可能会是失败,因为老年代空间也肯能不足,失败后就进行一次Full GC,当然担保前可以通过以往每次晋升到老年代的对象数量预估本次的,如果大于老年代现有的空余空间,则进行依稀FullGC,如果小于,根据是否允许失败,进行拷贝或进行Full GC
Misc
- 老年代除了方法区是,还有那些?还是可以主动划分区域为老年代?