ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JVM

2020-04-28 09:51:17  阅读:153  来源: 互联网

标签:Java 对象 虚拟机 线程 内存 JVM 加载


JVM包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收、堆和一个存储方法域

线程:Hotspot JVM中的Java线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的CPU上。当原生线程初始化完毕,就会调用Java线程的run()方法。当线程结束时,会释放原生线程和Java线程的所有资源

 

 

一、Java内存区域(运行时数据区)

 

图片来源:https://blog.csdn.net/weixin_43161811/article/details/103898577

JDK1.8以前:堆、栈(虚拟机栈、本地方法栈、程序计数器)、方法区(运行时常量池)、直接内存

以后:堆、栈、直接内存(元空间)

线程私有的:程序计数器、虚拟机栈、本地方法栈

线程共享的:堆、方法区、直接内存(非运行时数据区的一部分)

程序计数器作用-->字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制;在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能知道该线程上次运行到哪儿了。记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)

程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡

 

Java虚拟机栈:与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的

Java虚拟机栈是由一个个栈帧组成,每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息、常量池引用

局部变量主要存放了编译器可知的各种数据类型对象引用(reference)类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)

Java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError

若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常;若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常‘

 

本地方法栈:

虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息

本地方法一般使用其他语言编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理

 

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

Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。

现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法。可以将堆分成两块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)

Java堆还可以细分为:新生代和老年代;再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存

新生代(eden区、s0区、s1区),老年代(tentired)

大部分情况,对象都会首先在Eden区域分配,在依次新生代垃圾回收后,如果对象还活着,则会进入s0或者s1,并且对象的年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。

堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

 

方法区:

方法区与Java堆一样,是各个线程共享的内存区域、它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap

运行时常量池(Runtime Constant Pool) 是方法区的一部分。Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

方法区也被称为永久代

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存

和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

 

为什么要将永久代(PermGen)替换为元空间(MetaSpace)呢?

整个永久代有一个JVM本身设置固定大小上下限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。可以使用-XX:MaxMetaspaceSize标志设置最大元空间大小,默认值为unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize调整标志定义元空间的初始大小如果未指定此标志,则Metaspace将根据运行时的应用程序需求动态地重新调整大小

 

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常

JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java堆中开辟了一块区域存放运行时常量池

常量池项包含的内容-->

字面量(文本字符串、被声明为final的常量值、基本数据类型的值、其他),符号引用(类和结构的完全限定名、字段名称和描述符、方法名称和描述符)

 

直接内存

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

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

本机直接内存的分配不会受到Java堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制

 

 

 

知识补充:动态链接  

原文链接:https://blog.csdn.net/weixin_42096624/article/details/105227486

 

每一个栈帧都包含指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在class文件格式的常量池中存有大量符号引用(1.类的全限定名,2.字段名和属性,3.方法名和属性),字节码的方法调用指令就是以常量池中指向方法的符号引用为参数。这些符号引用一部分会在连接的解析阶段转为直接引用(向目标的指针、相对偏移量或者是一个能够直接定位到目标的句柄),这种转化称为静态解析。还有一部分引用会在运行期间转化为直接引用,这部分称为动态连接。

 

二、JAVA对象的创建过程

类加载检查-->分配内存-->初始化零值-->设置对象头-->执行init方法

1.类加载检查:虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程

2.分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”“空闲列表”两种,选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

内存分配的两种方式:

选择以上两种方式中的哪一种,取决于Java堆内存是否规整。而Java堆内存是否规整,取决于GC收集器的算法是“标记-清除”,还是“标记-整理”(也称作“标记-压缩”)

指针碰撞-->适用场合:堆内存规整(即没有内存碎片)的情况下    原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可  GC收集器:Serial、ParNew

空闲列表-->适用场合:堆内存不规整的情况下  原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块来划分给对象实例,最后更新列表记录  GC收集器:CMS

内存分配并发问题:

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS配上失败重试的方式保证更新操作的原子性

TLAB:为每一个线程预先在Eden区分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB的内存已用尽时,再采用上述的CAS进行分配

3.初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值

4.设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的对象实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式

5.执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚开始,init方法还没有执行,所有的字段都还为零。所以一般来说,执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来

 

补充知识:对象头  组成-->Mark Word;指向类的指针;数组长度(只有数组对象才有)

1,Mark Word:记录了对象和锁有关的信息,当对象被synchronized关键字当成同步锁时,围绕这个锁的一些列操作都和Mark Word有关  存放hashcode、锁信息、GC分代年龄等

JVM一般是这样使用锁和Mark Word的:

a.当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。

b.当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。

c.当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。

d.当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。

e.偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。

f.轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。

g.自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。

 

2,指向类的指针:

3,数组长度:只有数组对象才有

 

三、对象的访问定位有哪两种方式

通过栈上的reference数据来操作堆上的具体对象

主流访问方式:使用句柄;直接指针

句柄:如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

直接指针:如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址

 

四、堆内存中对象的分配的基本策略

堆空间的基本结构:eden-->s0-->s1-->tentired

1,对象优先在Eden分配:大多数情况下,对象在新生代Eden上分配,当Eden空间不够时,发起新生代GC

2,大对象直接进入老年代:最典型的大对象-->很长的字符串以及数组

3,长期存活的对象进入老年代:-XX:MaxTenuringThreshold 用来定义年龄的阈值

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

5,空间分配担保:在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

 

五、Minor GC和Full GC有什么不同

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

新生代GC:指发生新生代的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快

老年代GC:指发生在老年代的GC,出现了老年代GC经常会伴随至少一次的新生代GC,老年的GC的速度一般会比新生代GC的慢10倍以上

老年代GC的触发条件:

对于新生代GC,其触发条件非常简单,当Eden空间满时就将触发一次新生代GC。而老年代GC相对复杂,需要满足以下条件:

1.调用System.gc()  -->只是建议虚拟机执行老年代GC,但是虚拟机不一定真正去执行。

2.老年代空间不足  -->老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等

  为了避免以上原因引起的老年代GC,应当尽量不要创建过大的对象以及数组。

3.空间分配担保失败  -->使用复制算法的新生代GC需要老年代的内存空间作担保,如果担保失败会执行一次老年代GC

4.JDK1.7及以前的永久代空间不足  -->在JDK1.7及以前,HotSpot虚拟机中的方法区是用永久代实现的,永久代中存放的为一些Class的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满

5.Concurrent Mode Failure -->

 

六、如何判断对象是否死亡?

堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)

引用计数法:引用加1,失效减1

可达性分析算法:通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。

GC Roots是一些由堆外指向堆内的引用, GC Roots其实不是一组对象,而通常是一组特别管理的指向引用类型对象的指针,这些指针是tracing GC的trace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针,这也是tracing GC算法不会出现循环引用问题的基本保证。因此也容易得出,只有引用类型的变量才被认为是Roots,值类型的变量永远不被认为是Roots。

一般而言,GC Roots 包括(但不限于)如下几种:

Java 方法栈桢中的局部变量;已加载类的静态变量;JNI handles;已启动且未停止的 Java 线程

 

七、强引用,软引用,弱引用,虚引用

JDK1.2之前,Java中引用的定义很传统:如果reference类型的数据存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用

JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

强引用:使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OOM错误,是程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题  使用new来创建

软引用:如果一个对象只具有软引用,那就类似于可有可无的生活用品。内存空间足够,GC就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要GC没有回收它,该对象就可以被程序使用。软引用可以用来实现内存敏感的高速缓存 使用SoftReference类来创建软引用

软引用可以和一个引用队列联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

弱引用:如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用的对象具有更短的生命周期,只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。  使用WeakReference类来创建

弱引用可以和一个引用队列联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收

虚引用主要用来跟踪对象被垃圾回收的活动

一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知

使用 PhantomReference 来创建虚引用

 

 

八、如何判断一个常量是废弃常量

假如在常量池中存在字符串“abc”,如果当前没有任何String对象引用该字符串常量的话,就说明常量"abc"就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc”就会被系统清理出常量池

 

九、如何判断一个类是无用的类

类需要同时满足下面3个条件才能算是“无用的类”:

1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例

2.加载该类的ClassLoader已经被回收

3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

 

finalize()

类似C++的析构函数,用于关闭外部资源。但是try-finally等方式可以做得更好,并且该方法运行代价很高,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用

当一个对象可被回收时,如果需要执行该对象的finalize()方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了finalize()方法自救,后面回收时不会再调用该方法

 

十、垃圾收集有哪些算法,各自的特点

垃圾收集器:Serial收集器、ParNew收集器、Parallel Scavenge收集器、CMS收集器、G1收集器

 

标记-清除算法:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:效率问题;空间问题(标记清楚后会产生大量不连续的碎片)

复制算法:为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完成后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收

标记-整理算法:根据老年代的特点产生的一种标记算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

分代收集算法:当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以必须选择“标记-清除”或“标记-整理”算法进行垃圾收集

 

十一、HotSpot为什么要分为新生代和老年代

为了提升GC效率

 

十二、常见的垃圾回收器有哪些

1、Serial收集器

串行收集器是一个单线程收集器,它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程,直到它收集结束。新生代采用复制算法,老年代采用标记-整理算法

简单高效,适用于Client模式下的虚拟机

优点:与其他收集器的单线程相比,简单而高效。Serial收集器由于没有线程交互的开销,可以获得很高的单线程收集效率。

2、ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样。新生代采用复制算法,老年代采用标记-整理算法。Server模式下的虚拟机

3、Parallel Scavenge收集器

-XX:+UseParallelGC  使用Parallel收集器+老年代串行

-XX:+UseParallelOldGC 使用Parallel收集器+老年代并行

Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,手工优化存在的话可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择

新生代采用复制算法,老年代采用标记-整理算法

4、Serial Old收集器

Serial收集器的老年代版本,它同样是一个单线程收集器。两大用途:一种用途是在JDK1.5及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案

5、Parallel Old收集器

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

 

6、CMS收集器 Concurrent Mark Sweep

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用

并发收集器,第一次实现了让垃圾收集线程与用户线程基本上同时工作

CMS收集器是一种“标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些

初始标记:暂停所有的其他线程,并记录下直接与root相连的对象,速度很快

并发标记:同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断地更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方

重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫

主要优点:并发收集、低停顿

缺点:对CPU资源敏感;无法处理浮动垃圾;使用多个回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生

 

7、G1收集器

G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以及高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征

特点-->

并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让java程序继续执行

分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念

空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于”复制“算法实现的

可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内

 

G1收集器的运作大致分为以下几个步骤:

初始标记

并发标记

最终标记

筛选回收

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)

 堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

 

 

十三、类加载过程

类加载过程:加载-->连接-->初始化

连接过程:验证-->准备-->解析

加载:通过全类名获取定义此类的二进制字节流;将字节流所代表的静态存储结构转换为方法区的运行时数据结构;在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的loadClass()方法)。数组类型不通过类加载器创建,它由Java虚拟机直接创建

加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了

 

十四、类加载器

类加载器,使得Java类可以被动态加载到Java虚拟机中并执行。类加载器用来加载Java类到虚拟机中。Java虚拟机使用Java类的方式:Java源程序(.Java文件)在经过Java编译器编译之后就被转换成Java字节代码(.class)文件。类加载器负责读取Java字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个Java类。通过此实例的newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如Java字节代码可能是通过工具动态生成的,也可能是通过网络下载的。基本上所有的类加载器都是java.lang.Classloader类的一个实例。

java.lang.ClassLoader类-->根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等

系统提供的类加载器:

引导类加载器(bootstrap class loader):用来加载Java的核心库,是用原生代码来实现的,并不继承自java.lang.ClassLoader

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

系统类加载器(system class loader):根据Java应用的类路径(CLASSPATH)来加载Java类。一般来说,Java应用的类都是由它来完成加载的

 

 

JVM中内置了三个重要的ClassLoader,除了BootstrapClassLoader其他类加载器均由Java实现且全部继承自java.lang.ClassLoader:

BootstrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类或者被-Xbootclasspath参数指定的路径中的所有类

ExtensionClassLoader(扩展类加载器):主要负责加载目录%JRE_HOME%lib/ext目录下的jar包和类,或被java.ext.dirs系统变量所指定的路径下的jar包

AppClassLoader(应用程序类加载器):面向用户的加载器,负责加载当前应用classpath下的所有jar包和类

 

每个Java类都维护着一个指向定义它的类加载器的引用,通过getClassLoader()方法就可以获取到此引用

 

类加载器的代理模式:类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。

Java虚拟机是如何判断两个Java类是相同的-->类的全名;加载此类的类加载器

代理模式是为了保证Java核心库的类型安全。所有Java应用都至少需要引用java.lang.Object类,在运行的时候,java.lang.Object这个类需要被加载到Java虚拟机中。如果这个加载过程由Java应用自己的类加载器来完成的话,可能存在多个版本的java.lang.Object类,而且这些类之间不兼容。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

 

十五、类加载机制

类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,会占用很多内存。

真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器,后者称为初始加载器在Java虚拟机中判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。   一个类的定义加载器是它引用的其它类的初始加载器

类加载器在成功加载某个类之后,会把得到的java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。对于一个类加载器实例来说,相同全名的类只加载一次,即loadClass方法不会被重复调用

 

加载-->验证-->准备-->解析-->初始化-->使用-->卸载

 

类加载过程:

加载:加载过程完成以下三件事-->通过类的完全限定名称获取定义该类的二进制字节流;将该字节流表示的静态存储结构转换为方法区的运行时存储结构;在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口

验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

准备:类变量是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。

解析:将常量池的符号引用替换为直接引用的过程

初始化:初始化阶段才真正开始执行类中定义的Java程序代码。初始化阶段是虚拟机执行类构造器方法的过程

 

符号引用 vs 直接引用

符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中

直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在

 

十六、双亲委派模型

每一个类都有一个对应它的类加载器。系统中的ClassLoder在协同工作的时候会默认使用双亲委派模型即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载加载的时候,首先会把该请求委派该父类加载器的loadClass()处理,因此所有的请求最终都应该传送到顶层的启动类加载器BootstrapClassLoader中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器BootstrapClassLoader作为父类加载器。

用户自定义类加载-->应用程序类加载器-->扩展类加载器-->启动类加载器

每个类加载都有一个父类加载器  类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a "parent" class loader. When loading a class, a class loader first "delegates" the search for the class to its parent class loader before attempting to find the class itself. 

 

双亲委派模型好处:

双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了Java的核心API不被篡改。如果不用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如编写一个称为java.lang.Object类的话,那么程序运行的时候,系统就会出现多个不同的Object类

如果不想用双亲委派模型怎么办?

为了避免双亲委派机制,我们可以自己定义一个类加载器,然后重载loadClass()即可

如何自定义类加载器?

除了BootstrapClassLoader其他类加载器均由Java实现且全部继承自java.lang.Classloader。

 

 

如果想要编写自己的类加载器,只需要两步:

 

  • 继承ClassLoader类
  • 覆盖findClass(String className)方法

ClassLoader超类的loadClass方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用findClass方法。

如果要实现该方法,必须做到以下几点:

1.为来自本地文件系统或者其他来源的类加载其字节码。
2.调用ClassLoader超类的defineClass方法,向虚拟机提供字节码。

 

 

标签:Java,对象,虚拟机,线程,内存,JVM,加载
来源: https://www.cnblogs.com/liushoudong/p/12723546.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有