一、走近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
有栈线程的特例实现。
线程安全与锁优化
参考多线程博客