Java虚拟机

2017-10-15  本文已影响25人  今有所思

1.内存分区(运行时数据区域)

线程私有区(每个线程运行时独享的内存区域

(1)程序计数器

线程所执行字节码的行号指示器。

-Java方法,PC记录的就是下一条需要执行的字节码指令地址(行号)。

-Native方法,则值为空(Undefined)。

注:这是JVM虚拟机中唯一一个没有规定任何OutOfMemoryError的区域。

(2)Java虚拟机栈

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

(3)本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,区别是虚拟机栈为虚拟机执行Java方法服务(也就是字节码),而本地方法栈为虚拟机使用到的Native方法服务

共享内存区域

(4)Java堆

存放对象实例,几乎所有的对象实例都在这里分配内存。线程共享的Java堆可能划分出多个线程私有的分配缓冲区TLAB。Java堆是垃圾收集器管理的主要区域。

堆的划分:新生代和老年代。新生代分为Eden空间,From Survivor空间,To Survivor空间

默认的,新生代( Young )与老年代( Old )的比例的值为1:2 (该值可以通过参数–XX:NewRatio来指定)

注:对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:

a)对分配内存空间的动作进行同步处理-实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;

b)把分配内存的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预

先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB分配完并分配新的TLAB时,才需要同步锁定。

(5)方法区JDK 8被移除)

用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。这区域的内存回收目标主要是针对常量池的回收和类型的卸载。

-运行时常量池:主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用包括了下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

2.堆和栈的区别

1)物理地址

堆的物理地址分配是不连续的,因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种垃圾收集算法。

栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的,所以性能快。

2)内存分配

堆是不连续的,所以分配的内存是在运行期确认的,大小是不固定的。

栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。

3)存放的内容

堆存放的是对象的实例和数组。该区更关注的是数据的存储。

栈存放的是局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。

4)私有还是共享

栈内存属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。

堆内存中的对象对所有线程可见,可以被所有线程访问。

5)异常错误

当Stack满了,会抛出java.lang.StackOverFlowError

当Heap满了,会抛出java.lang.OutOfMemoryError:Java Heap Space Error。

3.垃圾收集算法

※对象存活判定算法(可回收对象的判定)

程序计数器、虚拟机栈、本地方法栈这些区域不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了,垃圾收集器关注的是堆和方法区中的垃圾

1)引用计数算法

算法思想:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器都为0的对象就是不可能再被使用的。

存在的问题:对象之间的相互循环引用问题

2)可达性分析算法

Java和C#中都是采用根搜索算法来判定对象是否存活的。

基本思想:通过一系列名为“大型网站

s”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。

在Java语言里,可作为GC Roots的对象包括下面几种:

(1)虚拟机栈(栈帧中的本地变量表)中引用的对象

(2)方法区中的类静态属性引用的对象

(3)方法区中的常量引用的对象

(4)本地方法栈中JNINative方法)的引用对象

实际上,在可达性分析算法中,要真正宣告一个对象死亡,至少要经历两次标记过程。

Step1如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法或者finalize已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。

Step2如果这个对象被判定为有必要执行finalize方法,那么这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它,如果对象重新与引用链上的任何一个对象建立关联,则对象成功拯救自己

永久代的垃圾收集主要回收两部分内容:

(1)废弃常量

(2)无用的类:判断一个类是无用的类,要同时满足下面3个条件才能算是“无用的类”:

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

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

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

※垃圾收集算法(4个+1个补充算法)

1、标记清除算法(Mark-Sweep

标记清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的可达性分析算法中判定垃圾对象的标记过程

缺点:

(1)标记和清除过程的效率都不高

(2)标记清除后会产生大量不连续的内存碎片

2、复制算法(Copy

优点:

(1)每次只对一块内存进行回收,运行高效

(2)内存分配时只需移动栈顶指针,按顺序分配内存即可,实现简单

(3)内存回收时不用考虑内存碎片的出现

缺点:可一次性分配的最大内存缩小了一半

3、标记-整理算法(Mark-Compact

复制算法比较适合于新生代,在老年代中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存

4、分代收集算法

一般是把Java堆分为新生代和老年代

在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集。

老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清除算法或标记整理算法来进行回收。

补充:

增量算法(IncrementalCollecting)

增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降

什么情况下触发垃圾回收

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Minor GCFull GC

(1)Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC回收新生代

(2)Full GC(老年代回收,一般伴随一次Minor GC)则有几种触发条件:

1)老年代空间不足

2)PermSpace空间不足

3)统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间(空间分配担保)

注意:PermSpace并不等同于方法区,只不过是Hotspot JVM用PermSpace来实现方法区而已,有些虚拟机没有PermSpace而用其他机制来实现方法区。

4.内存分配与回收策略

(1)对象优先在Eden分配

(2)大对象直接进入老年代

(3)长期存活的对象将进入老年代

JVM给每个对象定义一个年龄计数器,当对象在Eden区出生并躲过一次MinorGC,并且Survivor可以容纳的话,将被移入Survivor区,年龄设为1。以后每在Survivor区躲过一次MinorGC,年龄加一岁,当对象年龄加到15岁时,晋升到老年代。当然15岁的默认值可以通过-XX:MaxTenuringThreshold设置。

(4)动态对象年龄判定

有的时候无需到达15岁即晋升老年代。判定方法是如果Survivor区中相同年龄的所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象直接进入老年代

5.空间分配担保

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,

l如果大于,则此次Minor GC是安全的

l如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的;如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。

6.GC停顿原因(Stop the World),如何降低停顿时间

GC停顿原因

在Java语言里面,可作为GC Roots的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。如果要使用可达性分析来判断内存是否可回收的,那分析工作必须在一个能保障一致性的快照中进行——这里“一致性”的意思是整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中,对象引用关系还在不断变化的情况,这点不满足的话分析结果准确性就无法保证。这点也是导致GC进行时必须“Stop The World”的其中一个重要原因,即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的

如何降低停顿时间

(1)CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:

1)初始标记2)并发标记3)重新标记4)并发清除

其中初始标记、重新标记这两个步骤任然需要停顿其他用户线程。初始标记仅仅只是标记出GC ROOTS能直接关联到的对象,速度很快,并发标记阶段是进行GC ROOTS根搜索算法阶段,会判定对象是否存活。而重新标记阶段则是为了修正并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记阶段稍长,但比并发标记阶段要短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

(2)增量算法

7.Java中4种引用(强引用、软引用、虚引用、弱引用)

强引用:如“Object obj =

new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象

软引用:它用来描述一些还有用但并非必需的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。软引用经常用来实现内存敏感型的场景,如缓存。对于Cache,我们希望被缓存的对象最好始终常驻内存,但是如果JVM内存吃紧,为了不发生OutOfMemoryError导致系统崩溃,必要的时候也允许JVM回收Cache的内存,待后续合适的时机再把数据重新Load到Cache中。这样可以系统设计得更具弹性。

弱引用:它也是用来描述非需对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。相比于软引用,只具有弱引用的对象拥有更短暂的生命周期。如果需要用一张很大的HashMap作为缓存表,那么可以考虑使用WeakHashMap,当键值不存在的时候添加到表中,存在即取出其值。

虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。当垃圾收集器确定了某个对象是虚可及对象时,PhantomReference对象就被放在它的ReferenceQueue上,将PhantomReference对象放在ReferenceQueue上也就是一个通知,表明PhantomReference对象引用的对象已经结束,可供收集了。这使你能够刚好在对象占用的内存被回收之前采取行动。

8.垃圾收集器


CMS收集器

基于标记-清除算法实现的,整个过程分为4个步骤:初始标记,并发标记,重新标记,并发清除。其中初始标记和重新标记仍然需要“Stop the World”。

缺点:

(1)对CPU资源非常敏感

(2)无法处理浮动垃圾

(3)有大量空间碎片产生(标记-清除算法)

G1收集器(JDK1.7

特点:

(1)并行+并发。可充分利用CPU资源;

(2)分代收集;

(3)G1从整体看是“标记-整理”算法,从局部(两个Region之间)看,是“复制”算法。不会产生空间碎片;

(4)可预测的停顿。建立可预测的态度时间模型,能让使用者明确指定在一个长度为M毫秒的时间内,消耗在垃圾收集的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

9.类加载机制

虚拟机把描述类的数据Class文件加载到内存,并对数据进行校验转换解析初始化,最终形成可以Java虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。类加载的全过程分为加载,验证,准备,解析和初始化这五个阶段。

1)加载

在加载阶段,虚拟机需要完成以下三件事情:

1)通过一个类的全限定名来获取定义此类的二进制字节流

2)将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构

3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

2)验证

这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

3)准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区进行分配。

4)解析

解析阶段是虚拟机将常量池的符号引用转换为直接引用的过程

5)初始化

在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者说初始化阶段是执行类构造器方法的过程

10.双亲委派模型

每个类加载器都有一个独立的类名称空间。比如我们要加载两个类,如果要比较两个类是否相等只有在这两个类被同一个类加载器加载的前提下,比较才有意义。否则,即使两个类来自同一个class文件,被同一个JVM加载,但是加载它们的类加载器不同,则这两个类就不相等,就相当于两个命名空间中的等价类LoaderA::C和LoaderB::C。

ClassLoader有4个,其中一个是自定义类加载器,还有3个是系统提供的:

-Bootstrap ClassLoader(启动类加载器)

-Extension ClassLoader(扩展类加载器)

-Application

ClassLoader(应用程序类加载器)

双亲委派模型

这个模型要求除了Bootstrap

ClassLoader外,其余的类加载器都要有自己的父加载器。子加载器通过组合来复用父加载器的代码,而不是使用继承。在某个类加载器加载class文件时,它首先委托父加载器去加载这个类,依次传递到顶层类加载器(Bootstrap)。如果顶层加载不了(它的搜索范围中找不到此类),子加载器才会尝试加载这个类。

双亲委派模型最大的好处就是让Java类同其类加载器一起具备了一种带优先级的层次关系。如我们要加载顶层的Java类——java.lang.Object类,无论我们用哪个类加载器去加载Object类,这个加载请求最终都会委托给Bootstrap ClassLoader,这样就保证了所有加载器加载的Object类都是同一个类。如果没有双亲委派模型,那就乱了套了,完全可以搞出Root::Object和L1::Object这样两个不同的Object类。

双亲委派模型有效解决了以下问题:

l每一个类都只会被加载一次,避免了重复加载

l每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)

l有效避免了某些恶意类的加载(比如自定义了java.lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)

类如何确立其在虚拟机中唯一性

(1)类本身

(2)类加载器(每个类加载器都有一个独立的类名称空间)

破坏双亲委派模型

双亲委派模型只是推荐,而非强制,有三次大规模破坏该模型的情况。

(1)loadClass方法做的工作主要为实现双亲委托模型,我们重写的话可能会破坏该模型,在父类加载器无法加载的时候,再调用本身的findClass方法来进行类加载。JDK1.2之前,用户自定类加载器时会重写loadClass方法,这就破坏了双亲委派模型。

(2)模型缺陷:JNDI服务,SPI扩展类是由厂商自己实现,而启动类加载又不可能认识这些类。只好引入线程上下文类加载器Thread Context

ClassLoader。该类加载器可以通过setContextClassLoader()设置,如果创建线程时未设置,将会从父线程继承。如果在应用的全局范围内都没有设置,那就默认是AppClassLoader。有了这个,JNDI服务就可以去加载所需的SPI扩展代码,也就是父类加载器请求子类加载器去完成类加载的动作。这其实也就违背了双亲委派模型的一般性原则,但无可奈何。

(3)程序动态性的追求:OSGi。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构

11.静态分派和动态分派

Human man = new Man();

Human woman = new Woman();

“Human”称为变量的静态类型,“Man”称为变量的实际类型

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载,静态分派发生在编译阶段

在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。动态分派的典型应用是方法重写,动态分派发生在运行阶段

Java是静态多分派,动态单分派。

12.内存泄漏如何检测,如何定位,如何解决

内存泄露的定义:当某些对象不再被应用程序所使用,但是由于仍然被引用而导致垃圾收集器不能释放。

内存泄漏:

(1)过期引用(长生命周期的对象持有短生命周期对象的引用)

(2)缓存

(3)监听器和其他回调

4)各种连接(Java中的连接包括数据库连接、网络连接和I/O连接)

如何定位:

先通过内存映像分析工具(Eclipse

Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚是出现了内存泄漏还是内存溢出。

如果是内存泄漏,进一步查看泄漏对象的引用链,明确泄漏对象是怎么与GC Roots相连而导致垃圾收集器无法自动回收它们。明确引用链后,根据引用链的信息基本就能准备的定位是哪段代码发生了内存泄漏,修改这部分代码。

如果不存在内存泄漏,内存中的对象确实都必须活着,检查JVM堆参数(-Xmx和-Xms)以及服务器内存,看看堆内存是不是还可以稍微调大点儿,同时,检查代码,看看是不是存在生命周期过长的对象,尝试减小程序运行时对内存的消耗。

13.JVM参数设置和调优

参数设置:

堆的最小值:-Xms

堆的最大值:-Xmx

栈容量-Xss

调优工具

1.jps:用于输出VM中运行的进程状态信息

2.jstat:用于监视虚拟机运行时状态信息

3.jinfo:显示虚拟机配置信息

4.jmap:生成虚拟机的内存转储快照(heapdump文件)

5.jhat:分析heapdump文件

6.jstack:显示虚拟机的线程快照

7.可视化工具:JConsoleVisualVM

调优经验:

性能监控:问题没有发生,你并不知道你需要调优什么?此时需要一些系统、应用的监控工具来发现问题。

性能分析:问题已经发生,但是你并不知道问题到底出在哪里。此时就需要使用工具、经验对系统、应用进行瓶颈分析,以求定位到问题原因。

性能调优:经过上一步的分析定位到了问题所在,需要对问题进行解决,使用代码、配置等手段进行优化。

GC优化的目的

1)将转移到老年代的对象数量降低到最小;

2)减少full GC的执行时间。

为了达到上面的目的,一般地,你需要做的事情有:

1)减少使用全局变量和大对象;

2)调整新生代的大小到最合适;

3)设置老年代的大小为最合适;

4)选择合适的GC收集器。

简单思路

1)避免新生代大小设置过小

当新生代设置过小时,会产生两种比较明显的现象,一是minor GC次数频繁,二是可能导致minor GC对象直接进入老年代。当老年代内存不足时,会触发Full GC。

2)避免新生代设置过大

新生代设置过大,会带来两个问题:一是老年代变小,可能导致Full GC频繁执行;二是minor GC执行回收的时间大幅度增加。

3)避免Survivor区过大或过小

-XX:SurvivorRatio参数的值越大,就意味着Eden区域变大,minor GC次数会降低,但两块Survivor区域变小,如果超过Survivor区域内存大小的对象在minor GC后仍没被回收,则会直接进入老年代,

-XX:SurvivorRatio参数值设置过小,就意味着Eden区域变小,minor GC触发次数会增加,Survivor区域变大,意味着可以存储更多在minor GC后任存活的对象,避免其进入老年代。

4)合理设置对象在新生代存活的周期

新生代存活周期的值决定了新生代对象在经过多少次Minor GC后进入老年代。因此这个值要根据自己的应用来调优,JVM参数上这个值对应的为-XX:MaxTenuringThreshold,默认值为15次。

上一篇下一篇

猜你喜欢

热点阅读