文章目录
- 第2章 Java内存区域与内存溢出异常
- reference两种设计
- 堆设置
- 第3章 垃圾收集器与内存分配策略
- 垃圾回收
- 方法区中的垃圾回收
- 分代收集算法
- 查看垃圾收集器
- Serial收集器
- ParNew收集器
- Parallel Scavenge
- Serial Old
- Parallel Old
- CMS收集器
- G1收集器
- 内存分配
- 对象优先在Eden分配(p67)
- 大对象直接进入老年代(p68)
- 长期存活的对象进入老年代(p68)
- 动态对象年龄判断(P71)
- 空间分配担保(P73)
深入理解JAVA虚拟机
链接:https://pan.baidu.com/s/1XAgtncMpzLrYgMrSQx76ig 提取码:rps7
第2章 Java内存区域与内存溢出异常
- 虚拟机栈
- 程序计数器
- 方法区 (包含常量池)
- 堆
reference两种设计
- 如果使用句柄访问方式,java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息
- 如果使用直接指针访问方式,java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址
这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时2移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改.
使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本.Sun HotSpot
使用第二章方式进行对象访问
堆设置
- 设置堆最小值 -Xms1024m
- 设置堆最大值 -Xmx2048m
- 打印堆信息 -XX:+HeapDumpOnOutMemoryError
- -Xss128k设置栈内存大小
- -XX:MaxPermSize128m最大方法区容量
- -XX:PermSize128m 方法区容量
第3章 垃圾收集器与内存分配策略
垃圾回收
- 哪些内存需要回收
- 什么时候回收
- 如何回收
标记
- 引用计数法
- 根搜索法
可以作为GC Roots的对象包括
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中的类静态属性引用对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)的引用对象
强弱软虚引用
- 强引用就是指在程序代码之中普遍存在的,类似"Object obj = new Object()"这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
- 软引用用来描述一些还有用,但并非必需的对象.对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收.如果这次回收还是没有足够的内存,才会抛出内存溢出异常,JDK1.2之后,提供了SoftReference类来实现软引用
- 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一点,被弱引用关联的对象只能生存到下一次垃圾收集发生之前.当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象.JDK1.2后,提供了WeakReference类来实现弱引用
- 虚引用也称幽灵引用,他是最弱的一种引用关系.一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例.为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知.JDK1.2后,提供了PhantomReference类来实现虚引用
方法区中的垃圾回收
方法区也被称为永久代
在堆中,尤其在新生代中,常规应用进行一次垃圾收集可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此
类需要同时满足下面3个条件才能算是"无用的类":
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
分代收集算法
一般吧java堆分为新生代和老年代
新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集.
老年代中因为对象存活率高,没有额外空间对它进行分配担保,就必须使用"标记-清理"或"标记-整理"算法来进行回收
查看垃圾收集器
java -XX:+PrintCommandLineFlags -version
Serial收集器
单线程,进行垃圾收集时,必须暂停其他所有工作的线程
ParNew收集器
Serial的多线程版
Parallel Scavenge
Parallel Scavenge收集器也是一个新生代收集器,也是使用复制算法
Parallel Scavenge目标是达到一个可控制的吞吐量吞吐量就是CPU代码的时间与PCU总消耗时间的比值,即吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集)时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%.
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率的利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
- -XX:MaxGCPauseMillis控制最大垃圾收集停顿时间
- -XX:GCTimeRatio直接设置吞吐量大小
MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值.GC停顿的时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300m新生代肯定比收集500m快,原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒,停顿的时间在下降,但吞吐量也降下来了
GCTimeRatio参数的值应当是一个大于0小于100的整数,也就是垃圾收集时间占时间的比率,相当于是吞吐量的倒数,如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值是99,就是允许最大1%
Parallel Scavenge收集器还有一个参数 -XX:+UseAdaptiveSizePolicy
这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics),只需要设置基本的内存数据(如-Xmx设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio参数(更关注吞吐量)给虚拟机设立一个优化目标,具体细节参数调节工作就交给虚拟机完成了
Serial Old
Serial收集器的老年代版本
Parallel Old
Parallel是Parallel Scavenge收集器的老年代版本
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,CMS收集器就非常符合这类应用的需求
CMS基于"标记-清除"算法,包括4个步骤:
- 初始标记 (CMS initial mark)
- 并发标记 (CMS concurrent mark)
- 重新标记 (CMS remark)
- 并发清除 (CMS concurrent sweep)
其中初始标记,重新标记这两个步骤仍然需要"stop the world",初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记截短则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短
CMS 有三个缺点
- CMS收集器对CPU资源非常敏感(cpu少时,占用线程超过25%)
- CMS无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure"失败导致另一次Ful GC
- cms是一款基于"标记-清除"算法实现的收集器,意味着收集结束时会产生大量空间,空间碎片过多时,将会给大对象分配打来很大的麻烦,往往会出现老年代会有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次full gc.
- -XX:CMSInitiatingOccupancyFraction,默认是68,提高触发百分比,以降低内存回收次数以获取更好的性能.但是太高可能出现"concurrent mode failure",虚拟机将启动Serial Old重新进行老年代收集,性能反而降低
- -XX:+UseCMSCompactAtFullCollection 开关参数,用于Full GC后的提供一个碎片整理
- -XX:CMSFullGCsBeforeCompaction,这个参数用于执行多少次不压缩的Full GC后,跟着来一次带压缩的
G1收集器
内存分配
-XX:+PrintGCDetails来打印内存回收日志
对象优先在Eden分配(p67)
private static final int _1MB = 1024 * 1024;
/**
* VM 参数 最大(最小)堆20M,新生代大小10M
* -XX:+PrintGCDetails
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
*/
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
}
大对象直接进入老年代(p68)
虚拟机提供一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配.这样做的目的是避免在Eden区及两个Survivor区直接发生大量的内存拷贝(新生代采用的复制算法收集内存)
private static final int _1MB = 1024 * 1024;
/**
* VM 参数
* -XX:+PrintGCDetails
* 这里只能用b做单位不能写成20M的形式
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
*/
public static void main(String[] args) {
byte[] allocation;
allocation = new byte[4 * _1MB];
}
长期存活的对象进入老年代(p68)
如果对象在Eden初始并经过第一次Minor GC后任然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄增加一岁,当年龄增加到一定程度(默认是15)时,就会被晋升到老年代.老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置
allocation1对象需要256k的内存空间,Survivor空间可以容纳.当MaxtenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后悔非常干净地变成0KB.
private static final int _1MB = 1024 * 1024;
/**
* VM 参数
* -XX:+PrintGCDetails
* 这里只能用b做单位不能写成20M的形式
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4];
// 什么时候进入老年代取决于XX:MaxTenuringThreshold设置
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
allocation3 = null;
allocation3 = new byte[4 * _1MB];
}
而MaxTenuringThreshold=15时,第二次GC发生后allocation1对象则还留在新生代Survivor空间,这时候新生代任然有404kb的空间被占用
动态对象年龄判断(P71)
为了能更好的适应不同程序的内存状况,虚拟机并不总是要求对象年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
设置-XX:MaxTenuringThreshold=15,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,也就是说allocation1,allocation2直接进入了老年代,而没有等到15岁的临界年龄.因为这两个对象加起来已经达到512K,并且他们是同年的,满足同年对象达到Survivor空间的一半规则.我们只要注释掉其中一个对象的new操作,就会发现另外一个不会晋升到老年代中了
private static final int _1MB = 1024 * 1024;
/**
* VM 参数
* -XX:+PrintGCDetails
* 这里只能用b做单位不能写成20M的形式
* -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
*/
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4];
// allocation1 + allocation2大于survivor空间的一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
空间分配担保(P73)
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小水平是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC,如果小于,则查看HandlePromotionFailure设置是否允许担保失败:如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC.