实战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内存模式就是解释和规范这种情况的,将这种看似随机的状态变为可控
从而屏蔽多线程可能引发的问题

posted @ 2021-09-07 22:18  余***龙  阅读(167)  评论(0编辑  收藏  举报