面试宝典——JVM(一)09.03.02

内存划分

1.java运行时数据区是如何划分的

Java虚拟机在执行Java程序的过程中会把它所管理的内容划分为若干个不同的数据区域

分为线程共享和线程独享的两大区域
  • 线程共享:方法区和堆

共享的含义就是线程都是在一个共同的一个堆中创建自己的对象和操作自己的对象,方法区中存放了对象的类信息,供所有的线程直接访问。

  • 线程独享:虚拟机栈,本地方法栈,程序计数器

独享就是这个线程必须有一套属于自己的,例如程序计数器,它是记录代码执行到的具体的行数,如果两个线程公共,那会乱套的。

具体如图:
在这里插入图片描述

  1. 程序计数器:一块较小的内存空间,作用可看作是当前线程所执行的字节码的行号指示器
    字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

Java虚拟机的多线程:通过线程轮流切换并分配处理器执行时间的方式来实现的。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。我们称这类内存区域为**“线程私有”的内存**。

  1. Java虚拟机栈:是为虚拟机执行Java方法(也就是字节码)服务,是线程私有的,生命周期与线程相同。

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  1. 本地方法栈:是为虚拟机使用到的Native方法服务。具体的虚拟机可以自由的实现本地方法栈。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

  2. Java堆:是Java虚拟机所管理的内存中最大的一块。是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例

  3. 方法区

方法区(method area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它却有一个别名叫做non-heap(非堆),目的应该是与Java堆区分开来。

  1. 运行时常量池:运行时常量池(runtime constant pool)是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述信息外,还有信息常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不需要常量一定只有编译器才能产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的就是String类的intern()方法。

  1. 直接内存:直接内存(direct memeory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分也被频繁地使用,而且也可能导致OutOfMemeoryError异常出现。

2.创建一个对象 ,jvm如何分配空间个这个对象

指针碰撞:假设Java堆中内存是绝对完整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一块与对象大小相等的距离,这种分配方式即为指针碰撞。
空闲列表:如果堆中的内存不是规整的,已经使用的内存和空闲的内存相互交错,,虚拟机就必须维护一个列表,记录上那些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例冰更新列表上的记录,这种分配方式称为空闲列表
注意: 选择哪种分配方式是由所采用的垃圾收集器是否带有压缩整理功能决定。因此,在使用Serial,ParNew等带有Compart过程的收集器时,系统采用指针碰撞,而CMS这种基于Mark-Sweep算法的收集器时,系统采用的分配方式时空闲列表。

3.对象的内存布局

有几部分组成

对象头:
    1. 用于存储对象自身的运行时数据(哈希码,Gc分代年龄,锁状态标 志,线程持有的锁,偏向线程ID,偏向时间戳等)
  • 2.类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
    注意 :如果对象是一个Java数组,那么对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但从数组的元数据信息中却无法确定数组大小。
实例数据
  • 对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承的,还是子类中定义的都要记录。 受虚拟机分配策略参数和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles,ints,shorts/chars,bytes/booleans,oops,相同宽度的字段被分配到一起。父类变量出现在子类之前。
对齐填充
  • 并不是必然存在的,也没有特殊的含义,它仅仅七折占位符的作用。

4.怎么样判断对象死了

一、引用计数法

程序给对象添加一个引用计数器,每有一个变量引用它时,计数器加1。当引用断开时,计数器减1。当计数器为0时,代表着没有任何变量引用它,该对象就是死亡状态,JVM需要对此类对象进行回收。

引用计数法的实现简单,效率也很高。但绝大数主流的虚拟机并没有采取此计数算法来管理内存,原因是此计数算法无法回收那些具有相互循环引用的对象,此类对象确实已经不再被使用,但由于互相引用着对方,导致各自的计数器都不为0,因此JVM无法回收它们。

二、可达性分析法

程序创建一系列的GC Roots作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象与GC Roots没有任何引用链相连的话,即此对象到GC Roots不可达,则证明此对象是不可用的,JVM稍后将会对此类对象进行回收。

大多数主流的JVM都采用这样的算法来管理内存,它能够解决对象之间的循环引用的问题。对象与对象之间虽然有循环引用,当他们到GC Roots没有任何引用链,系统还是判定它们为可回收对象。

5.回收流程

java的垃圾回收分为

三个区域:新生代 老年代 永久代

Eden区:当一个实例被创建了,首先会被存储在堆内存年轻代的 Eden 区中。

Survivor 区(S0 和 S1):作为年轻代 GC(Minor GC)周期的一部分,存活的对象(仍然被引用的)从 Eden 区被移动到 Survivor 区的 S0 中。类似的,垃圾回收器会扫描 S0 然后将存活的实例移动到 S1 中。

老年代: 老年代(Old or tenured generation)是堆内存中的第二块逻辑区。当垃圾回收器执行 Minor GC 周期时,在 S1 Survivor 区中的存活实例将会被晋升到老年代,而未被引用的对象被标记为回收。

img

一个对象实例化时 先去看伊甸园有没有足够的空间
如果有 不进行垃圾回收 ,对象直接在伊甸园存储.
如果伊甸园内存已满,会进行一次minor gc
然后再进行判断伊甸园中的内存是否足够
如果不足 则去看存活区的内存是否足够.
如果内存足够,把伊甸园部分活跃对象保存在存活区,然后把对象保存在伊甸园.
如果内存不足,向老年代发送请求,查询老年代的内存是否足够
如果老年代内存足够,将部分存活区的活跃对象存入老年代.然后把伊甸园的活跃对象放入存活区,对象依旧保存在伊甸园.
如果老年代内存不足,会进行一次full gc,之后老年代会再进行判断 内存是否足够,如果足够 同上.

如果不足 会抛出OutOfMemoryError.

img

GC虽然可以进行内存空间的释放,但同时频繁的GC一定会影响性能,GC发生的频率越低,你的系统就越高效.

6.回收算法⭐

1.标记—清除算法
  • 概念:算法分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
  • 是最基础的收集算法
  • 主要不足:一个是效率问题,一个是空间问题,标记清楚后会产生大量不连续的内存碎片,可能导致无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
2.复制算法
  • 目的:解决效率问题
  • 概念:将可用的内存按容量划分为大小相等的两块,每次只用一块,当这一块的内存使用完后,将存活下来的内存转移到另一块内存上,把已经使用过的内存空间一次清除
  • 优点:按顺序分配内存,实现简单,运行高效。
  • 缺点:将内存缩小为原来的一半,代价太大。若不想浪费50%的空间,需要有额外的空间进行分配担保。

对复制算法的优化:因为新生代中的对象98%的都是朝生夕死的,所以并不需要按1:1比例来划分内存空间。将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和另外一块Survivor空间,最后清理掉Eden和刚才用过的Survivor空间。默认8:1,如果Survivor空间不够用时,需要依赖老年代进行分配担保。

3.标记—整理算法
  • 概念:首先标记出所有需要回收的对象,然后让所有的存活的对象都向一端移动,直接清除掉边界以外的内存。
4.分代收集算法
  • 概念:根据对象存活周期的不同将内存划分为几块,一般把Java堆分为新生代和老年代。
    新生代中每次垃圾收集时都发现有大批对象死去,只有少量存活,选用复制算法
    老年代中因为对象的存活率高,没有额外的空间对其进行分配担保,所以采用“标记—清除”或“标记—整理”算法

7.介绍了几个收集器

  1. Serial收集器:单线程收集器,不仅仅因为它只使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的时在他进行垃圾收集的时候,必须暂停其他的所有的工作线程
  • 优点:简单而高效(与其他的收集器的单线程相比),专心做垃圾收集可获得最高的单线程收集效率。
  • 对于运行在Client模式下的虚拟机,Serial收集器是一个很好的选择。
  1. ParNew收集器:其实就是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其他和Serial收集器完全一样
  • 运行在Server模式下的虚拟机中首选的新生代收集器;目前只有Serial收集器和ParNew收集器能和CMS收集器配合使用。
  1. Parallel Scavenge收集器:使用复制算法的收集器,又是并行的多线程收集器。
  • 目的是达到一个可控制的吞吐量(CPU用于运行用户代码的时间与CPU的总消耗时间的比值)
  • 注意:停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验;高吞吐量可以高效的利用CPU的时间,尽快完成程序的运算任务。主要适用在后台计算而不需要太多交互的任务
  1. Serial Old收集器:单线程收集器,使用“标记—清理”算法。主要意义是运行在Client模式下的虚拟机使用
  2. Parallel Old收集器:使用多线程和“标记—整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge 加Parallel Old收集器。
6.CMS收集器⭐
  • 概念:基于“标记—清除”算法,是一种以获取最短回收停顿时间为目标的收集器。在互联网站或者B/S系统的服务端上,尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
  • 运行过程:首先标记一下GC Roots能直接关联到的对象,速度很快(初始标记);接下来就是进行GC Roots Tracing 的过程(并发标记阶段);然后为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(重新标记阶段);最后并发清除,清除的同时用户进程会导致新的垃圾,时间长,不发生用户进程停顿。
  • 主要优点:并发收集,低停顿,适用于响应时间要求高的系统。
  • 主要缺点:
  1. CMS收集器对CPU资源非常敏感
  2. CMS收集器无法处理浮动垃圾,可能出现”Concurrent Mode Failure“失败而导致另一次Full GC的产生。
  3. 会产生大量不连续的内存碎片,可能导致无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
7.G1收集器⭐
  • 概念:是一款面向服务端应用的垃圾收集器,是当今收集器技术发展的最前沿成果之一。使用Region划分内存空间,具有优先级的区域回收方式。
  • 最大的特点:G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region会扮演Eden空间,Survivor空间或老年代独立空间,采用不同的策略处理。会在后台维护一个优先级列表,优先处理回收价值收益最大的Region。
  • 主要特点:
  1. 并行与并发:利用多CPU,多核环境下的硬件优势使用多个CPU来缩短Stop-The-World停顿的时间,通过并发的方式让Java程序继续执行。
  2. 分代收集:采用不同的方式去处理新创建的对象和已经存活了一段时间的,熬过多次GC的旧对象以获取更好的收集效果。
  3. 空间整合:从整体上来看,是基于“标记—整理”算法实现的收集器,从局部(两个Region)上来看是基于“复制”算法实现的。意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。有利于程序长时间运行
  4. 可预测的停顿:降低停顿时间是G1和CMS共同关注的,但是G1除了追求低停顿外,还能建立可预测的停顿时间模型。
  • 运作步骤:首先标记一下GC Roots能直接关联到的对象,需要停顿线程,但速度很快(初始标记);接下来就是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象。耗时较长,但可与用户程序并发执行(并发标记阶段);然后为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要停顿线程,但是可并行执行(重新标记阶段);最后筛选回收。

8.在堆里边内存怎样分配的⭐

  • java堆中的内存是最大的,几乎所有对象实例(以及数组等) 都被存放在java堆当中

  • java堆由于是垃圾收集器管理的主要区域,所以也称为GC堆

  • 堆在分配内存时,可以分配出多个线程私有的分配缓冲区(TLAB)无论它们怎么划分,存储的仍然都是对象实例(划分只是为了更方便的管理、回收内存)

  • 除此之外,java堆在磁盘中也是不连续的(如同链表在堆中存储不连续一样),但它们在逻辑上是连续的

9.gc流程是啥⭐

通过一系列成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索(搜索走过的路径称为引用链),当一个对象没有和引用链连接,则代表该对象没有被引用,即此对象不可用了(即便该对象和其他对象有链接,但只要没有和引用链所连接,就会被判定为不可用,该对象底下的所有对象都不可用),于是此对象将会被虚拟机所回收。

10.对象的分配策略⭐

  1. 对象优先在Eden分配
  2. 大对象直接进入老年代
  3. 长期存活的对象将进入老年代
  4. 动态对象年龄判定
  5. 空间分配担保

两个无关性

JDK的命令行工具

  1. jps:虚拟机进程状况工具
    可以通过RMI协议查询开启了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名
  2. jstat:虚拟机统计信息监视工具
    是用于监视虚拟机各种运行状态信息的命令行工具,是运行期定位虚拟机性能问题的首选工具
  3. jinfo:Java配置信息工具
    作用是实时地查看和调整虚拟机各项参数。
  4. jmap:Java内存映像工具
    用于生成堆转储快照(一般称为heapdump或dump文件),还可以查询finalize执行队列,Java堆和永久代的详细信息,如空间使用率,当前用的是哪种收集器等。
  5. jhat:虚拟机堆转储快照分析工具
    jhat命令与jmap搭配使用,来分析jmap的堆转储快照,但一般不会直接使用jhat来分析dump文件。主要因为分析工作是一个耗时而且消耗硬件资源的过程,既然都要在其他机器上进行分析,那就没必要收到命令行工具的限制,而且jhat分析功能相对来说比较丑陋,有VisualVM以及 Memory Analayzer等工具能实现更强大更专业的分析功能。
  6. jsrack:Java堆栈跟踪工具
    用于生成虚拟机当前时刻的线程快照(一般称为threaddump或Java core文件)

虚拟机的加载机制

  • 概念:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制

加载过程

加载模型

  1. 从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(虚拟机自身的一部分),另一种是所有其他的类加载器(由Java语言实现,独立于虚拟机外部,全部继承自抽象类java.lang.ClassLoader)
  2. 从Java开发人员的角度来讲,类加载器还可以划分得更细致一些,绝大部分Java程序都会使用一下三种系统提供的类加载器。
  • 启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的。
  • 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器。是程序中的默认类加载器。
    在这里插入图片描述
双亲委派模型

如上图中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型。要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。父子关系是使用组合(Composition)关系来复用父加载器的代码的。

双亲委派模型是在JDK1.2期间被引用进来的

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

双亲委派模型的好处:Java类随着他的类加载器一起具备了一种带有优先级的层次关系。

破坏双亲委派模型

由于双亲委派模型不是一个强制性的约束模型,二十一种推荐的类加载器实现方式。因此有例外的类加载器不遵循这一模型

  1. 双亲委派模型第一次“被破坏”:发生在双亲委派模型出现之前,即JDK1.2发布之前。因为类加载器和抽象类java.lang.ClassLoader在JDK1.0就已经存在,因此JDK1.2后添加了一个新的protected方法findClass(),来解决已经存在的用户自定义类加载器的实现代码
  2. 双亲委派模型第二次“被破坏”:由这个模型自身的缺陷导致的,双亲委派很好的解决了各个类加载器的基础类的统一问题,但是只能作为被用户代码调用的API,因此无法解决基础类调用回用户的代码
  • 典型例子:JNDI服务,JDBC,JCE,JAXB,JBI等
  • 解决思路:线程上下文类加载器,可通过java.lang.Thread类的setContextClassLoaser()方法进行设置。如果创建线程时还没设置,将会从父线程中继承一个如果在应用程序的全局范围内都没设置过的话,类加载器默认就是应用程序类加载器。
    有了线程上下文类加载器,就可以“徇私舞弊”,可以使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。Java中所有涉及SPI的加载动作都采用这种方式。
  1. 双亲委派模型第三次“被破坏”:由于用户对程序动态性的追求而导致的。动态性指的是当前一些热门的名词:代码热替换,模块热部署
  • OSGI是业界“事实上”的Java模块化标准,OSGI实现模块化热部署的关键是它自定义的类加载器机制的实现。每一个程序模块(OSGI中称为Bundle)都有一个自己的类加载器。当需要更换一个Bundle连同类加载器一起换掉以实现代码的热替换。

当收到类加载请求时,OSGI将按照下面的顺序进行类搜索:

  • 将以java.*开头的类委派给父类加载器加载
  • 否则,将委派列表名单内的类委派给父类加载器加载
  • 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
  • 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  • 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
  • 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  • 否则,类查找失败。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值