java之jvm
Posted muzinan110
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java之jvm相关的知识,希望对你有一定的参考价值。
1.JVM内存模型
线程独占:栈,本地方法栈,程序计数器
线程共享:堆,方法区
回答以上问题是需回答两个要点:
1. 各部分功能
2. 是否是线程共享
2.JMM与内存可见性
JMM是定义程序中变量的访问规则,线程对于变量的操作只能在自己的工作内存中进行,而不能直接对主内存操作.由于指令重排序,读写的顺序会被打乱,因此JMM需要提供原子性,可见性,有序性保证.
3.类加载与卸载
加载机制-双亲委派模式
双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器.父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.
优点:
避免类的重复加载
避免Java的核心API被篡改
分代回收
分代回收基于两个事实:大部分对象很快就不使用了,还有一部分不会立即无用,但也不会持续很长时间.
年轻代->标记-复制
老年代->标记-清除
回收算法
1.CMS算法
1.7前主流垃圾回收算法,为标记-清楚算法,优点是并发收集,停顿小.
2.G1算法
1.9后默认的垃圾回收算法,特点保持高回收率的同时减少停顿.采用每次只清理一部分,而不是清理全部的增量式清理,以保证停顿时间不会过长
3.ZGC
1.11中提供的高效垃圾回收算法,针对大堆内存设计,可以处理TB级别的堆,可以做到10ms以下的回收停顿时间.
考察点
深入理解JVM内存模型
了解类加载机制
了解内存可见性
了解常用的GC算法实现和适用场景
能根据业务场景选择合适JVM参数和GC算法
加分项
编译器优化
问题排查经验与思路
JVM调优经验和调优思路
了解最新的技术趋势(ZGC和Graalvm)
真题汇总
1.简单描述一下JVM的内存模型
jvm内存模型:方法区、堆、程序计数器 、本地方法栈、虚拟机栈
这里针对方法区、栈、堆、程序计数器做一个说明
这里我们来说个流程:
程序(非多线程)开始运行的时候,在系统中会自动分配一个栈,这个时候程序计数器就开始起到作用了,它会指示jvm对编译之后的字节码的执行方向,同时在执行一个方法的时候就会在栈中分配一个属于方法一个栈帧,方法的局部变量都会存放在这个栈帧中,其生命周期随着方法的结束而释放,这里强调一点的是先进后出的逻辑,堆中的数据当没有对象引用的时候就成了孤立数据,此时就会被GC垃圾回收器对其进行内存释放。
方法区包含了常量池:存放类信息、常量、静态变量、即时编译器编译后的代码等。其中静态成员变量在类装载的时候就进行了创建,在整个程序结束时按序销毁。
静态成员变量在类装载的时候就进行了创建,在整个程序结束时按序销毁。
实例变量在类实例化对象时候创建,在对象销毁的时候销毁。
局部变量在局部范围使用时创建,跳出局部范围销毁。
这里我们说个题外话,在术语中经常会听到编译期和运行期
编译期:就是将源码编译成二进制的.class字节码文件,并将文件放到了磁盘中,编译期相当于只是做了一个翻译的过程
运行期:这块就是我们java解释器将二进制.class字节码解释成程序能识别的程序(将磁盘中的代码放到内存中就是类加载过程)
2.什么时候会触发FullGC?
触发MinorGC(Young GC)
虚拟机在进行minorGC之前会判断老年代最大的可用连续空间是否大于新生代的所有对象总空间
1、如果大于的话,直接执行minorGC
2、如果小于,判断是否开启HandlerPromotionFailure,没有开启直接FullGC
3、如果开启了HanlerPromotionFailure, JVM会判断老年代的最大连续内存空间是否大于历次晋升的大小,如果小于直接执行FullGC
4、如果大于的话,执行minorGC
触发FullGC
老年代空间不足
如果创建一个大对象,Eden区域当中放不下这个大对象,会直接保存在老年代当中,如果老年代空间也不足,就会触发Full GC。为了避免这种情况,最好就是不要创建太大的对象。
持久代空间不足
如果有持久代空间的话,系统当中需要加载的类,调用的方法很多,同时持久代当中没有足够的空间,就出触发一次Full GC
YGC出现promotion failure
promotion failure发生在Young GC, 如果Survivor区当中存活对象的年龄达到了设定值,会就将Survivor区当中的对象拷贝到老年代,如果老年代的空间不足,就会发生promotion failure, 接下去就会发生Full GC.
统计YGC发生时晋升到老年代的平均总大小大于老年代的空闲空间
在发生YGC是会判断,是否安全,这里的安全指的是,当前老年代空间可以容纳YGC晋升的对象的平均大小,如果不安全,就不会执行YGC,转而执行Full GC。
显示调用System.gc
3.Java类加载器有几种,关系怎样的?
JAVA类加载器包括几种?
引导类加载器 bootstrap class loader
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器 extensions class loader
它负责加载JAVA_HOME/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
应用程序类加载器 application class loader
应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
自定义类加载器 java.lang.classloder
就是自定义啦,通过继承java.lang.ClassLoader类的方式
类加载器之间的关系
启动类加载器,由C++实现,没有父类。
拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
自定义类加载器,父类加载器肯定为AppClassLoader。
如何自定义一个类加载器?
通过继承ClassLoad定义一个类加载器。
应用场景
如Tomcat容器,每个WebApp有自己的ClassLoader,加载每个WebApp的ClassPath路径上的类,一旦遇到Tomcat自带的Jar包就委托给CommonClassLoader加载。同包的隔离。另外成熟的开源框架,都有自己的classloade。
4.双亲委派机制的加载流程是怎样的,有什么好处?
双亲委派机制
请注意双亲委派模式中的父子关系并非通常所说的类继承关系。
其工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。
双亲委派机制作用
通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改
双亲委派模式的工作原理的是;如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的双亲委派模式.那么这种模式有什么作用呢?
双亲委派模式优势
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
5.1.8为什么用Metaspace替换掉PermGen?Meatspace保存在哪?
1. PermGen空间的情形
这部分内存空间完全被移除 PermSize和MaxPermSize JVM参数被忽略掉,如果存在的话启动时会发出警告。
2. Metaspace空间分配模型
1) 大部分类的metadata现在分配到本地内存。
2) 用来描述类的metadata的类已被删除。
3. Metaspace容量
1) 默认情况下,类的metadata的分配是受本机可用内存的限制(当然容量要取决于你使用的32位还是64位的JVM以及操作系统可用的虚拟内存)。
2) 一个新的参数可用了(MaxMetaspaceSize),允许你限制本地用于类的metadata的内存容量。如果你没有指定这个参数,Metaspace将会在运行时根据应用的需求自动调整大小。
4. Metaspace垃圾回收
1) 一旦类的metadata使用达到“MaxMetaspaceSize”,死的类和类加载器的垃圾回收器会被触发。
2) 适当的Metaspace的监控&调优将明显地被要求以限制此类垃圾回收的频率或延迟。过度的Metaspace垃圾收集可能是您的应用程序中类,类加载器内存泄漏或容量不够的症状。
5. Java 堆空间的影响
一些其他数据已迁移到Java堆空间。这意味着下一个未来的JDK 8升级您可能会看到Java堆空间的增加。
6. Metaspace监控
1) Metaspace的使用情况可以从HotSpot 1.8的verbose GC log输出。
2) jstat & jVisualVM
6.编译器会对指令做哪些优化?(简答描述编译器的指令重排)
引言:在Java中看似顺序的代码在JVM中,可能会出现编译器或者CPU对这些操作指令进行了重新排序;在特定情况下,指令重排将会给我们的程序带来不确定的结果..
1. 什么是指令重排?
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
2. 数据依赖性
主要指不同的程序指令之间的顺序是不允许进行交互的,即可称这些程序指令之间存在数据依赖性。
主要的例子如下:
名称 代码示例 说明
写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
进过分析,发现这里每组指令中都有写操作,这个写操作的位置是不允许变化的,否则将带来不一样的执行结果。
编译器将不会对存在数据依赖性的程序指令进行重排,这里的依赖性仅仅指单线程情况下的数据依赖性;多线程并发情况下,此规则将失效。
3. as-if-serial语义
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
分析: 关键词是单线程情况下,必须遵守;其余的不遵守。
代码示例:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
分析代码: A->C B->C; A,B之间不存在依赖关系; 故在单线程情况下, A与B的指令顺序是可以重排的,C不允许重排,必须在A和B之后。
结论性的总结:
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
核心点还是单线程,多线程情况下不遵守此原则。
4. 在多线程下的指令重排
首先我们基于一段代码的示例来分析,在多线程情况下,重排是否有不同结果信息:
class ReorderExample
int a = 0;
boolean flag = false;
public void writer()
a = 1; //1
flag = true; //2
Public void reader()
if (flag) //3
int i = a * a; //4
……
上述的代码,在单线程情况下,执行结果是确定的, flag=true将被reader的方法体中看到,并正确的设置结果。 但是在多线程情况下,是否还是只有一个确定的结果呢?
假设有A和B两个线程同时来执行这个代码片段, 两个可能的执行流程如下:
可能的流程1, 由于1和2语句之间没有数据依赖关系,故两者可以重排,在两个线程之间的可能顺序如下:
可能的流程2:, 在两个线程之间的语句执行顺序如下:
根据happens- before的程序顺序规则,上面计算圆的面积的示例代码存在三个happens- before关系:
A happens- before B;
B happens- before C;
A happens- before C;
这里的第3个happens- before关系,是根据happens- before的传递性推导出来的
在程序中,操作3和操作4存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。从图中我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义。
核心点是:两个线程之间在执行同一段代码之间的critical area,在不同的线程之间共享变量;由于执行顺序、CPU编译器对于程序指令的优化等造成了不确定的执行结果。
5. 指令重排的原因分析
主要还是编译器以及CPU为了优化代码或者执行的效率而执行的优化操作;应用条件是单线程场景下,对于并发多线程场景下,指令重排会产生不确定的执行效果。
6. 如何防止指令重排
volatile关键字可以保证变量的可见性,因为对volatile的操作都在Main Memory中,而Main Memory是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。
volatile还有一个作用就是局部阻止重排序的发生,对volatile变量的操作指令都不会被重排序,因为如果重排序,又可能产生可见性问题。
在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写都可以确保变量的可见性。但是实现方式略有不同,例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而volatile变量干脆都是读写内存。
7. 可见性
这里提到的可见性是指前一条程序指令的执行结果,可以被后一条指令读到或者看到,称之为可见性。反之为不可见性。这里主要描述的是在多线程环境下,指令语句之间对于结果信息的读取即时性。
7.简单描述一下volatile可以解决什么问题?如何做到的?
8.简单描述一下GC的分代回收?
为什么要进行分代回收?
JVM使用分代回收测试,是因为:不同的对象,生命周期是不一样的。因此不同生命周期的对象采用不同的收集方式。
可以提高垃圾回收的效率。
Java程序运行过程中,会产生大量的对象,其中有些对象是与业务相关的。比如Http请求的Session对象,线程,Socket
连接等。但是还有一些对象,主要是程序运行过程中生成的临时变量(比如方法中的局部变量),这些对象生命周期会比较短,
比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会
长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有
效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分而治之的思想,进行代的划
分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
如何分代
如下图所示
虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代
(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系
不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
年轻代分三个区。一个Eden区,两个Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还
存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外
一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将
被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时
存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去
过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于
两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
老年代
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的
都是一些生命周期较长的对象。
持久代
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或
者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新
增的类。持久代大小通过-XX:MaxPermSize=进行设置。
什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,
清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对
年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很
大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲
出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge
GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调
节。有如下原因可能导致Full GC:
· 年老代(Tenured)被写满
· 持久代(Perm)被写满
· System.gc()被显示调用
9.G1与CMS的区别?
1、CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现,它的运作过程如下:
1)初始标记 2)并发标记 3)重新标记 4)并发清除
初始标记、从新标记这两个步骤仍然需要“stop the world”,初始标记仅仅只是标记一下GC Roots能直接关联到的对象,熟读很快,并发标记阶段就是进行GC Roots Tracing,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生表动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长点,但远比并发标记的时间短。
优点:并发收集、低停顿。
缺点:
1)CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
2)CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。
3)CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片。当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
2、G1收集器
G1是一款面向服务端应用的垃圾收集器。G1具备如下特点:
1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,
5、G1运作步骤:
1、初始标记;2、并发标记;3、最终标记;4、筛选回收
上面几个步骤的运作过程和CMS有很多相似之处。
初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,
并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。
最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。
最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
10.对象引用有哪几种,有什么特点?
在jdk1.2之前,java中引用的定义:如果引用类型的数据类型中存储的数值代表的是一块内存的起始地址,就称这块内存代表一个引用。在jdk1.2之后,java引入了4种对象引用类型,级别由高到低分别是:强引用、软引用、弱引用和虚引用。
为什么需要不同的引用
对于需要长期运行的应用程序来说,如果无用的对象所占的内存空间无法及时释放的话,那么在一个局部的时间段里就会形成事实上的内存泄露,如果要及时地释放内存,在java中最稳妥的方式就是在使用完对象过后 立即执行“object = null”,当然这这是理想状态。
通过这4种引用类型 强行调用垃圾回收方法 System.gc() 来解决内存泄露的问题,不同的引用方式会有不同的作用,下面我们来一一讲解。
强引用(StrongReference):直接引用对象,内存不足时也不会回收
软引用(SoftReference):内存不足时,回收引用相关联的对象
弱引用 (WeakReference):无论内存是否充足,都回收引用相关联的对象
虚引用(PhantomReference):任何时候都可以被垃圾回收器回收
强引用
强引用是日常编程中最常用的一种引用类型,它的特点是只要引用存在,就永远不会调用垃圾回收方法对其进行释放内存,java虚拟机宁可出现OutOfMemoryError错误停止运行,也会保存内存空间。只用当这个对象的引用被释放掉,对象才会被释放。
正是因为强引用具备这样的特点,所以我们的开发原则是尽量少实例化对象。
强引用是造成java内存泄露的主要原因之一。
软引用
软引用是指非必须引用,在内存足够时是不会对其进行回收,而在内存不足的时候垃圾回收器会将其回收并释放内存。java中软引用对应着SoftReference类,如果想要一个对象被软引用,只需要将其作为参数传入SoftReference类的构造方法中就行了。
软引用主要用于用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真实来源查询这些数据。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,java虚拟机就会把这个软引用加入到与之关联的队列引用中。
弱引用
弱引用相对于软引用,被弱引用的对象拥有更短的内存时间(生命周期)。垃圾回收器一旦发现了被弱引用的对象,就会回收它的内存,不管当前内存空间是不是足够。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些弱引用的对象。对应java中的WeakReference类,使用方法与软引用基本相同,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收器回收,java虚拟机就会把这个软引用加入到与之关联的队列引用中。
弱引用主要应用于监控对象是否被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记
虚引用
顾名思义:形同虚设,虚引用并不会决定对象的生命周期,如果一个对象仅有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。程序也不能通过虚引用访问被引用的对象。
虚引用主要用来跟踪垃圾回收器回收的活动,虚引用与弱应用,软引用的区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象内存之前,把这个虚引用加入到与之关联的队列中。
11.使用过哪些JVM调试工具,主要分析哪些内容?
监视JVM
jps
jstat
jstatd
Jmc
故障排除
jcmd
jinfo
jhat
jmap
jsadebugd
jstack
jps (JavaVirtual Machine Process Status Tool): 虚拟机进程状况工具
命令格式:jps [options] [hostid]
-q : 抑制类名的输出,JAR文件名和传递给main方法的参数,仅生成本地JVM标识符列表。
-m: 显示传递给该main方法的参数。输出可能是null嵌入式JVM。
-l : 显示应用程序main类的完整包名或应用程序的JAR文件的完整路径名。
-v : 显示传递给JVM的参数。
-V : 抑制类名的输出,JAR文件名和传递给main方法的参数,仅生成本地JVM标识符的列表。
-Joption : 传递option给JVM,其中的选项是optionsJava应用程序启动器的参考页面中描述的选项之一。例如,-J-Xms48m将启动内存设置为48 MB。
样例:
jstat(Java Virtual Machine (JVM) statistics):监视Java虚拟机(JVM)统计信息。
命令格式:jstat [ option vmid [interval[s|ms] [count] ]
Interval:间隔时间 count:次数
class:显示类加载器行为的统计信息。
compiler:显示有关Java HotSpot VM即时编译器行为的统计信息。
gc:显示垃圾回收堆行为的统计信息。
gccapacity:显示有关世代及其对应空间容量的统计数据。
gccause:显示有关垃圾回收统计信息(相同-gcutil)的摘要,其中包含最后和当前(适用时)垃圾收集事件的原因。
gcnew:显示新一代行为的统计信息。
gcnewcapacity:显示有关新一代及其相应空间大小的统计信息。
gcold:显示旧版本和Metaspace统计信息的统计信息。
gcoldcapacity:显示有关旧一代大小的统计信息。
gcmetacapacity:显示有关元空间大小的统计信息。
gcutil:显示有关垃圾收集统计信息的摘要。
printcompilation:显示Java HotSpot VM编译方法统计信息。
jinfo(Configuration Info for Java):生成配置信息。
命令格式:jinfo [ option ] pid
-flag 名称 : 打印指定命令行标志的名称和值。
-flag [+ | - ]名称 : 启用或禁用指定的布尔命令行标志。
-flag name = value : 将指定的命令行标志设置为指定的值。
-flags : 打印传递给JVM的命令行标志。
-sysprops : 将Java系统属性打印为名。
jmap(Memory Map for Java):内存映射工具[生成堆转储快照]
命令格式:jinfo [ option ] vmid
-dump:[live,] format = b,file = filename : 以hprof二进制格式转储Java堆filename。live子选项说明是否之dump出存活的对象。
-finalizerinfo : 打印有关正在等待最终确定的对象的信息(linux)。
-heap :显示java堆详细信息,如使用哪种回收器、参数配置、分代状况等(linux)。
-histo [:live] : 显示堆中对象统计信息,包括类、实例数量、合计容量。
-clstats : 打印Java堆的类加载器智能统计。对于每个类加载器,它的名称,它的活动程度,地址,父类加载器以及它加载的类的数量和大小。
-F : -dump或 -histo选项不响应时,该选项强制生成dump快照(不支持live)。
jhat(JVM Heap Analysis Tool):虚拟机堆转储快照分析工具
命令格式:jhat [ options ] 堆转储文件
jstack(Stack Trace for Java):Java堆栈跟踪工具
命令格式:jstack [ options ] pid
-F : jstack[ -l] pid不响应时强制堆栈转储。
-l : 打印有关锁的其他信息,例如所java.util.concurrent拥有的同步器列表。
-m : 打印混合模式堆栈跟踪,其中包含Java和本机C / C ++框架。
以上是关于java之jvm的主要内容,如果未能解决你的问题,请参考以下文章