JVM - 基础知识

关注公众号:xy的技术圈java

如何判断对象已死?

有两种方法,分别为:引用计数法和可达性分析法算法

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值+1;当引用失效时,计数器值-1,任什么时候刻计数器值为0的对象就是不能再被使用的。数组

此方式高效简单,但不能解决循环引用的问题。安全

可达性分析算法

经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(图论术语:从GC Roots到这个对象是不可到达的),则此对象是不可用的。架构

若是对象在进行可达性分析后发现没有与GC Roots相连的引用链,也不会当即死亡。它会暂时被标记上而且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。若是被断定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动创建的、低优先级的线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。若是这时仍是没有新的关联出现,那基本上就真的被回收了。app

Java中可做为GC Roots的对象包括:ide

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

为何它们能够做为GC Roots?由于这些对象确定不会被回收。好比,虚拟机栈中是正在执行的方法,因此里面引用的对象不会被回收。函数

JVM内存分哪几个区,每一个区的做用是什么?

程序计数器

指向当前线程正在执行的字节码指令的地址(行号),线程CPU调度的最小单位(进程是资源分配的最小单位),CPU时间片是能够被强占的,因此要记住行号。JVM中惟一一个没有规定任何OutOfMemoryError状况的区域。性能

虚拟机栈

存储当前线程运行方法所须要的数据、指令和返回地址。(单位栈帧) (局部变量表(编译时期确认大小),操做数栈,动态连接(多态),返回地址) 。每一个方法在执行的同时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法出口等信息。网站

每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出站的过程。

可能会抛出栈深度异常StackOverflowError)或内存溢出异常(OutOfMemoryError)

本地方法栈

与虚拟机栈相似,不过是调用native方法的栈。

方法区

保存了类信息、常量、静态变量(static)、JIT、运行时常量池。有的人称为“永久代”,后更名为“元空间”。

堆是Java对象的存储区域,任何用new字段分配的Java对象实例和数组,都被分配在堆上,Java堆可以使用-Xms -Xmx进行内存控制,值得一提的是从JDK1.7版本以后,运行时常量池从方法区移到了堆上

什么是类加载器,类加载器有哪些?

实现经过类的权限定名获取该类的二进制字节流的代码块叫作类加载器。主要有如下四种类加载器:

启动类加载器(Bootstrap ClassLoader):用来加载java核心类库,没法被java程序直接引用。

扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。通常来讲,Java 应用的类都是由它来完成加载的。能够经过 ClassLoader.getSystemClassLoader()来获取它。

用户自定义类加载器:经过继承 java.lang.ClassLoader类的方式实现。

JDK 9以后的改变

保持三级分层类加载器架构以实现向后兼容。可是,从模块系统加载类的方式有一些变化。JDK 9类加载器层次结构以下图所示。

JDK9的类加载器

在JDK 9中,引导类加载器是由类库和代码在虚拟机中实现的。为了向后兼容,它在程序中仍然由null表示。例如,Object.class.getClassLoader()仍然返回null。 可是,并非全部的Java SE平台和JDK模块都由引导类加载器加载。举几个例子,引导类加载器加载的模块是java.base,java.logging,java.prefs和java.desktop。其余Java SE平台和JDK模块由平台类加载器和应用程序类加载器加载。

JDK 9中再也不支持用于指定引导类路径,-Xbootclasspath和-Xbootclasspath/p选项以及系统属性sun.boot.class.path。-Xbootclasspath/a选项仍然受支持,其值存储在jdk.boot.class.path.append的系统属性中。JDK 9再也不支持扩展机制。可是,它将扩展类加载器保留在名为平台类加载器的新名称下ClassLoader类包含一个名为getPlatformClassLoader()的静态方法,该方法返回对平台类加载器的引用。

平台类加载器用于另外一目的。默认状况下,由引导类加载器加载的类将被授予全部权限。可是,几个类不须要全部权限。这些类在JDK 9中已经被取消了特权,而且它们被平台类加载器加载以提升安全性。

为何要使用双亲委派模型?

使用双亲委派模型,有一个显而易见的好处就是Java类随着它的类加载器一块儿具有了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,不管哪一个类加载器要加载这个类,始终都要委派给启动类加载器进行加载,所以,Object类在程序的各类类加载器环境中都是同一个类。

相反,若是没有使用双亲委派模型,由各个类加载器自行去加载的话,假如用户本身编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那么系统就会出现多个不一样的Object类,那么程序也将会变得一片混乱。(若是真的这样作,将会发现正常编译,但运行失败,即便使用自定义的类加载器,强行用defaineClass()方法去加载一个以“java.lang”开头的类也不会成功,若是这样作,将会收到一个由虚拟机本身抛出的“java.lang.SecurityException:Prohibited package name:java.lang”)

如何破坏双亲委派模型?

这并不是是不可能的事情,一个典型的例子即是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它须要调用独立厂商实现部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?

为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器能够经过java.lang.Thread类的setContextClassLoader()方法进行设置,若是建立线程时还未设置,它将会从父线程中继承一个;若是在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。

有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所须要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动做,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是迫不得已的事情。

Java中全部涉及SPI的加载动做基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。

双亲委派模型的第三次“被破坏”是因为用户对程序的动态性的追求致使的,例如OSGi的出现。在OSGi环境下,类加载器再也不是双亲委派模型中的树状结构,而是进一步发展为网状结构。

JVM直接内存概念和异常

直接内存不是Java虚拟机规范中定义的内存区域,可是这部份内存也被频繁地使用,并且也可能致使OutOfMemoryError异常出现。

在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可使用native函数库直接分配堆外内存,而后经过一个存储在Java堆里面的DirectByteBuffer对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在Java堆和native堆中来回复制数据。

Java中垃圾收集用到的方法有哪些?

一、引用计数算法(Reference Counting)

给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的便可被回收。

优势是实现简单,判断效率高

缺点是很难解决对象之间的相互循环引用(objA.instance = objB; objB.instance = objA)的问题,因此java语言并无选用引用计数法管理内存

二、可达性分析算法(GC Root Tracing)

Java和C#都是使用根搜索算法来判断对象是否存活。经过一系列的名为“GC Root”的对象做为起始点,从这些节点开始向下搜索,搜索全部走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来讲就是GC Root到这个对象不可达时),证实该对象是能够被回收的。

三、标记-清除算法(Mark-Sweep)

这是垃圾收集算法中最基础的,根据名字就能够知道,它的思想就是标记那些要被回收的对象,而后统一回收。这种方法很简单,可是会有两个主要问题:

  • 效率不高,标记和清除的效率都很低;

  • 会产生大量不连续的内存碎片,致使之后程序在分配较大的对象时,因为没有充足的连续内存而提早触发一次GC动做。

四、标记-整理算法(Mark-Compact)

该算法是为了解决标记-清楚,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不一样之处就是在清除对象的时候先将可回收的对象移动到一端,而后清除掉这一端边界之外的对象,这样就不会产生内存碎片。

五、复制算法(Copying)

为了解决效率问题,复制算法将可用内存按容量划分相等的两部分,而后每次只使用其中的一块,当第一块内存用完时,就将还存活的对象复制到第二块内存上,而后一次性清除完第一块内存,再将第二块上的对象复制到第一块。可是这种方式,内存的代价过高,每次基本上都要浪费一块内存。

因而将该算法进行了改进,内存区域再也不是按照1:1去划分,而是将内存划分为8:1:1三部分,较大的那分内存叫Eden区,其他两块较小的内存叫Survior区。每次都会先使用Eden区,若Eden区满,就将对象赋值到第二块内存上,而后清除Eden区,若是此时存活的对象太多,以致于Survivor不够时,会将这些对象经过分配担保机制赋值到老年代中。(Java堆又分为新生代和老年代)。

六、分代收集算法(Generational Collection)

根据对象的存活周期的不一样将内存划分为几块,通常就分为新生代和老年代,根据各个年代的特色采用不一样的收集算法。新生代(少许存活)用复制算法,老年代(对象存活率高)“标记-清理”算法。

认真写文章,用心作分享。

我的网站:yasinshaw.com

公众号:xy的技术圈

相关文章
相关标签/搜索