1. 内存模型
线程私有:
1)程序计数器:指示程序执行位置、便于流程控制;线程切换时记录程序执行位置
2)虚拟机栈:方法执行内存模型(一次方法执行对应一个栈帧),包括局部变量表、操作数栈
3)本地方法栈:调用native方法,创建的局部变量
线程公有:
1)堆:存放对象实例 可分为
对象头:锁、GC状态、类型指针;实例数据;对象填充(对其8个倍数)
堆内存采用分代划分:主要划分为新生代和老年代,新生代又划分为eden、survivor区,整体比例为8:1:1
2)方法区:类信息、常量池
ps:class文件常量池:常量的字面量放到class文件中
运行时常量池:class常量池加载到内存的方法区
字符串常量池:内存堆中,全局,采用hash表保存字符串引用,值在堆中;intern 方法查找池中常量
3)直接内存:便于NIO类使用的内存
2. 类加载
加载:1)通过类名获取字节流 2)将文件中静态存储结构转变为方法区的运行时数据结构 3)生成class对象,作为方法区类数据的访问入口
链接:
1)验证:文件格式、元数据、字节码、符号引用校验
2)准备:为类变量分配内存,设置零值
3)解析:将常量池中的符号引用转换为直接引用
初始化:
执行类构造器方法(cinit),此方法自动收集类变量赋值、静态代码块语句,代码顺序执行
类加载器:
1)内置三个内加载器:启动类加载器(bootstrapclassloader)、最顶层类加载器,由c++实现,负责加载lib目录的jar;扩展类加载器(extensionclassloader),加载ext目录下的jar;应用程序类加载器(applicationclassloader),加载当前classpath的所有jar
2)双亲委派机制:在类加载过程中先判断是否被加载,若无则将请求委托给父类加载器,若父类加载器无法处理才由自己处理 目的:保证核心类库安全,使其不会被自定义类加载器加载
破坏双亲委派机制
1)SPI机制:以java.sql.Driver为例,Driver是jdk提供的一个接口,各大数据库厂商提供具体实现类,但是依据双亲委派机制,由引导类加载的Driver无法拿到由应用程序加载器加载的实现类,但是通过线程上下文类加载器可以实现
2)web应用类加载器:每个应用对应一个类加载器,先尝试自己加载,随后给父类加载器加载(保证用户类优先级高于容器提供的类)
JVM对象
对象创建:
1. 类加载检查:检查类是否已经被加载过
2. 分配内存:
1)堆内存规整:指针碰撞,空闲内存放一边,移动分界指针即可
2)堆内存不规整:空闲列表,维护一个内存可用列表
3. 初始化零值(变量赋为零值)
4. 设置对象头
5. 初始化:执行<init>方法(语句块、变量初始化、构造函数)’
ps:父类静态属性>子类静态属性>父类非静态属性(按代码顺序执行)>子类非静态属性
对象访问:
java程序访问时通过栈上reference数据结构操作对象,访问对象主要有两种方式:
1)句柄:在堆内存中划分一块句柄池,reference指向句柄池中地址,句柄负责指向对象内存地址和类对象地址
2)直接指针:reference指向对象地址,但还需通过对象去访问类对象
句柄的好处时reference存储的是稳定的句柄地址,在对象移动的时候只需要改变句柄对象指针即可;使用直接指针的好处是速度快,接生了一次指针定位的开销(hotspot使用直接指针)
GC:
判断对象是否回收:
1. 引用计数:
每个对象都有一个引用计数器,若被引用则加一,若为零则可被释放
优点:简单高效; 缺点:不能解决循环依赖
ps:强引用:不能被GC 软引用:内存不足的时候被回收 弱引用:GC时一定被回收;虚引用:和无引用一样,只是GC时受到系统通知
2. 可达性分析:
以GCRoots为起点搜索,搜索不到的则可被释放
GCRoots:1)栈帧中局部变量 2)方法区中的静态变量和常量 3)本地方法栈JNI引用对象
优点:更加精确,解决了循环依赖问题
具体来说其实经历了两次标记,首先可达性分析标记可回收对象,如果要执行finalize,则将其放入队列中,随后进行第二次标记,判断在执行finalize,是否被其他引用
缺点:实现复杂;可达性分析耗时,甚至STW
HotSpot具体实现:
1)GCRoots一般存在方法区和栈帧中,如果每次GC都去全局查找GCRoots则太费时间,因此采用空间换时间的思想,用一个oopMap结构来记录内存那个位置是GCRoots,这样GC时就不用查找
2)许多条命令的执行都会导致OopMap结构数据发生变化,如果为每次执行这些命令都生成OopMap的话,太耗费空间,因此只在指定时间点记录信息,这些时间点称为安全点
3)目前主要有两种方式让所有线程跑到最近的安全点,然后停顿下来
抢先式中断:首先将线程全部中断,然后再检查哪些没有到达安全点 ,没有到达安全点的就恢复线程,让他跑到安全点(几乎不用)
主动式中断:不直接对线程操作,仅仅简单的设置一个标志(共一个标志),各个线程执行时在安全点主动轮询这个标志,发现中断标志为真是就自己挂起
使用safepoint之后还可以进行改进,即使用“安全区域”,所谓安全区域指的是一段代码,在这段代码中,引用关系不变,因此可以生成OopMap并进行GC。当线程执行到SafeRegion后如果要离开时则要等待GC完成
准确GC?
垃圾回收算法:
2.1 标记-清除算法
首先标记所有需要回收的对象,标记回收后统一清理
缺点:1)效率问题:标记与清除过程效率不高 2)空间问题:标记清除产生大量不连续碎片
2.2 复制算法
将可用内存划分为大小相等的两块,每次只使用其中一块,当这块内存用完之后,就将还存活的对象复制到另一块,然后将已使用过一块直接清理
改进算法:将内存分为两部分,一部分是较大的Eden(占80%)和两个survivor区(10%),每次使用eden和一个survivor区,每次回收时将eden和survivor的活对象复制到另一个survivor区中,也就是只有10%的空间会被浪费
内存分配担保:并不是每次活着的对象都少于10%,因此如果另一个survivor空间没有足够空间存放存活对象将对象存放到老年代(即用老年代来担保)
优点:解决了内存碎片,可高速分配连续空间
缺点:浪费部分空间,使用效率低
2.3 标记-整理算法
首先先标记,然后将所有存活的对象都向一端移动,随后直接清理端边界以外的内存即可
优点:可有效利用堆空间,并且无内存碎片
缺点:复制开销大
垃圾收集器:
1)Serial、Serial Old
串行收集器,在进行收集的时候必须其他线程直到GC完成,
适用于单核或者比较小的堆中
2)Parallel Scavenge、Parallel Old
jdk1.8默认收集器组合
使用多线程收集垃圾,这组收集器关注的是可控吞吐量,吞吐量是 执行用户代码时间/总时间,收集器可以设置两个参数来控制吞吐量,分别为最大GC停顿时间、吞吐量比率,吞吐量越大越适合高运算的场景,可高效利用cpu;而停顿时间短则适合与用户交互的程序
3)ParNew、CMS
ParNew:新生代收集器,采用复制算法,多线程收集
CMS收集器(老年代)是一种以获取最短回收停顿时间为目标的收集器。主要用于希望系统有最短停顿时间的应用(互联网站或B/S系统的服务端)
CMS并非没有暂停,而是用两次短暂停来替代串行标记整理算法的长暂停。
回收周期如下:
初始标记 -> 并发标记 -> 重新标记 -> 并发清除
初始标记:标记GC ROOTS能直接关联到的对象(全暂停)
并发标记:标记剩余的,此时是并发执行
重新标记:重新标记并发标记阶段遗漏的对象(在并发标记阶段对象状态的更新导致)(全暂停)
并发清除:清除标记的对象
缺点:
1)CMS收集器在并发阶段会占用部分cpu资源使得应用程序变慢。
2)CMS无法处理浮动垃圾,所谓浮动垃圾是指在并发清理阶段产生的新垃圾,这部分垃圾出现在标记之后,因此无法在当次GC下清理
3)CMS是基于“标记-清除”算法实现的收集器,可能会有大量碎片
ps:新生代收集器全使用复制算法,老年代传并行收集器使用标记整理,CMS使用标记清除
G1适用于新生代、老年代
4)G1收集器
特点:
1)不需要其它收集器的配合独立管理整个GC操作
2)G1在运行期间不会产生碎片。
3)可预测的停顿:降低停顿时间是G1和CMS共同关注的点,但G1除了追求降低停顿外,还能建立可预测的停顿时间,能设置消耗在垃圾收集上的时间不得超过指定的时间。
4)G1将内存分为各个小的region,并且根据每个区域垃圾回收的价值建立一个优先顺序(garbage first),回收价值是由回收后所获得的空间与回收所需要时间的经验值来确定,优先回收价值最大的区域,保证在有效时间内获得最高的回收效率
ps:内存分配策略
1)对象优先分配在Eden,当Eden区中如果没有足够空间,则会发起一次Minor GC(即清理新生代的GC),多次Minor GC则会触发Major GC(清理老年代) 注:所谓Full GC是指是清理整个堆空间—包括年轻代和老年代
2)大对象直接进入老年代
所谓大对象是指需要大量连续内存空间的Java对象(如很长的字符串以及数组)
3) 长期存活的对象进入老年代