《Java虚拟机》必知必会——十四个问题总结(内存模型+GC)

引用:https://www.2cto.com/kf/201608/543537.html

一、Java概述

1、Java相较于PHP、C#、Ruby等一样很优秀的编程语言的优势是什么?

(1)体系结构中立,跨平台性能优越。Java程序依赖于JVM运行,javac编译器编译Java程序为平台通用的字节码文件(.class),再由JVM与不同操作系统匹配,装载字节码并解释(也有可能是编译,会在第三个问题中说到)为机器指令执行。

(2)安全性优越。通过JVM与宿主环境隔离,且Java的语法也一定程度上保障了安全,如废弃指针操作、自动内存管理、异常处理机制等。

(3)多线程。防止单线程阻塞导致程序崩溃,分发任务,提高执行效率。

(4)分布式。支持分布式,提高应用系统性能。

(5)丰富的第三方开源组件Spring、Struts、Hibernate、Mybatis、Quartz等等等等。

 

2、字节码是什么?.class字节码文件是什么?

(1)字节码是包含Java内部指令集、符号集以及一些辅助信息的能够被JVM识别并解释运行的符号序列。字节码内部不包含任何分隔符区分段落,且不同长度数据都会构造成n个8位字节单位表示。

(2).class里存放的就是Java程序编译后的字节码,包含了类版本信息、字段、方法、接口等描述信息以及常量池表,一组8位字节单位的字节流组成了一个字节码文件。

 

3、JVM是什么?HotSpot虚拟机有什么特点?

JVM全称Java Visual Machine,Java虚拟机。是Java程序的运行环境,主要负责装载字节码文件,并解释或编译成对应平台的机器指令执行。

我们使用最多的是JDK缺省自带的HotSpot虚拟机,使用解释器加编译期并存架构方案。一开始的时候使用解释器,使编译未结束时就可以解释字节码为本地机器指令执行,提高效率。编译器用在HotSpot的热点探索功能上,在存在频繁调用的方法或循环次数较多的代码时,就会把这类代码块标记为“热点代码”,通过内嵌的双重JIT(Just in time compiler)将字节码直接编译成对应机器指令,以提高效率。

 

 

 

二、Java内存模型

 

1、PC计数器

线程私有,用于记录当前线程正在执行字节码的地址,如果执行的是native本地方法,PC计数器为空。

 

2、Java栈

线程私有,也叫作Java虚拟机栈,用于存储栈帧,栈帧的入栈出栈过程即方法调用到执行结束的过程。栈帧中主要存放方法执行所需的局部变量表(包括局部变量的声明数据类型、对象引用等)、操作数栈、方法出口等信息。

 

3、本地方法栈

与Java栈功能类似,只是用于存储native本地方法的相关信息。

 

4、Java堆

线程公用,用于存放对象实例,包括数组,也叫GC区,是GC主要工作的区域。也正是如此,由于GC频率过快与效率不高,堆区的可能成为JVM性能瓶颈,于是考虑到性能,堆区不再是对象内存分配的唯一选择。这里就涉及到了对象的逃逸分析与栈上分配。

逃逸分析就是用来分析对象的作用域是否在方法内部,当方法返回了当前类实例对象、方法中为当前类成员变量赋值、方法中引用当前类成员变量的值时就会发生逃逸,依然在堆上分配内存。但当对象的作用域就在方法内时,比如在方法内创建了该类的实例,没有返回、没有引用,则这种情况就直接在Java栈上分配内存,随着栈帧的出栈释放空间,减轻了堆区GC的压力。

 

5、方法区

线程公用,存储了每一个Java类的结构信息,比如:字段、各种方法的字节码内容数据、运行时常量池等。方法区也被称为永久带。一般没有显示要求,GC只对方法区中的常量池回收以及类型卸载。

 

6、运行时常量池

属于方法区的一部分,类加载器将类的字节码文件加载如JVM中后,会把字节码文件中的常量池表转化为运行时常量池。

 

 

 

三、Java垃圾回收机制

 

1、常见的标记可用对象的算法有哪些?

(1)引用计数法:每个对象都创建一个私有的引用计数器,当该对象被其他对象引用时(出现在等号右边),引用计数器加1;当不再引用时,引用计数器减一;当引用计数器为0时,对象即可被回收。这种方式存在着当两个对象互相引用时,二者引用计数器值都不为0无法被回收的问题;

(2)根搜索算法:JVM一般使用的标记算法,把对象的引用看作图结构,由根节点集合出发,不可达的节点即可回收,其中根节点集合包含的如下5种元素:

1、Java栈中的对象引用;

2、本地方法栈中的对象引用;

3、运行时常量池的对象引用;

4、方法区中静态属性的对象引用;

5、所有Class对象;

 

2、常见的垃圾回收算法有哪些?JVM使用哪种?

(1)标记-清除算法:分两个阶段执行,第一个阶段标记可用对象,第二个阶段清除垃圾对象;这个方法很基础简单,但效率低下,而且会产生内存碎片(不连续的内存空间),无法再次分配给较大对象。

(2)复制算法:被广泛用于新生代对象的回收。将内存分为两个区域,新对象都分配在一个区域中,回收时将可用对象连续复制到另一个区域,回收完成后,新对象分配在有对象的区域,循环往复。这种算法不会产生内存碎片,且效率较高,但因为同时只有一个区域有效,会导致内存利用率不高。

(3)标记-整理算法:被应用于老年代对象的回收。这种算法与标记清除算法类似,第一个阶段标记可用对象,第二个阶段将可用对象移动到一段连续的内存上,解决了标记-清除算法会产生内存碎片的缺点。

(4)分代回收算法:在HotSpot虚拟机中,基于分代的特点(堆内存可进一步分为年轻代、老年代,老年代存放存活时间较长的对象),JVM GC使用分代回收算法。

年轻代使用复制算法:分为一个较大的Eden区与两个较小的、等大小的Survivor区(From Space与To Space),比例一般是8:1:1。新对象都分配在Eden区,当GC发生时(新生代的GC一般叫做Minor GC),将Eden区与From区中的可用对象复制到To区中,From Space与To Space互换名称,循环方法。直到发生如下两种情况,对象进入老年代:

1′ From区内的对象已经达到存活代数阀值(经过GC的次数达到设定值),GC时不会进入To区中,直接移动至老年代;

2′ 在回收Eden区与From区后,超出To区可容纳范围,则直接将存活对象移动至老年代。

《《Java虚拟机》必知必会——十四个问题总结(内存模型+GC)》

老年代使用标记-整理算法:当老年代满的时候,会触发Full GC(新生代与老年代一起进行GC)。

 

3、常见的垃圾回收器有哪些?有什么特点?适合应用与什么场景?

(1)Serial收集器

年轻代采用复制算法、串行回收、与“Stop the world”机制(GC时停止其他一切工作),适用于单核CPU环境,绝对不推荐应用于服务器端。

Serial提供了老年代的回收器Serial Old,采用标记-整理算法,其他特性与新生代一致。

Serial+Serial Old适合客户端场景。

(2)ParNew收集器

相当于Serial的多线程版本,并行回收,年轻代同样采用复制算法与“Stop the world”机制,适用于多核CPU、低延迟环境,推荐应用于服务器场景。

(3)Parallel收集器

与ParNew类似,复制算法、并行回收、“Stop the world”机制,但是与ParNew不同,Parallel可以控制程序吞吐量大小,也被称为吞吐量优先的垃圾收集器。

与Serial类似,Parallel也有老年代版本,Parallel Old,同样采用标记整理-算法。

Parallel+Parallel Old非常适用于服务器场景。

(4)CMS收集器

与Parallel的高吞吐对应,CMS就是为高并发、低延时而生的。采用标记-清除算法、并行回收、“Stop the world”。因为采用了标记-清除算法,会产生大量内存碎片,要慎重使用。

(5)G1收集器

是一款基于并行、并发、低延时、暂停时间可控的区域化分代式垃圾回收器。

具有革命意义的设计,放弃了堆区年轻代、老年代的划分方案,而是将堆区或分成约2048个大小相同的独立Region块。

 

4、GC的优化方案?

基本的原则就是尽可能地减少垃圾和减少GC过程中的开销。其中需要注意,JVM进行次GC的频率很高,但因为Minor GC占用时间极短,所以对系统产生的影响不大。更值得关注的是Full GC的触发条,具体措施包括以下几个方面:

(1)不要显式调用System.gc()

调用System.gc()也仅仅是一个请求(建议)。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。但即便这样,很多情况下它会触发Full GC,也即增加了间歇性停顿的次数。

(2)尽量减少临时对象的使用

临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,也就减少了Full GC的概率。

(3)对象不用时最好显式置为Null

一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

(4)尽量使用StringBuffer,而不用String来累加字符串

由于String是常量,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

(5)能用基本类型如Int,Long,就不用Integer,Long对象

基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

(6)尽量少用静态对象变量

静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

(7)分散对象创建或删除的时间

集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行Full GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

 

 

5、Java即使有了GC也会出现的内存泄漏情况?举例说明。

1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。

 

Static Vector v = new Vector();

for (int i = 1; i<100; i++)

{

Object o = new Object();

v.add(o);

o = null;

}

 

在这个例子中,代码栈中存在Vector对象的引用v和Object对象的引用o。在For循环中,我们不断的生成新的对象,然后将其添加到Vector对象中,之后将o引用置空。问题是当o引用被置空后,如果发生GC,我们创建的Object对象是否能够被GC回收呢?答案是否定的。因为,GC在跟踪代码栈中的引用时,会发现v引用,而继续往下跟踪,就会发现v引用指向的内存空间中又存在指向Object对象的引用。也就是说尽管o引用已经被置空,但是Object对象仍然存在其他的引用,是可以被访问到的,所以GC无法将其释放掉。如果在此循环之后,Object对象对程序已经没有任何作用,那么我们就认为此Java程序发生了内存泄漏。

 

2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

 

3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

    原文作者:孤独的代码
    原文地址: https://www.cnblogs.com/xxj-bigshow/p/8632649.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞