spring事务的那篇文章拖了很久,因为最近两个月特别忙;公司组织架构变更,加上之前公司**有很多因此停滞的需求接踵而至,
基本每天都加班到10点多,身心俱疲,所以就放下了博客更新;那为啥又捡起来了呢?说来话长,由于公司电商业务的极速增长,且人员流失过多,导致电商那边的需求忙不过来了,我被我们总监“永久性的卖身”给营销部;新部门最近有需求上线,这两天就自己熟悉下业务看看代码,顺便就把博客捡起来了,不过马上就要双11了这是营销部的重头戏,10月份会有很多需求;加油吧!骚年!
之前在公司有参加研发效能部同事组织的技术分享-java探针技术,分享内容很多都是底层的知识,不经让我回想起了大学期间学习的jvm,现在回忆起来除了那张概要图其他的都模模糊糊,于是花了点时间详尽的再看了一遍,趁着这几天时间把它吐出来。
一、类加载子系统
1.编译
当我们打开idea新建一个xxx.java文件,然后写个main方法,最后点击运行,输出结果;这一整个流程首先经历的就是将xxx.java文件通过javac编译成xxx.class文件,整个编译过程也是很复杂的,编译器会经过一系列的如词法分析、语法分析、语义分析和字节码生成器等过程,才可以生成class文件;编译的具体过程感兴趣的可以自行百度查阅或者可以看周志明的《深入理解java虚拟机》第三版的第四部分。
2.加载
编译期生成xxx.class文件之后,就进入了jvm虚拟机系统,首先迎接它的就是类加载子系统,在这里需要经过三大关分别是加载、链接和初始化
2.1 加载
class文件在加载阶段主要干了以下三件事:
- 通过一个类的全限定名获取此类的二进制流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 生成一个对应的java.lang.Class的实例
是谁干了上面的几件事?就是我们常听到的ClassLoader
- Bootstrap ClassLoader
- 使用c语言实现,嵌套在jvm内部
- 用来加载java的核心库(rt.jar,resources.jar)
- 不继承自java.lang.ClassLoader,没有父类加载器
- 直接在java、javax、sun等开头的文件
- Extension ClassLoader
- java语言编写
- 派生于ClassLoader类
- 父类为Bootstarp ClassLoader
- 加载jre/lib/ext中的类
- Application ClassLoader
- java语言编写
- 派生于ClassLoader类
- 父类为Extension ClassLoader
- 加载classpath下的类应用 程序默认的类加载器
既然提到了类加载器,我们不得不提的就是双亲委派模型(也就是类的加载规则)
Q1.什么是双亲委派模型?
- 如果一个类收到了类加载请求,并不会自己去加载而是委托给父类去加载
- 如果父类加载器还存在其父类加载器,则向上继续委托,直到到达最顶层
- 如果父类能完成加载,就会加载,如果父类无法加载子类会尝试自己去加载
Q2.双亲委派模型存在的意义?
答:我们开发中使用到的一些基础类,比如各种8大基本数据类型对应的包装类,如果没有双亲委派存在,同一项目当别人去用基础数据类型的时候用你的还是基类的;
这样就会使得开发很混乱,没有一个统一的标准,双亲委派存在的意思就在这里;实现简单又能解决程序中各种混乱同名类的问题,很好地对基础类进行了统一的管理。
Q3.双亲委派一定就是最优设计吗?
答:不是的,在历史中有三次双亲委派模式被打破的场景,分别是
- 引入双亲委派模型是在jdk1.2,使用原先的loadClass方法,而在次之前就有了ClassLoader即用户自定义类加载器,为了兼容老版本规定用户实现自定义类加载器时只需实现findClass方法
- 引入JNDI(Java 命名与目录接口)模块之后,就出现了基础类需要调用用户代码,这时不得不打破双亲委派模式,于是就出现了线程上下文类加载器(Thread Context ClassLoader)这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个
- 由于用户对程序动态性的追求,即程序需要热部署,每次用户代码改变,类加载都走一套自底向顶委派的模型显然无法满足;结合原有树状模型再拓展出其余网状结构,委派的动作可以在同级同层传递,OSGI就随之诞生了
2.2 链接
当class文件加载完成之后就到了链接阶段,整个阶段分为三个部分:验证、准备和解析
- 验证
- 校验字节码是否符合规范(比如java文件必须以CAFEBABY为开头、版本号是否符合标准、常量池中常量是否都能支持等等)
- 准备
- 为类变量(类中被static修饰的变量)分配内存并设置类变量初始值(不包含用final修饰的static常量)
- 解析
- 将常量池内的符号引用替换为直接引用的过程
Q1:关于准备阶段final修饰的类变量呢?
答:final static修饰的已经算是类常量了,这个在编译期间就已经初始化值了,也就是你给他什么值,编译完之后就是什么值
Q2:准备阶段的初始值是啥?
答:比如说int的初始值就是0;
Q3:什么是符号引用?什么是直接引用?一脸懵逼
答:可以使用javap -v xxx.class文件查看,找到相应的方法;只要执行ane-warray、 checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个字节码指令,所对应的那行java代码定义的变量就是符号引用;直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。
2.1 初始化
- 执行类构造器方法过程,此方法不需要定义,会收集类变量并为其赋值
- 类变量是被static修饰的,才会被clinit方法执行
- 普通类变量的赋值操作是被init方法执行的,对应我们应用程序的构造器
Q1:类变量怎么又赋值?
答:看下链接篇的Q1,准备阶段是赋初始值,初始值;初始化阶段是赋真实值真实值(你给他什么值就是什么值)
Q2:clinit是什么?init又是什么?
答:clinit是存在类变量的时候出现的,可有可无;init是构造器方法,必须有;二者都是在字节码中出现的,具体可以自行分析下字节码,不知道怎么分析?来问我
二、运行时数据区
经历过一系列筛选的字节码,这时候也该到用的时候了,所以它来到了运行时数据区;在这里的每个区域其实没有严格的先后顺序,都是协同并肩作战的,所以以下篇幅不分顺序,按习惯叙述
PC寄存器
- 类似于CPU中的寄存器主要储存指令相关的信息
- 内存空间很小,运行速度最快
- 每个线程都有自己的程序计数器,线程私有,生命周期和当前线程一致
- 存放当前方法的JVM指令地址
- 程序控制流指示器,分支、循环、跳转、异常处理、线程恢复都依赖它
- 不会OutOfMemoryError,也没有GC
- 寄存器存在的意义是防止线程切换后找不到执行记录
Q1:简单点?
答:通俗点说,就是记录当前线程走到哪里了;
举个栗子:
线程A和线程B都有10行代码
A执行了5行,然后B抢占到了CPU资源也执行了5行,这时A又抢占到资源了怎么办?
执行第6行。执行个鬼捶捶,谁告诉你的?寄存器记录了。对,答得很对!
虚拟机栈
栈是运行时单位,堆是存储单位
- 线程私有,里面存储一个个的栈帧
- 每个栈帧代表一个类中的方法
- 生命周期跟线程一致
- 不会发生GC,但是存在OOM
- 栈帧在线程间是不可以共享的
Q0:什么是运行时单位和储存单位?
答:运行时单位就是说,你的代码中具体方法开始执行也就是处于运行态的时候,它就出现了;储存单位呢,就是说你在加载阶段加载的那些个信息,什么类信息描述啦,方法信息描述,运行时常量池,实例变量啦等信息就放在堆中(这里堆囊括了方法区,暂不讨论jdk版本问题)
Q1:栈帧是什么呦?
答:其实栈帧对应的就是我们程序中的每一个方法
Q2:栈帧是最小单位吗?
答:no no no,栈帧里面存了好多东西呢!给看下?
- 局部变量表
- 定义一个数组,存储方法的参数和方法中的局部变量
- 线程私有不存在安全问题
- 大小在编译期确定,运行期不会改变大小
- 最基本的储存单元是槽
- 主要存放8种基本数据类型,引用类型和returnAdderss
- 如果是非静态方法会将this对象的引用放在局部变量表的0位
- 操作数栈
- 操作数栈也可称之为表达式栈
- 在方法执行中,往栈中写入或者提取数据
- 主要用于保存计算过程中的临时结果
- 动态链接
- 栈帧中指向运行时常量池中,该栈帧所属方法的引用,为了实现当前方法的代码实现动态链接
- 动态链接:编译时没法确定具体执行哪个类的方法,比如java中的多态(晚期绑定),除了非虚方法之外的都是虚方法
- 静态链接:编译时可以确定调用哪个方法(早期绑定),像静态、私有、final、实例构造等方法为非虚方法,对应静态链接
- 方法返回值
- 存放调用该方法的PC寄存器的值
- 附加信息
Q3:呃呃,信息量有点大,有实体对应的可以看不?
答:且看下图,这个是可视化的字节码文件,在这里光标指的地方就是main方法的局部变量表,因为main方法是静态的所以局部变量表没有储存this引用,by the way,在main方法同级别有两个方法init和clinit这两个就解答了初始化阶段的Q2。
堆
在栈章节有讲过堆区是储存单位,但是堆在1.6,1.7,1.8三个版本都有改动,改动的地方主要是在字符串常量池、静态变量和方法区的位置变化;
1.6中方法区类似于堆使用的是虚拟机内存被称为永久代,字符串常量池和静态变量都放在永久代中;
1.7之后就将字符串常量池和静态变量移至堆中储存,其他的不变;
1.8之后废除永久代,开辟一块直接内存储存原先永久代中的数据,并将这块内存称为元空间。
堆空间在逻辑上被分为新生代和老年代两块空间,这个分代跟垃圾回收紧密相连
我们新创建的对象是放在新生代的(排除大对象),当新生代空间满的时候会触发一次Young GC,Eden中经历过一次GC还存活的对象,在该对象的头部记录年龄为1岁,并将其移动至To Survivor区域,这时候To Survivor区和From Survivor区会互换,当进行第二次Young GC时,会将From Survivor区和Eden区中还存活的对象年龄+1并放入To Survivor区然后From和To互换,当From区的内存空间满了或者对象年龄超过-X:PretenureSizeThreshold参数设置的值时,此时对象就会进入老年代了
总结下:
- Eden区的存活对象永远先往TO区放,TO区永远是空的
- 只有Eden区满的时候才会触发young GC而survivor区2满的时候不会触发YGC
- 大对象(很长的字符串或者很多元素的数组)直接进入老年代
方法区
方法区主要是存放类的的一些描述信息和运行时常量池,且为每个类维护一个运行时常量池
- 运行时常量池:我们日常中随便一个main方法都需要加载很多个系统类,我们不可能每次都去加载一遍这些公共的类,所以常量池就出现了,在类加载之后开始运行的时候将常量池放到方法区就是运行时常量池了
- 类型信息
- 全类名
- 父类全类名
- 类的修饰符
- 类实现接口的有序列表
- 域信息(类属性)
- 类的成员变量
- 域的类型,修饰符,等
- 方法信息:方法描述符等信息
- JIT代码缓存
Q1.对象只能在堆上分配吗 ?
答:是也不是!怎么说?有个技术叫逃逸分析,经过逃逸分析,如果一个对象并没有逃逸出本方法的时候,可以认为该对象可能被优化为在栈上分配空间,但是,因为这项技术还不是很成熟,所以jvm中并没有使用这项技术,对象还是都分配在堆上的
逃逸:是指方法内的对象是否有可能在方法外被使用,如果有被方法外引用了,则发生了逃逸