
字节码行号指示器,线程私有。分支,循环,跳转,异常处理都需要程序计数器去执行代码执行到哪里。
java虚拟机栈栈内存,线程私有,线程销毁了这个栈就不占用了。局部变量存在这里。
本地方法栈native方法用的。
java堆内存线程共享,一般对象都分配在堆上。堆内存可以换废除多个线程私有的分配缓冲区( thread lcoal allocation buffer )TLAB,以提升对象分配时的效率,这样搞是不是为了线程不冲突?
方发区线程共享,存储常量、静态变量,即时编译器编译后的代码缓存。
运行时常量池属于方发区的一部分,常量池用于存放编译期生成的各种字面量与符号引用。
直接内存不属于虚拟机运行时数据区的一部分。一般是nio那个channel 会用到的堆外内存。
对象的创建当虚拟机遇到一条字节码new指令的时候,先检查这个指令的参数是否能在常量池中定位到,并检查这个引用代表的类是否已被加载、解析和初始化过。没有则执行相应的过程。
类加载检查通过后开始为新生对象分配内存。分配内存有两种方式:
把指针向内存中空闲的方向移动一段距离。如果堆内存不规整,已使用的和空闲的搅在一起,则不能用这种方式。
空闲列表虚拟机维护一个列表,记录哪块儿内存是可以用的,分配的时候从列表中找到一块儿足够大的空间划分给对象实例,并更新表上的记录。这叫做空闲列表的方式为对象分配内存。
选择哪种分配方式由java堆是否规整决定。而java堆是否规整就要看垃圾收集器是否带有压缩整理的功能了。
对象创建比较频繁,要一直去改指针或者操作空闲列表,在并发情况下不安全。
有两个方案解决创建对象的线程安全问题:
1.为对象分配内存的时候加同步机制,加一个cas锁
2.把内存分配的动作按照线程划分在不同空间中,就不会冲突了。各种一块儿地。TLAB
主要分为三个部分:对象头(header)、实例数据(instance data)、对齐填充(padding)。
对象的访问定位有两种:句柄和直接指针,两种方式各有优势。
句柄的好处是在对象被移动(垃圾回收会移动)的时候只需要改变句柄中的实例数据指针。
直接指针主要是快。
栈不够分配了,写那种递归没有跳出循环逻辑的时候会出现
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.标记清除后会产生大量不连续的内存碎片,可能会导致后续大对象无法找到租后的连续空间,进而提前触发垃圾回收。
目前商用虚拟机大多数用这个算法去回收新生代。
把内存分为大小相等的两块,每次只使用一块,当这块内存使用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存清理掉。
如果内存中大多数对象都是存活的,那复制成本比较大。而且这种空间浪费比较严重。优势是没多少内存碎片。
新生代对象大多朝生夕死,适合用标记复制算法。老年代的对象中存活的比较多,更适合标记整理算法。
标记整理算法是一块儿内存,把存活的对象往一边移动,直接清理掉边界以外的内存。
多线程的收集器,ParNew+CMS之前是官方推荐的服务端模式下的收集器解决方案。
CMS concurrent mark sweep以获得最短停顿时间为目标的收集器。基于标记清除算法。也会造成stop the world 就是卡死。
缺点:对处理器资源非常敏感。处理器核心不足4个的时候,CMS对用户程序的影响就可能变得很大。而且是标记清除算法,有内存碎片化的弊端。
全功能的垃圾收集器。
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内存模型与线程