一个java虚拟机启动时,肯定是java命令,或者javax命令. 当启动时,先装载配置,找到配置文件之后, 会定位所需的dll. jvm.dll是java虚拟机一个主要的实现.在匹配当前系统版本dll后, 会用dll去初始化jvm虚拟机,获取相关一些native接口. 比如JNIEnv接口. 这个接口它提供了大量与jvm的操作,然后找到main方法, 就开始运行了.
基本结构首先java虚拟机,会有一个类加载的系统. 就是我们说的classloader,去把java的class文件加载到内存当中去. 内存空间是分区域的. 分为方法区,java堆, 栈和本地方法栈; 虚拟机运行时,和一般cpu一样,我们需要有一个指针来指向下一条,这就是pc寄存器. 我们需要一个执行引擎来执行我们的代码, 这就是执行引擎. java运行还有一个很重要的模块: 垃圾收集器.想要一个java程序成功运行,需要上面所有模块协同配合.
内存空间
- 堆
- 线程共享区域,对象都保存在堆中.年轻代+老年代
- -Xms: 最小空间大小
- -Xms:最大空间大小
- -XX:NewSize:新生代最小空间大小
- -XX:MaxNewSize设置新生代最大空间大小
- 老年代=堆空间大小-年轻代大空间大小
- 推荐年轻代/堆=3/8
- 年轻代内部分为Eden:From:To=8:1:1
- 之前有持久代,此内存区域在8已被移除
- 方法区
- 线程共享区域,存放类信息,常量,静态变量
- 栈
- 线程私有
;栈由一系列帧组成(因此java栈也叫做帧栈)
;帧保存一个方法的局部变量.操作数栈.常量池指针
;每一次方法调用创建一个帧,并压入栈;小对象直接分配在栈上;大对象或者逃逸对象无法栈上分配
- 线程私有
- 本地方法栈
- 调用本地方法库,实现与操作系统,硬件交互
- PC寄存器
- 线程私有;每个线程拥有一个PC寄存器,在线程创建时创建;控制程序指令的执行顺序(程序如何一步步执行)
- 唯一没有规定任何oom区域
我们来看一个栈/堆/方法区交互的一个例子
public class Main {
public static void main(String[] args) {
Sample test1=new Sample("测试1");
//test1是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面
Sample test2=new Sample("测试2");
test1.printName();
test2.printName();
}
public static class Sample{
//运行时,jvm把main的信息都放入方法区
private String name;
//new Sample实例后,name引用放入栈区里,name对象放入堆里
private Sample(String name){
this.name=name;
}
//print方法本身放入方法区里
public void printName(){
System.out.println(name);
}
}
}
堆中内容:Sample实例
方法区:Sample类信息,方法printName(); Main类信息,main方法
栈:局部变量:test1; 执行main()主线程的方法调用栈
- 对象分配规则
- 优先分配Eden,Eden空间不足,执行Minor GC
- 大对象直接老年代
- 每个线程有一个工作内存;主内存是所有线程共享的.工作内存中保存的是该线程使用到的变量的主内存的副本拷贝
- 当数据从主内存复制到工作存储时,必须有两个动作:1. 主内存执行的读操作; 2.工作内存执行响应的load操作;
- 当数据从工作内存拷贝到主内存时,两个操作: 1.工作内存执行的存储(store)操作; 2.主内存执行的相应的写操作;
- 普通变量,一个线程更新的值,不能立马反应在其他变量中.如果想在其他线程中立即可见,需要使用volatile关键字
可见性
volatile
* 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
* 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
* 保证可见性的方法
* volatile
* synchronized
* final(一旦初始化完成,其他线程立刻可见)
指令重排
重排原因
计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列.这个序列是会输出确定的结果.但是,CPU和编译器为了提升程序执行效率,会按照一定的规则运行进行指令优化(和mysql的执行计划有点像),并发多线程场景下,指令重排会产生不确定的执行效果.
原则
- 一个线程内保证语义的串行性;
- volatile规则:volatile变量的写,先发生于读
- 锁规则:解锁(unlock)必然发生在随后的加锁前
- 传递性:A先于B,B先于C,那么A必然先于C
- 线程start方法优于它的每一个动作
- 线程所有操作先于线程终结(Thread.join())
- 线程中断先于被中断线程的代码
- 对象的构造函数执行结束先于finalize()
分代思想
- 根据对象存活周期分类,短命对象-新生代; 长命对象-老年代
- 少量对象存活,适合复制算法
- 大量对象存活,适合标记清理或标记压缩
Stop-The-World
- 上图有提到标记清除算法,java程序被暂停, 全局停顿,所有java代码停止,native代码可以执行,但不能和jvm交互
- 为啥存在: 可以类比在屋里打扫房间,一边扫一边又新垃圾产生,永远打扫不干净
- 危害
- HA系统,主备切换;
- 新生代快,老年代慢;服务停止无响应
- 通过垃圾回收算法,可以看出不同算法各有千秋,但是jvm不是单纯使用某一种算法来进行垃圾回收的.对其进行了一定的封装-垃圾收集器,使用不同垃圾收集器,使程序性能达到最优
- 垃圾回收主要针对的是堆空间.所以看一下堆内存
- 包括:新生代和老年代;新生代又分:eden(伊甸区)和两个幸存区(s0和s1,又可称:from to);
- Eden:From:to =8:1:1
加载
- 装载类第一步
- 取得类的二进制流
- 转为方法区数据结构
- java堆中生成对应的java.lang.Class对象
连接
验证
- 验证class流格式正确; 文件格式验证/元数据验证/字节码验证/符合引用验证等
准备
- 分配内存
解析
- 符合引用(字符串引用对象)替换为直接引用(指针或者地址偏移量;引用对象一定在内存);
初始化
- 执行类构造器
- static变量 赋值语句
- static{}语句
- 子类初始化前保证父类初始化
- 检查是否装载是自下向上的.加载类的话,是自上向下的.
- 双亲模式:子类查看类是否被加载,没有就调用父类的loadClass方法,直到类么有父类;此过程可以看成双亲模式;
- 一个类加载器收到类加载请求,会把请求委派给父类加载器完成.最终所有加载请求都传到最顶层的启动类加载器中.只有当父类加载器无法完成这个加载请求,才会交给子类去尝试加载
- 好处: java类随着其类加载器具备具有优先级的层次关系;保证类加载不乱;
- 调优工具: VisualVM;Jconsole;JProfiler; 前两个都是jdk自带的.
- Tomcat里面, 添加上两行关于远程监控的配置(自行查吧.挺简单的)
CPU
- 繁忙原因
- 频繁gc
- 多线程上下文切换
….没写完,等我补..尴尬