java虚拟机

JVM的组成:

类加载器

运行时数据区

执行引擎

本地库接口

 

1.JVM运行时数据区

程序计数器:

线程私有,它可以看作是当前线程所执行字节码的行号指示器;如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

 

虚拟机栈:

线程私有,生命周期和线程一致。每个方法在执行时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行结束,对应着栈帧从虚拟机栈中入栈到出栈的过程。

这个区域有可能出现两个异常,一种是StackOverflowError,当前线程请求的栈深度大于虚拟机所允许的深度时,会抛出这个异常。(例如:将一个函数反复递归自己);另一种是OutOfMemoryError异常,当虚拟机栈可以动态扩展时(当前大部分虚拟机都可以),如果无法申请足够多的内存就会抛出这个异常(例如:在死循环里面不断启动新线程)

 

本地方法栈:

本地方法栈与虚拟机栈发挥的作用相似,虚拟机栈为java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务(通常是其他语言写的方法);也会有 StackOverflowError 和 OutOfMemoryError 异常。

 

方法区:

线程共享的内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。静态变量不会被回收,静态变量的引用可以被回收;

 

java堆:

线程共享的内存区域,这块区域是JVM所管理的内存中最大的一块,主要存放对象实例和数组,也是垃圾回收的主要区域;

OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

 

2.类加载机制

类的加载:类的加载是指将类的class文件中的二进制数据读进到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向java程序员提供了访问方法区内的数据结构的接口。

 

类的生命周期(7个阶段):加载->连接->初始化->使用->卸载

加载:查找并加载类的二进制数据,在java堆中也创建一个java.lang.Class类的对象;

连接:连接又包含三块内容:验证,准备,初始化。1)验证:文件格式,元数据,字节码,符号引用验证2)准备:为类的静态变量分配内存,并将其初始化为默认值;3)解析:把类中的符号引用转换成直接引用。

初始化:为类中的静态变量赋予正确的初始值

使用:new出对象程序中使用

卸载:执行垃圾回收

 

3.垃圾回收

JDK默认的JVM是HotSpot,

垃圾回收是对已经死亡的对象进行回收,怎么样判断对象已经死亡呢?

1.引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1,当引用计数为0的对象就是不可能被再被使用的。但是主流的JVM没有使用这个算法来管理内存,因为

它难以解决对象之间相互循环引用的问题。如下代码所示:除了对象objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。

public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
    ReferenceCountingGc objA = new ReferenceCountingGc();
    ReferenceCountingGc objB = new ReferenceCountingGc();
    objA.instance = objB;
    objB.instance = objA;
    objA = null;
    objB = null;
    }
}


 

2.可达性分析算法

这个算法的基本思想就是通过一系列的"GC Roots"的对象作为节点,从这些节点开始向下搜索,节点走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的时候说明对象不可用。作为主流JVM使用的算法。

可达性分析算法

可作为GC Roots的对象:

虚拟机栈(栈帧中的局部变量表)中的引用的对象(注意,方法执行完后,栈帧会出栈);

方法区中静态属性static引用的对象;

方法区中常量final引用的对象;

本地方法栈JNI(即Native方法)引用的对象;

 

引用分成4种:强引用,软引用,弱引用,虚引用

强引用:大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那么

垃圾回收器绝不会回收它。(Object obj = new Object();)

软引用:在系统发生要发生内存溢出异常时,将会把这些对象列入回收范围之中进行二次回收;

弱引用:对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。

 

不可达的对象不是非死不可:

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在可达性分析后没有发现与GC Roots相连的引用链,那么它将被第一次标记并进行一次筛选,筛选的条件是此对象是否需要执行finalize()方法。当对象没有覆盖finalize()方法或者已经执行过finalize()方法(系统只会调用一次finalize()方法),虚拟机把这两种情况视为不需要执行(finalize()是Object的protected方法)。

如果对象被认为需要执行finalize()方法,那么对象将会被放置在一个叫做F-Queue的队列中,并稍后在一个由虚拟机自动创建的、低优先级的Finalizer线程去执行它(即去执行对象的finalize()方法)。

finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将会对F-Queue中的对象进行第二次小规模的标记,如果对象在finalize()方法中成功拯救自己——只需重新跟引用链上的任一对象进行关联即可,比如把自己赋值给某个对象变量或者成员对象,那么在第二次标记时它将被移除出"即将回收"集合。

 

判定对象死亡过程:(1)GC对与GC Roots没有引用链的对象进行第一次标记,并进行一次筛选;

(2)另一个低优先级的线程去调用那些被筛选出来的对象的finalize()方法;

(3)GC进行第二次标记,如果前一步的对象没有在finalize()方法中拯救自己,那么在第一步中没有筛选到的对象和第二步中没有在finlize()方法中拯救自己的对象将会被回收。

 

垃圾收集算法:

1.标记清除算法

最基础的算法,分为标记和清除两个阶段:首先标记需要清除的对象,在标记完成后统一回收所有被标记的对象。

它有两点不足,一个是效率问题,标记和清除的过程效率都不高;一个是空间问题,标记和清除之后会产生大量不连续的

内存碎片,空间内存碎片太多会导致分配大对象时无法找到足够的连续内存而不得不触发一次垃圾回收动作。

 

2.复制算法

为了解决效率问题,出现了复制算法,它将可用内存按容量划分为大小相等的两块,每次只需要使用其中的一块,执行垃圾回收动作时,把存放对象的那块内存上还存活的对象复制到另一块内存上,然后再把那块内存空间的对象一次性清理掉;

这使得每次只对半个区域进行垃圾回收,内存分配时也不用考虑内存碎片的情况。但是只能利用其中一半的内存。

 

3.标记-整理算法

标记整理算法的标记过程同标记清除法,但是在后续步骤不是直接对对象进行清理,而是让所有存活的对象都向一侧移动

然后直接清理掉端边界以外的内存。这样的好处是避免了内存碎片。

4.分代收集算法

当前商业虚拟机的GC都是采用分代收集算法(sun的HotSpot是商用虚拟机),根据对象的生存周期把对象分成新生代和老年代(JDK1.8把永久代改为元空间,直接使用物理内存)

新生代的对象大多数"朝生夕死"的,每次GC都会有大量对象死去,少量存活,使用复制收集算法,只需要付出少量存活对象的复制成本就可以完成收集。新生代分为分为Eden区和Survivor(Survivor From、Survivor To)大小比例默认为8:1:1。

老年代因为对象存活率高,没有额外的空间进行分配担保,就使用标记-清除或者标记-整理算法。

 

新生代收集:

新产生的对象优先进入Eden区,当Eden区满了之后再使用Survivor From,当Survivor From也满了之后就进行Minor GC(新生代GC),将Eden区和Survivor中存活的对象copy进Survivor To,然后清空Eden和Survivor From,这个时候原来的Survivor From变成了Survivor To,原来的Survivor To变成了Survivor From。复制的时候

如果Survivor To无法容纳全部的存活对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。

 

进入老年代的条件:

大对象直接进入老年代,目的是为了避免在Eden区和Survivor之间发生大量的内存复制。

长期存活的对象进入老年代,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。再熬过一次Minor GC年龄就加1,当它的年龄到达15岁,就会移入老年代。但是JVM不是要求一定要年龄到达15岁才移入老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。

垃圾收集器

垃圾收集算法是垃圾回收的方法论,而垃圾收集器是具体实现。

 

1.Serial(串行)收集器

最基本、历史最悠久的收集器,串行收集垃圾,在它进行垃圾收集的时候,必须暂停其他所有的工作线程直到收集结束,即"Stop the World"。优点是简单而高效(没有线程切换的开销)

 

2.ParNew收集器

ParNew收集器是Serial收集器的多线程版本,除了使用了多线程收集外,其他行为跟Serial收集器一样。

 

3.Parallel Scavenge 收集器

Parallel Scavenge收集器使用的是复制算法,也是一个并行的多线程收集器。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

 

4.Serial Old收集器

Serial Old收集器是新生代Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。

 

5.Parallel Old收集器

Parallel Old是新生代收集器Prarllel Scavenge的老年代版本,使用多线程和“标记-整理”算法。

 

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。对于互联网站或者B/S系统的这种注重响应速度的服务端来说,CMS是很好的选择。从名字Mark Sweep可以看出,CMS是基于“标记-清除”算法实现的。

CMS的优点:并发收集、低停顿。

缺点:

(1)对CPU资源非常敏感,面向并发设计程序的通病,虽然不至于导致用户线程停顿,但是会降低吞吐率;

(2)无法清理“浮动垃圾”,由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断出现,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次的GC;

(3)会产生大量空间碎片,因为CMS是基于“标记-清除”算法,这种算法的最大缺点就是会产生大量空间碎片,给分配大对象带来麻烦,不得不提前触发Full GC。

 

7.G1收集器

G1收集器特性:

并行与并发;

分代收集;

空间整合;

可预测的停顿

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值