深入理解java虚拟机JVM笔记

一、走近java

常用虚拟机

HotSpot VM

二、 java内存区域与内存溢出异常

在这里插入图片描述

运行时数据区

程序计数器

字节码行号指示器,线程私有。分支,循环,跳转,异常处理都需要程序计数器去执行代码执行到哪里。

java虚拟机栈

栈内存,线程私有,线程销毁了这个栈就不占用了。局部变量存在这里。

本地方法栈

native方法用的。

java堆内存

线程共享,一般对象都分配在堆上。堆内存可以换废除多个线程私有的分配缓冲区( thread lcoal allocation buffer )TLAB,以提升对象分配时的效率,这样搞是不是为了线程不冲突?

方发区

线程共享,存储常量、静态变量,即时编译器编译后的代码缓存。

运行时常量池

属于方发区的一部分,常量池用于存放编译期生成的各种字面量与符号引用。

直接内存

不属于虚拟机运行时数据区的一部分。一般是nio那个channel 会用到的堆外内存。

对象的创建

当虚拟机遇到一条字节码new指令的时候,先检查这个指令的参数是否能在常量池中定位到,并检查这个引用代表的类是否已被加载、解析和初始化过。没有则执行相应的过程。
类加载检查通过后开始为新生对象分配内存。分配内存有两种方式:

指针碰撞

把指针向内存中空闲的方向移动一段距离。如果堆内存不规整,已使用的和空闲的搅在一起,则不能用这种方式。

空闲列表

虚拟机维护一个列表,记录哪块儿内存是可以用的,分配的时候从列表中找到一块儿足够大的空间划分给对象实例,并更新表上的记录。这叫做空闲列表的方式为对象分配内存。
选择哪种分配方式由java堆是否规整决定。而java堆是否规整就要看垃圾收集器是否带有压缩整理的功能了。

对象创建的时候线程冲突的问题解决

对象创建比较频繁,要一直去改指针或者操作空闲列表,在并发情况下不安全。
有两个方案解决创建对象的线程安全问题:
1.为对象分配内存的时候加同步机制,加一个cas锁
2.把内存分配的动作按照线程划分在不同空间中,就不会冲突了。各种一块儿地。TLAB

对象的内存布局

主要分为三个部分:对象头(header)、实例数据(instance data)、对齐填充(padding)。

对象的访问定位

有两种:句柄和直接指针,两种方式各有优势。
句柄的好处是在对象被移动(垃圾回收会移动)的时候只需要改变句柄中的实例数据指针。
直接指针主要是快。
在这里插入图片描述

stack over flow

栈不够分配了,写那种递归没有跳出循环逻辑的时候会出现

out of memory

内存不够分配了

三、垃圾收集器与内存分配策略

判断对象是否需要回收

引用计数法

原理:给对象添加一个引用计数器,每当有一个地方引用它的时候计数器就加1,引用失效就减1。
在java领域不适合,主流的java虚拟机里都没有选择引用计数法来管理内存。原因是这个算法有很多例外的情况需要考虑,必须要配合大量额外处理才能保证正确的工作,比如单纯的引用计数很难解决对象之间循环引用的问题。
比如a和b对象互相应用,但是这俩对象没有其他地方用到,用引用计数法就无法回收他们。

可达性分析算法

当前主流的商用程序语言 (java、c#)的内存管理都是通过可达性分析算法来判定对象是否存活的。
基本思路:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些店开始,根据引用关系向下搜索,搜索过程锁走过的路径称为“引用链”。如果某个对象到GC Roots没有任何引用链相连,或者GC Roots到这个对象不可达,则证明这个对象需要回收。

引用分类

强引用 strongly reference、软引用 soft refreence、弱引用 weak reference、虚引用 phantom reference。

强引用

引用赋值 A a=new A(); 只要有强引用就不会回收掉被引用的对象。

软引用

描述一些还有用,但非必须的对象。在内存溢出之前先把软引用的回收了,还是不够用才抛异常。

弱引用

非必须对象。下一次垃圾回收就收掉了。当垃圾收集器开始工作,无论当前内存是否足够,弱引用对象都会被回收。

虚引用

虚引用跟没有差不多,唯一的用处是为了能在这个对象被回收的时候有一个通知。

对象回收的两次标记

第一次标记是GC Roots不可达,如果对象的finalize方法已经被虚拟机调用过,或者没有实现finalize方法,就不回收了。否则就要被回收。这个finalize方法是干啥的?

方发区的垃圾回收

主要回收废弃的常量和不再使用的类型。不强制要求回收。一般大量使用反射、动态代理这种需要jvm把不使用的类卸载掉,不然方发区内存压力比较大。

垃圾收集算法

从如何判断对象消亡的角度触发,垃圾收集算法可以划分为“引用计数式垃圾收集”和“间接垃圾收集”。主流jvm主要用的追踪式垃圾收集。

分代收集理论

当前商业虚拟机的垃圾收集器大多数遵循了“分代收集”的理论进行设计。

标记清除算法

分为“标记”和“清除”两个阶段:首先标记处所有需要回收的对象,在标记完成后,统一回收所有未被标记的对象。标记过程就是判断对象是否是垃圾的过程。
缺点:
1.执行效率不稳定,对象太多的时候标记和清除都比较慢。
2.标记清除后会产生大量不连续的内存碎片,可能会导致后续大对象无法找到租后的连续空间,进而提前触发垃圾回收。
在这里插入图片描述

标记复制算法

目前商用虚拟机大多数用这个算法去回收新生代。
把内存分为大小相等的两块,每次只使用一块,当这块内存使用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存清理掉。
如果内存中大多数对象都是存活的,那复制成本比较大。而且这种空间浪费比较严重。优势是没多少内存碎片。
在这里插入图片描述

标记整理算法

新生代对象大多朝生夕死,适合用标记复制算法。老年代的对象中存活的比较多,更适合标记整理算法。
标记整理算法是一块儿内存,把存活的对象往一边移动,直接清理掉边界以外的内存。
在这里插入图片描述

HotSpot的算法细节实现

经典垃圾收集器

ParNew

多线程的收集器,ParNew+CMS之前是官方推荐的服务端模式下的收集器解决方案。

CMS concurrent mark sweep

以获得最短停顿时间为目标的收集器。基于标记清除算法。也会造成stop the world 就是卡死。
缺点:对处理器资源非常敏感。处理器核心不足4个的时候,CMS对用户程序的影响就可能变得很大。而且是标记清除算法,有内存碎片化的弊端。

Garbage First收集器 简称G1

全功能的垃圾收集器。

ZGC收集器

不让垃圾回收也是个好策略,重启服务,没有fullgc。

四、虚拟机性能监控、故障处理工具

jdk很多小工具的命名参考了linux

jps 虚拟机进程状况工具

jps命名参考了linux的ps 列出正在执行的java进程。有点鸡肋

jstat 虚拟机统计信息监视工具

用jms就好了吧,虽然是商用的

jinfo java配置信息工具

用jms就好了吧,虽然是商用的

jmap java内存映像工具

或者 kill -3 也能拿到,jms可以搞dump看

jhat 虚拟机堆转出快照分析工具

没啥用,不能在服务器上直接分析堆转储快照,太耗费资源,容易把服务搞挂,下载dump也是一样。一般下载下来用 visualVM 或者eclipse memory analyzer、IBM HeapAnalyzer 都能替代。

jstack java堆栈跟踪工具

没啥用,有其他工具。

jconsole 压测监视用的

visual VM 多合一故障处理工具

JMC 可持续在线的监控工具

五、调优案例分析与实战

单体应用其实也可以在一个服务器上部署多个节点,搞成逻辑集群,然后不平均的负载均衡,分别内存回收。这种缺点是可能会有磁盘竞争。如果用的本地缓存比较多,也是一种浪费,因为不同节点都有自己的一份缓存。
fullgc最好一天只出现一次,定时重启。
控制fullgc的频率关键是老年代相对稳定,主要取决于应用中的大多数对方是否能复合朝生夕灭的原则。大多数对象的生存时间不应该太长,尤其是不能有成批量的、长生存时间的大对象产生。
大多数b/s网站,大多数对象都是请求级或者页面级的。也还好。
大多数64位虚拟机比32位的要耗费内存多一天,主要是由于指针膨胀、数据类型对齐补白造成的。

dictionary的内存注意也要设置。
java调用shell脚本也要注意。
同步数据可能速率不匹配,导致在一方系统中可能有挤压。必须上个queue异步去跑。
不恰当的数据类型也会导致内存利用率过低。

六、类文件结构

了解即可

七、虚拟机类加载机制

jvm的视频感觉需要再看看
常量一般在编译器就把各个类的常量抽到常量池中去了。
双亲委派

类加载的过程

加载

从网络中、jar中、zip中读取二进制流

验证

准备

会为静态变量分配内存并赋值初始值,而不是真实的值,真实的值在类初始化的时候赋上去。非静态变量(实例变量)在这里不会分配内存,实例变量会在类初始化的时候和对象一起在堆中分配内存。
但是常量就直接赋值了。这点和静态变量不同。

解析

初始化

父类的静态语句块要由于子类的静态语句块先执行。

双亲委派

热部署或者tomcat部署不影响,或者共享某部分代码。

八、虚拟机字节码执行引擎

运行时栈帧结构

栈帧属于运行时数据区中的栈内存,是进行方法调用和方法执行背后的数据结构。
栈帧存储了局部变量表、操作数栈、动态链接和方法返回地址等信息。
每一个方法从调用开始到执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

局部变量表

一组变量值的存储空间,用来存放方法参数和方法内部定义的局部变量。形参和局部变量。
局部变量表以变量槽为最小单位,一个槽不超过32位,对于double和long这俩64位数据类型,会分配两个32位的连续的槽。
为了节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,作用域不一定覆盖整个方法体,如果执行到下面,这个变量没啥用了,那这个对象所对应的变量槽就可以给其他变量用。好处是节省栈帧空间,坏处是对垃圾回收可能有副作用。有时候在后面赋值为null可能有点用。

操作数栈

后入先出栈。

动态链接

静态解析:在第一次使用或者类加载阶段转化为直接引用。
动态链接:每一次执行再转化。

方法调用

非虚方法,在类加载的时候就确定的方法,就这一个版本,没有重写或者重载造成混淆的。
虚方法,有重写的或者重载的,在类加载的时候没有确定到底是调用哪个类的方法,需要通过分派去调用。

分派

玩概念,没啥用。

静态分派

主要是解决重载问题,几个同名方法,入参继承同一个父类。仔细看代码,也能看出来到底是怎么执行的。

动态分派

运行时根据实际类型确定方法执行版本的分派过程称为动态分派。
主要是和重写有关系,到底执行哪个子类里面的代码要在运行时确定。Object的equals也是一样。
不要在构造方法里写业务代码,因为初始化时机的问题,十有八九会有bug。
字段没有多态性,可能输出的还是父类的属性。

//这里面有没有get set方法差别可大了。因为方法有多态性,字段没有多态性。妈的,还是不要这么玩。没有get、set方法执行的很诡异。
public class Test {
    static class Basea {
        private int money = 1;

        public int getMoney() {
            return money;
        }

        public void setMoney(int money) {
            this.money = money;
        }

        public Basea() {
            setMoney(2);
            printa();
        }

        public void printa() {
            System.out.println("basea--" + getMoney());
        }
    }

    static class Suba extends Basea {
        private int money = 3;

        public int getMoney() {
            return money;
        }

        public void setMoney(int money) {
            this.money = money;
        }

        public Suba() {
            setMoney(4);
            printa();
        }

        @Override
        public void printa() {
            System.out.println("Suba--" + getMoney());
        }
    }

    public static void main(String[] args) {
        Basea sub = new Suba();
        System.out.println(sub.getMoney());
    }
}


public class Test {
    static class Basea {
        public int money = 1;

        public int getMoney() {
            return money;
        }

        public void setMoney(int money) {
            this.money = money;
        }


    }

    static class Suba extends Basea {
        public int money = 3;

        public int getMoney() {
            return money;
        }

        public void setMoney(int money) {
            this.money = money;
        }


    }

    public static void main(String[] args) {
        Basea sub = new Suba();
        //这俩输出的不一样,没必要子类父类定义同名变量,太傻逼了
        System.out.println(sub.money);
        System.out.println(sub.getMoney());
    }
}

单分派与多分派

java动态分派属于单分派类型。

九、类加载及执行子系统的案例与实战

tomcat:部分类库隔离、部分类库共享、服务器自身不受部署代码的影响,tomcat需要自定义classloader,破坏双亲委派。
jboss源码可以看java规范。

代码热更新也是通过自定义classloader实现的。

十、前端编译与优化

前端解语法糖、泛型类型擦除、自动拆箱装箱、条件编译。编译处理掉那种很low的无用代码。

十一、后端编译与优化

主要是解释执行与即时编译、可以看编译原理。

十二、java内存模型与线程

java内存模型规定了所有的变量(非局部变量)都存储在主存。每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的主内存副本,线程对简历的所有操作(读取、赋值)等都必须在工作内存中进行,而不腻直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存完成。
主存和工作内存交互要保证原子性,jvm定义了几个指令去做。
在这里插入图片描述

volatile

jvm提供的最轻量级的同步机制。适合一改多读的情况。
volatile会禁止指令重排序,通过插入内存屏障实现。

Happens-Before 先行发生原则

判断数据是否存在竞争,线程是否安全的手段。可以解决并发环境下两个操作之间是否可能存冲突的所有问题。

java线程实现 HotSpot

每个线程直接映射到操作系统的线程上,受操作系统的管理。

java线程调度

系统为线程分配处理器使用权的过程,主要有两种:协同式和抢占式。

协同式

线程的执行时间由线程本身来控制,线程把自己的工作执行完了后,要主动通知系统切换到另一个线程上去。
好处:实现简单。而且线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以一般没有什么线程同步的问题。Lua的协同例程就是这么实现的。
坏处:线程执行时间不可控制,如果程序执行有问题,一直不告诉系统进行线程切换,那么程序会一直阻塞在哪里。

抢占式

线程将有系统来分配执行时间,线层的切换不由线程本身来确定。

协程

协同式调度的线程。
优势是轻量。

有栈协程

会做调用栈保护、回复的协程为有栈协程。

无栈协程

一般实现 await、async、yeild关键字会用到,用处很少。本质是一个有限状态机,状态保存在闭包里。比有栈协程还要轻量的多。

纤程 Fiber

有栈线程的特例实现。

线程安全与锁优化

参考多线程博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值