深入理解java虚拟机JVM笔记

2 篇文章 0 订阅

一、走近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

有栈线程的特例实现。

线程安全与锁优化

参考多线程博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值