深入理解JVM虚拟机

1、Java内存区域

在这里插入图片描述

1.1程序计数器(线程私有)

他可以看做是当前线程所执行的字节码的行号(执行到哪一行指令),由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间额方式来实现的,在任何一个确定的时刻,一个处理器都只会处理一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,即线程私有

总结:程序计数器是面向线程的

1.2Java虚拟机栈(线程私有)

Java虚拟机栈也是线程私有的,他的生命周期和线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会创建一个栈帧用于存储局部变量表(存放了基本数据类型、对象引用)、操作数栈、方法出口等信息,每个方法被调用到执行完毕,就对应这一个栈帧在虚拟机中从入栈到出栈的过程。

总结:Java虚拟机栈是面向方法的

1.3本地方法栈(线程私有)

本地方法栈与虚拟机栈非常相似,其区别只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到本地方法服务

1.4堆(线程共享)

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此区域的唯一目的就是存放对象实例,Java世界里几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的内存区域,即垃圾回收发生在此区域。

Java堆可以处于物理上不连续的内存中,但在逻辑上他应该被视为是连续的。

1.5方法区(线程共享)

方法区与Java堆一样,是多个线程共享的内存区域。它用于存储已被虚拟机加载的类型信息、常量、静态常量等。

1.6运行时常量池(线程共享)

运行时常量是方法区的一部分,Class文件除了有类的字段、方法、接口等信息,还要一项信息是常量池表,用于存放编译期生成的各种字面量与引用,这部分内容将在类加载到方法区的运行时常量池中。

1.7对象的内存布局

对象在内存中可分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头包含两类信息:第一类是用于存储对象自身的运行时数据,如哈希码值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等,官方称他为“Mark Word” 。第二类是类型指针,即对象指向他的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。最后一类是对齐填充,他并不是必然存在,也没有特别的含义,他仅仅起着占位符的作用。

2、垃圾收集器与内存分配策略

2.1概述

在Java内存运行时区域的各个部分中,程序计数器、虚拟机栈、本地方法栈3个区随着线程而生,随线程而灭,(虚拟机)栈中的栈帧随着方法的进入和退出有条不紊地执行者出栈入栈操作。

2.2对象已死?

  • 引用计数法:在对象中添加一个引用计数器,每当有一个地方引用他时,计数器值就加一;当引用失效时,计数器减一;任何时刻计数器为零的对象就是可能再被使用的对象。但是,在Java领域,主流的虚拟机都没有选用引用计数法来管理内存,主要原因是:这个看似简单的算法有很多例外情况需要考虑,必须配合大量额外处理才能保证正确地工作,比如单纯的引用计数法就很难解决对象之间的相互引用问题。

  • 可达性分析:这个算法的基本思路是通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。

    • 在Java中,固定可作为GC Roots的对象包括以下几种:在方法区中类静态属性引用的对象,例如Java累的应用类型静态变量、在方法区中常量引用的对象,例如字符串常量池中的引用、虚拟机内部的引用等。
  • 强软弱虚引用:在JDK1.2之后 ,Java对引用的概念进行了扩张,将引用分为:强、软、弱、虚引用。

    • 强应用是最传统的“引用定义”,是指在程序代码之中普遍存在的应用赋值,即类似于“Object obj = new Object()”这种引用关系,无论任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象
    • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出异常。
    • 弱引用也是用来描述那些非必须的对象,但是他的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾回收器开始工作,无论当前内存是否足够,都被回收掉只被弱引用关联的对象。
    • 虚引用也称“幽灵引用”,它是最弱的一种引用关系。一个对象是否有虚引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

2.3生存还是死亡

即使在可达性分析中判定为不可达的对象,也不是“非死不可”的,这时候他还暂时处于“缓刑”阶段,要真正宣告一个对象的死亡,至少要经理两次标记的过程。

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那么他将会被第一次标记,随后进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。

2.3垃圾收集算法

2.3.1分代收集理论

当前虚拟机,大都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质上是一套符合大多数程序运行实际情况的经验法则,他建立在两个分代假说之上:

  • 弱分代假说:绝大多数对象都是朝生夕灭的
  • 强分代假说:熬过越多次垃圾收集过程的对象越难以消亡
  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数

2.3.2收集名词

  • 新生代收集:Minor GC/Young GC:指目标发生在新生代的垃圾收集
  • 老年代收集:Major GC/Old GC:指目标只是老年代的垃圾收集(特指CMS收集器)
  • 混合收集:Mixed GC:指目标是收集整个新生代以及部分老年代的垃圾收集(特指G1收集器)
  • 整堆收集:Full GC:收集整个Java堆和方法区的垃圾收集

2.3.3垃圾收集算法

  • 标记清除算法:算法分为“标记”和“清除”两个阶段:首先标记出所有需要收集的对象,在完成标记后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。他的主要缺点有两个:第一是执行效率不稳定,如果Java堆中包含大量的对象,而且其中大部分是需要回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随着对象数量增长而降低;第二是内存空间的碎片化问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够连续的内存空间而不得不触发另一次垃圾收集动作。

  • 标记复制算法:为了解决标记清除算法面对大量可回收对象时效率低的问题,有人提出了一种名为“半区复制”的垃圾收集算法。他将可用内存分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还活着的对象复制到另一块上面,然后再把已使用的内存空间一次性清理掉。

  • 标记清理算法:标记过程与“标记清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都想内存空间一端移动,然后直接清理边界以外的内存。

2.3经典垃圾收集器

  • Serial收集器:最基础,历史最悠久的收集器,这个收集器是一个单线程工作的收集器,在它进行垃圾收集时,必须暂停其他所有工作的线程,直到他收集结束。
  • ParNew收集器:是Serial收集器的多线程版本。
  • Parallel Scavenge收集器(JDK8新生代默认):基于标记-复制算法实现的收集器,也能够进行并行收集,他的目标是达到一个可控制的吞吐量(处理器运行用户代码的时间与处理器总消耗时间的比值)。
  • Parallel Old收集器(JDK8老年代默认):支持多线程并发收集,基于并发-整理算法实现。
  • CMS收集器:CMS收集器是基于标记-清除算法实现的,他的运作过程分为四个步骤:初始标记、并发标记、重新标记、并发清除。
  • G1收集器:开除了面向局部收集的设计思路和基于Region的内存布局形式,主要面向服务端。

3、内存分配与回收策略

3.1内存分配

对象的内存分配,从概念上讲,应该都是在堆上分配。在经典分配的设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。

3.1.1对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机会发起一次Minor GC。

3.1.2大对象直接进入老年代

大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。HotSpot虚拟机提供了-XX:PretenureSizeThreshhold=3145728(3MB)参数,指定大于该设置值得对象直接在老年代分配,这样做的目的就是避免在Eden区以及两个Survivor区之间来回复制,产生大量的内存复制操作。

3.1.3长期存活的对象将进入老年代

长期存活的对象将进入老年代,虚拟机多数垃圾收集器都采用了分代收集来管理内存,那内存回收时就必须能决策哪些存活对象应该放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄计数器,存储在 对象头中。对象通常在Eden区里诞生,如果经过一次Minor GC仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其年龄设置为1岁。对象在Survivor中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认为15),就会晋升到老年代中。对象晋升到老年的的年龄阈值可以通过-XX:MaxTenuringThreshold设置。

3.1.4动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

3.1.5空间分配担保

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败;如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试一次Minor GC,尽管这次Minor GC是有风险的,如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC。

4、虚拟机类加载机制

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

4.1类加载的时机

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三部分统称为连接。

4.2类与类加载

对于任意一个类,都必须由加载它的类加载器和整个类本身一起共同确立起在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

4.3双亲委派模型

站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Boottrap Classloader),整个类加载器使用C++语言实现,是虚拟机自身的一部分,另外一种就是其他所有的类加载器,这些类加载器都有Java语言实现,独立存在于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader。

绝大多数Java程序都会使用下面三个系统提供的类加载器:

  • 启动类加载器
  • 扩展类加载器
  • 应用程序加载器

JDK9之前的Java应用都是由这三种类型加载器互相配合来完成加载的,如果用户觉得有必要,还可以加入自定义的类加载器。这些类加载器之间的协作关系如下图:

在这里插入图片描述
上图所展示的各种累加载器之间的层次关系被称为类加载器的“双亲委派模型”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去完成加载。

使用双亲委派模型来组织类加载器之间的关系,一个显而易见的好处就是Java中的类随着他的类加载器一起具备了一种带有优先级的层次关系。例如类加载java.lang.Object,它放在rt.jar中,无论哪一个类加载器加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序中的各个类加载器环境中都能保证是同一个类。

4.4破坏双亲委派模型

双亲委派魔性并不是一个具有强制约束性的模型,而是Java设计者推荐给开发者们的类加载器实现方式。

双亲委派模型的第一次被破坏发生在双亲委派模型出现之前。为了兼容之前用户自定义类加载器的代码,在JDK1.2之后添加了新的方法,引导用户编写类加载器时尽可能去重写这个类。

双亲委派模型的第二次被破坏是由这个模型吱声的缺陷导致的。比如:基础类型又要调回用户的代码,典型的就是JNDI服务,JNDI服务虽然是Java的标准服务,但是需要对资源进行查找和管理。如何解决这个问题呢?Java设计团队增加了一个线程上下文类加载器。

双亲委派模型第三次被破坏是由于用户对程序动态性的追求导致的。这里的动态性指的是代码热替换、模块化热部署等。

5、Java内存模型

Java内存模型的主要目的是**定义程序中各种变量的访问规则,**即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的细节。

Java内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存,线程中的工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的数据。不同的线程之间也无法访问对方工作内存中的变量。

关于volatile关键字的补充:当一个变量被定义成 volatile 之后,它将具备两项特性:第一项是保证此变量对所有线程的可见性.这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。比如,线程A 修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A 回写完成了之后再对主内存进行读取操作,新变量值才会对线程B可见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值