启动流程

JVM总结_加载

一个java虚拟机启动时,肯定是java命令,或者javax命令. 当启动时,先装载配置,找到配置文件之后, 会定位所需的dll. jvm.dll是java虚拟机一个主要的实现.在匹配当前系统版本dll后, 会用dll去初始化jvm虚拟机,获取相关一些native接口. 比如JNIEnv接口. 这个接口它提供了大量与jvm的操作,然后找到main方法, 就开始运行了.

基本结构

JVM总结_方法区_02

首先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
    • 大对象直接老年代
内存模型
  • 每个线程有一个工作内存;主内存是所有线程共享的.工作内存中保存的是该线程使用到的变量的主内存的副本拷贝

JVM总结_java_03

  • 当数据从主内存复制到工作存储时,必须有两个动作: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()
垃圾回收算法

JVM总结_Java_04

分代思想

  • 根据对象存活周期分类,短命对象-新生代; 长命对象-老年代
  • 少量对象存活,适合复制算法
  • 大量对象存活,适合标记清理或标记压缩

Stop-The-World

  • 上图有提到标记清除算法,java程序被暂停, 全局停顿,所有java代码停止,native代码可以执行,但不能和jvm交互
  • 为啥存在: 可以类比在屋里打扫房间,一边扫一边又新垃圾产生,永远打扫不干净
  • 危害
    • HA系统,主备切换;
    • 新生代快,老年代慢;服务停止无响应
垃圾收集器
  • 通过垃圾回收算法,可以看出不同算法各有千秋,但是jvm不是单纯使用某一种算法来进行垃圾回收的.对其进行了一定的封装-垃圾收集器,使用不同垃圾收集器,使程序性能达到最优
  • 垃圾回收主要针对的是堆空间.所以看一下堆内存

JVM总结_java_05

  • 包括:新生代和老年代;新生代又分:eden(伊甸区)和两个幸存区(s0和s1,又可称:from to);
  • Eden:From:to =8:1:1

JVM总结_加载_06

类加载机制

JVM总结_Java_07

加载

  • 装载类第一步
  • 取得类的二进制流
  • 转为方法区数据结构
  • java堆中生成对应的java.lang.Class对象

连接

验证

  • 验证class流格式正确; 文件格式验证/元数据验证/字节码验证/符合引用验证等

准备

  • 分配内存

解析

  • 符合引用(字符串引用对象)替换为直接引用(指针或者地址偏移量;引用对象一定在内存);

初始化

  • 执行类构造器
    • static变量 赋值语句
    • static{}语句
  • 子类初始化前保证父类初始化
类加载器

JVM总结_方法区_08

  • 检查是否装载是自下向上的.加载类的话,是自上向下的.
  • 双亲模式:子类查看类是否被加载,没有就调用父类的loadClass方法,直到类么有父类;此过程可以看成双亲模式;
双亲委派
  • 一个类加载器收到类加载请求,会把请求委派给父类加载器完成.最终所有加载请求都传到最顶层的启动类加载器中.只有当父类加载器无法完成这个加载请求,才会交给子类去尝试加载
  • 好处: java类随着其类加载器具备具有优先级的层次关系;保证类加载不乱;
JVM调优
  • 调优工具: VisualVM;Jconsole;JProfiler; 前两个都是jdk自带的.
  • Tomcat里面, 添加上两行关于远程监控的配置(自行查吧.挺简单的)

CPU

  • 繁忙原因
    • 频繁gc
    • 多线程上下文切换

….没写完,等我补..尴尬