实战java虚拟机-jvm故障诊断与性能优化-读书笔记
jvm基本结构
- 栈和函数调用关系
局部变量表
当调用的函数的局部变量个数不同时,会影响递归的深度
局部变量在函数调用结束后,会随着函数销毁
public class TestStackDeep {
private static int count=0;
public static void recursion(long a,long b,long c){
long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
count++;
recursion(a,b,c);
}
public static void recursion(){
count++;
recursion();
}
public static void main(String args[]){
try{
recursion(0L,0L,0L);
//recursion();
}catch(Throwable e){
System.out.println("deep of calling = "+count);
//e.printStackTrace();
}
}
}
localvar2的b变量可以复用a的位置
localvar1就不行
public class LocalVar {
public void localvar1(){
int a=0;
System.out.println(a);
int b=0;
}
public void localvar2(){
{
int a=0;
System.out.println(a);
}
int b=0;
}
public static void main(String[] args) {
}
}
public class LocalVarGC {
public void localvarGc1() {
byte[] a = new byte[6 * 1024 * 1024];
System.gc();
}
申请空间后立即回收,因为a还在被引用,所以无法回收
public void localvarGc2() {
byte[] a = new byte[6 * 1024 * 1024];
a = null;
System.gc();
}
垃圾回收前将变量a设置为null,byte数组失去引用,所以可以回收
public void localvarGc3() {
{
byte[] a = new byte[6 * 1024 * 1024];
}
System.gc();
}
垃圾回收前,局部变量a失效,虽然a已经离开了作用域,但a还在局部变量中,所以不能回收
public void localvarGc4() {
{
byte[] a = new byte[6 * 1024 * 1024];
}
int c = 10;
System.gc();
}
回收之前,a失效,并且c重用了a局部变量的位置,所以a已经被销毁,可以回收
public void localvarGc5() {
localvarGc1();
System.gc();
}
可以回收
public static void main(String[] args) {
LocalVarGC ins = new LocalVarGC();
// ins.localvarGc1();
// ins.localvarGc2();
// ins.localvarGc3();
ins.localvarGc4();
}
操作数栈
保存计算中间结构
帧数据区
保存这访问常量池的指针,方便程序访问常量池
保存着异常处理表
栈上分配:对于那些线程私有对象,可以分配栈上,而不是堆上,可以避免垃圾回收
如果生成大量反射对象,则有可能元数据空间不够用
public static void main(String[] args) {
int i = 0;
try {
for (i = 0; i < 10000; i++) {
CglibBean bean = new CglibBean("geym.zbase.ch2.perm" + i, new HashMap());
}
} catch (Exception e) {
System.out.println("total create count:" + i);
throw e;
}
}
jdk1.7设置方法区(永久区)大小: -XX:PermSize=10m -XX:MaxPermSize=10m
jdk1.8设置元数据区: -XX:MaxMetaspaceSize=10m
常用jvm虚拟机参数
打印gc日日志到文件: -XX:+PrintGCDetails -Xloggc:D:/a.log
- 类加载/卸载跟踪
查看类的卸载过程:-XX:+TraceClassUnloading
查看类的加载过程:-XX:+TraceClassLoading
输出结果:
Example是通过反射生成的类
[Loaded Example from __JVM_DefineClass__]
[Unloading class Example 0x00000007c006a028]
打印虚拟机接收到的命令行显式参数-XX:+PrintVMOptions
- 堆溢出
public class DumpOOM {
public static void main(String[] args) {
Vector v=new Vector();
for(int i=0;i<25;i++)
v.add(new byte[1*1024*1024]);
}
}
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/a.dump
java.lang.OutOfMemoryError: Java heap space
Dumping heap to D:/a.dump ...
Heap dump file created [15108906 bytes in 0.012 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at geym.zbase.ch3.heap.DumpOOM.main(DumpOOM.java:19)
内存溢出时导出所有参数: -XX:+HeapDumpOnOutOfMemoryError
- 配置栈大小:
-Xss
垃圾回收概念与算法
引用计数,标记压缩法,标记清除法,复制算法和分代,分区
引用计数法
对于一个对象A,只要有任一个对象引用了A,则A的引用计数+1,当引用失效则引用计数-1,引用计数为0,则A不可以再被使用
缺点:
1.无法处理循环引用的情况
2.引用计数器在每次引用产生和消除的时候,需要一个加法和减法操作,对性能有一些影响
标记清除法
分为两个阶段:
1.标记阶段:首先通过根节点,标记所有从根节点能到达的对象,没有被标记的就是垃圾对象
2.清除阶段: 清理所有没有被标记的对象
缺点: 产生内存碎片
标记压缩算法
1.标记阶段:首先通过根节点,标记所有从根节点能到达的对象,没有被标记的就是垃圾对象
2.压缩阶段:将存活对象压缩到内存的一端,然后清理剩余的空间
复制算法
将所有内存分为两块,每次只使用一块
在垃圾对象多,存活对象少的地方适合使用
分代算法
卡表:表示老年代某一区域的所有对象是否持有新生代对象的引用
这样在新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对象的引用关系
当卡表标志位为1时,才需要扫描老年代的对象,标志位为0则代表所在区域的老年代对象都没有对新生代对象的引用
分区算法
将整个堆空间划分为连续的小空间,每一个区域独立使用,独立回收,可以减少GC时间
真正的垃圾:判断可触及性
可触及性分3种状态:
1.可触及的
2.可复活的:对象引用被释放,但是对象有可能在finalize()方法中复活
3.不可触及的:对象的finalize()方法被调用,并且没有复活,就会进入不可触及状态,对象永远不会被复活,因为finalize()只会被调用一次
第一次设置为null时,调用gc,发现对象被复活了,再次释放对象引用并执行gc,对象才真正被回收
因为finalize只会执行一次,第二次没有执行finalize,所以对象没有被复活
public class CanReliveObj {
public static CanReliveObj obj;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanReliveObj finalize called");
obj=this;
}
@Override
public String toString(){
return "I am CanReliveObj";
}
public static void main(String[] args) throws InterruptedException{
obj=new CanReliveObj();
obj=null;
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj 是 null");
}else{
System.out.println("obj 可用");
}
System.out.println("第二次gc");
obj=null;
System.gc();
Thread.sleep(1000);
if(obj==null){
System.out.println("obj 是 null");
}else{
System.out.println("obj 可用");
}
}
}
引用和可触及性强度
java中4个级别:强,软,弱,虚
-
强引用
-
软引用
通过强引用建立软引用:SoftReference<User> userSoftRef = new SoftReference<User>(u);
GC未必会回收软引用对象,但是,内存资源紧张时,软引用会被回收,所以软引用不会导致内存溢出 -
弱引用
发现即被回收,不管当前内存空间足够与否,都会回收它的内存
通过强引用建立弱引用:WeakReference<User> userWeakRef = new WeakReference<User>(u);
-
虚引用
作用:跟踪对象垃圾回收的情况
当垃圾回收器准备回收一个对象时,如果发现还存在虚引用,就会在回收对象后,将这个虚引用加入到引用队列,以通知应用程序对象的回收情况
垃圾收集器和内存分配
串行收集器
使用单线程进行垃圾回收的回收器,每次回收时,只有一个线程在工作
- 新生代串行收集器
特点:
1.使用单线程进行垃圾回收
2.独占式垃圾回收
-XX:UseSerialGC
指定使用新生代串行收集器和老年代串行收集器
-XX:+UseParNewGC
新生代使用ParNew回收器,老年代使用串行收集器
-XX:+UseParallelGC
新生代使用ParallelGC收集器,老年代使用串行收集器
- 老年代串行收集器
老年代串行收集器使用标记压缩算法
因为老年代收集时间一般比新生代长,一旦老年代收集器启动,应用程序会停顿很长时间
并行收集器
- 新生代ParNew收集器
只是简单将串行收集器多线程化,它的回收策略,算法,以及参数和新生代串行回收器一样
-
新生代ParllelGC回收器
特点:特别关注系统吞吐量,支持自适应GC调节策略
-XX:+UseParallelGC
: 新生代使用ParallelGC回收器,老年代使用串行回收器
-XX:+UseParallelOldGC
:新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器
-XX:MaxGCPauseMillis
: 设置最大垃圾收集停顿时间
-XX:GCTimeRatio
: 设置吞吐量的大小(0-100的整数,默认值是19)
-XX:+UseAdaptiveSizePolicy
: 打开自适应GC策略,
在这种模式下,新生代的大小,eden和survivor的比例,晋升老年代的年龄参数会自动调整 -
老年代ParallelOldGC
特点:也是关注吞吐量的收集器,使用标记压缩算法 -
CMS回收器
特点: (Concurrent Mark Sweep)并发标记清除, 主要关注系统停顿时间
主要步骤:
初始标记,并发标记,预清理,重新标记,并发清除和并发重置
其中初始标记和重新标记是独占系统资源的
-XX:+UseConcMarkSweepGC
:启用CMS垃圾回收器
-
G1回收器
主要工作区域是eden区和survivor区
4个阶段:
1.新生代GC
2.并发标记周期
3.混合收集
4.如果需要,会进行fullGC -
在TLAB上分配对象: 线程本地分配缓存
为了加速对象分配而生
将一部分对象不分配在堆上,而是分配在线程专用内存分配区域
锁
锁优化:
1.减少持有时间
尽可能减少某个锁的占有时间,减少线程间互斥可能
2.减少锁粒度
ConcurrentHashMap使用拆分锁对象方式提高吞吐量
3.锁分离
读写锁
4.锁粗化
如果遇到一连串对同一锁不断进行请求和释放,便把所有锁操作整合成对锁的一次请求,
从而减少对锁请求同步次数
5.无锁化
java内存模型
对于并发程序,如果一个线程修改了全局变量A,在另外一个线程不一定能读取到最新的值
java内存模式就是解释和规范这种情况的,将这种看似随机的状态变为可控
从而屏蔽多线程可能引发的问题