给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值+1;当引用失效时,计数器值-1,任何时刻计数器值为0的对象就是不能再被使用的。
此方式高效简单,但不能解决循环引用的问题。
通过一系列的称为“ GC Roots ”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(图论术语:从GC Roots到这个对象是不可到达的),则此对象是不可用的。
如果对象在进行可达性分析后发现没有与GC Roots相连的引用链,也不会立即死亡。它会暂时被标记上并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。如果被判定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动建立的、低优先级的线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。如果这时还是没有新的关联出现,那基本上就真的被回收了。
为什么它们可以作为GC Roots?因为这些对象肯定不会被回收。比如,虚拟机栈中是正在执行的方法,所以里面引用的对象不会被回收。
指向当前线程正在执行的字节码指令的地址(行号),线程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中,引导类加载器是由类库和代码在虚拟机中实现的。为了向后兼容,它在程序中仍然由 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环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
直接内存不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和native堆中来回复制数据。
给对象添加一个引用计数器,每当一个地方引用它时,数据器加1;当引用失效时,计数器减1;计数器为0的即可被回收。
优点是实现简单,判断效率高
缺点是很难解决对象之间的相互循环引用(objA.instance = objB; objB.instance = objA)的问题,所以java语言并没有选用引用计数法管理内存
Java和C#都是使用根搜索算法来判断对象是否存活。通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。
这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记那些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:
效率不高,标记和清除的效率都很低;
会产生大量不连续的 内存碎片 ,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
该算法是为了解决标记-清楚,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候先将可回收的对象移动到一端,然后清除掉这一端边界以外的对象,这样就不会产生内存碎片。
为了解决效率问题,复制算法将可用内存按容量划分相等的两部分,然后每次只使用其中的一块,当第一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一块内存。
于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大的那份内存叫Eden区,其余两块较小的内存叫Survior区。每次都会先使用Eden区,若Eden区满,就将对象赋值到第二块内存上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制赋值到老年代中。(Java堆又分为新生代和老年代)。