JVM虚拟机面试题

1入门部分

1.1为什么要学习JVM?
学习JVM主要有两方面的原因:
1.从自身利益出发来讲,学习JVM主要是因为中高级程序员在面试时会提到相关问题。
2.从提升自身技术水平来讲,深入理解JVM可以帮助我们从平台的角度去提高解决问题的能力。例如
(1)有效防止内存泄漏(Memory leak)
(2)优化线程锁的使用(Thread Lock)
(3)科学进行垃圾回收(Garbage collection)
(4)提高系统吞吐率(throughtput)
(5)降低延迟(Delay),提高性能(performance)

1.2你了解哪些JVM产品?
市场主流JVM主要有:
1.由Sun公司研发,于2010年被Oracle公司收购的HotSpot VM
2.由BEA公司研发,于2008年被Oracle公司收购的JRockit VM
3.IBM内部使用的J9 VM
4.TaobaoJVM:ALiJVM团队开发,基于OpenJDK开发了AlibabaJDK
注:HotSpot是目前应用最官方,最主要的一款JVM虚拟机

1.3JVM的构成有哪几部分?
JVM主要由四个部分组成:
1.类加载系统(ClassLoader System),主要负责将类加载到内存中去。
2.运行时数据区(Runtime Data Area),主要负责存储数据的信息(例如对象、方法等等)。
3.执行引擎(Execution Engine),负责主要负责解释执行的字节码/执行CG操作等。
4.本地库交界口(Native Interface)负责融合不同的编程语言为Java所用。

2类加载部分

2.1你知道哪些类加载器?
1.BootstrapClassLoader:启动类加载器/根类加载器
2.ExtClassLoader:扩展类加载器
3.AppClassLoader:应用程序加载器/系统类加载器

2.2什么是双亲委派类加载模型?
1.概念:类加载器受到了类加载的请求后,不会自己先行加载,而是将此请求委托给父类加载器去执行;如果父类加载器也存在父类加载器,则继续向上委托,一次递归,请求最终到达顶层的启动加载器;如果父类加载器可以完成类加载任务,则成功返回。如果不能完成,子加载器才尝试自己去加载。父类加载器一层层向下分配任务,如果子类加载器能加载则加载此类。如果将加载任务分配到系统类加载器仍无法加载此类,则抛出异常。
2.意义:避免类的重复加载。
3.图解:
Alt

2.3双亲委派方式加载类有什么优势、劣势?
1.优点
(1)避免类被重复加载,确保一个类的全局唯一性
(2)保护程序的安全,防止核心API被随意的篡改
2.缺点
由于检查类是否加载的委派过程是单向的,这会导致顶层的类加载器无法访问底层的类加载器所加载的类。

2.4描述一些类加载时候的基本步骤是怎样的?
按照Java虛拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
类加载步骤

其中,验证,准备,解析3个部分统称为链接(Linking)
验证、准备、解析=

2.5什么情况下会触发类的加载?
以下情况会触发类的初始化:
1.遇到new,getstatic,putstatic,invokestatic这4条指令;
2.使用java.lang.reflect包的方法对类进行反射调用;
3.初始化一个类的时候,如果发现其父类没有进行过初始化,则先初始化其父类(注意!如果其父类是接口的话,则不要求初始化父类);
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类;
5.当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则先触发其类初始化;

2.6类加载时静态代码块一定会执行吗?
不一定。类加载的方式有多种,并不是每一种方式都能执行静态代码块。
在执行的时候加上以下一句JVM的运行参数,可以观看类加载过程
-XX:+TraceClassLoading。
类加载时是否会执行静态代码块取决于加载的方式。

2.7如何理解类的主动加载和被动加载?
主动/被动加载又称为主动/被动引用

类的主动引用有(一定发生类的初始化)
1.new一个对象
2.调用类的静态成员(除了final常量)和静态方法
3.使用java.lang.reflect包的方法对类进行反射调用
4.当虚拟机启动,java Hello,则一定会初始化Hell类,先启动main方法所在的类
5.当初始化一个类,如果父类没有被初始化,则会先初始化它的父类

类的被动引用(不会发生类的初始化)
1.当访问一个静态类时,只有真正声明该静态类的域才会被初始化
2.通过数组定义类的引用,不会触发该类的初始化
3.引用常量不会触发此类的初始化

2.8为什么要自己定义类加载器,如何定义?
自定义类加载器的原因如下:
隔离加载类、修改类的加载方式、扩展加载源、防止源码泄露

自定义类加载器实现步骤:
1.可以通过继承抽象类javalangClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
2.在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
3.在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

3字节码增强部分

3.1为何要学习字节码?
了解字节码可以准确且直观的理解java中更深层次的东西。因为在任何一种高级语言里面,我们解释一个程序如何运作都是跟两个方面是有关系的。第一个方面,就是本身语言的规范,这一点就是规范人的意识以及程序的意识,如同国家领导出台的文件。第二个方面,虚拟机,本地机器具体的实施,这一点就如同地方官员践行政策,为了达到文件的指导思想,也为了结合实际,会有一些具体问题具体分析的存在。故会和“规范”有所出入,这是非常正常的。

一般而言,在我们初学代码的时候,我们学的就是规范,但是在具体实践的过程中(工作或者解题的时候)一般是以实际情况为准,也就是要用字节码与虚拟机推导。Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点;Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

3.2如何解读字节码内容?
1.源代码,java文件经过编译后,可以通过notepad++(需要安装一下HEX-Editor插件)打开.class文件,文件内容默认是一种16进制的格式。
2.使用Idea插件jclasslib。代码在编译后,我们可以在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息。

3.3字节码内容由哪几部分构成?
1.magic(魔数)
2.minor_version(次版本号)
3.major_version(主版本号)
4.constant_pool_count(常量池计数器)
5.constant_pool[constant_pool_count-1](常量池)
6.access_flags(类的访问标志)
7.this_class(当前类名索引值)
8.super_class(父类名索引值)
9.interfaces_count(接口计数)
10.interfaces[interfaces_count](接口数组)
11.fields_count(成员变量计数)
12.fields[fields_count](成员变量数组)
13.methods_count(方法计数)
14.methods[methods_count](方法数组)
15.attributes_count(属性计数)
16.attributes[attributes_count](属性数组)

3.4什么是字节码增强?
Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。Java字节码增强主要是为了减少冗余代码,提高性能等。

3.5为什么要进行字节码增强?
掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

3.6你了解哪些字节码增强技术?
1.ASM
cglib是基于ASM上实现的,ASM常用于自己操作字节码的需求,因为它可以在被类加载之前动态地修改生成,也可以直接生成.class文件在放入jvm。
ASM api是通过ClassWriter接口基于访问者模式,让我们使用ClassVisitor,MethodVisitor,FieldVisitor API接口进行回调实现。
ASM是在汇编指令层次级别上操作字节码的,需要使用者有一定的class组织结构和jvm汇编基础。
2.java assist
JBoos旗下的一个产品,可以直接通过java编程的方式动态生成类或者改变类结构,操作较ASM更为简单。
3.Java Agen
Java Agent是Java Instrumentation API的一部分,它提供了向现有已编译的Java类添加字节码的功能,相当于字节码插桩的入口。可以侵入运行在JVM上的应用程序,进而修改应用程序中各种类的字节码。

3.7什么是热替换以及如何实现?
热替换是在不停止正在运行的系统的情况下进行类(对象)的升级替换;
实现步骤:
1.创建业务service类,将此类作为字节码增强对象
2.创建Transformer对象,用于对CycleService对象进行功能增强
3.建Agent对象,用于调用DefaultClassTransformer对象执行字节码增强,在Agent中可定义两个方法进行不同时间点进行增强:
premain方法,此方法在main方法执行之前执行。(方法声明固定写法)
agentmain方法,此方法在main方法启动后,也就时候程序运行时进行执行。(方法声明固定写法)
4.添加maven插件用于对项目进行打包
依赖中,Premain-Class和Agent-Class用于指定代理类,Can-Redefine-Classes 是否需要重新定义所有类,默认为false,可选。Can-Retransform-Classes 是否需要retransform,默认为false,可选。依赖添加后,先执行maven clean对原有类文件进行清除,然后执行maven package对项目进行打包。
说明,当一个代理jar包中的manifest文件中,既有Premain-Class又有Agent-Class时,如果以命令行方式在VM启动前指定代理jar,则使用Premain-Class;反之如果在VM启动后,动态添加代理jar,则使用Agent-Class。
5.创建CycleServiceTests类,对CycleService对象进行调用,也就是启动服务,假如需要在此服务启动之前进行增强,可以考虑在idea的vm参数中添加-javaagent:E:\TCGBIV\DEVCODES\CSDNCODES\cgb2202codes\01-java\target\01-java-1.0-SNAPSHOT.jar,这里的jar位
6.添加tools依赖
程序启动之后,通过VirtualMachine 的 attach api加载 Java Agent,这组 api 其实是 JVM 进程之间的的沟通桥梁,底层通过socket 进行通信,JVM A 可以发送一些指令给JVM B,B 收到指令之后,可以执行对应的逻辑,比如在命令行中经常使用的 jstack、jps 等,很多都是基于这种机制实现的。VirtualMachine 在jdk的lib下面的tools.jar中,如果不在classpath的话,要加进去。可通过添加依赖的方式进行tools的依赖配置。
7.创建AgentInstrumentTests类
8.分别运行CycleServiceTests、AgentInstrumentTests类进行测试。

4JVM运行内存部分

4.1JVM运行内存是如何划分的?
根据《Java虚拟机规范》的规定,运行时数据区通常包括这几个部分:程序计数器(Program Counter Register)、Java栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、堆(Heap)。
JVM内存划分

4.2JVM中的程序计数器用于做什么?
程序计数器是用来存储指向下一条指令的地址,也可以看作是当前线程执行的字节码的行号指示器。
计算机在任何时刻,一个处理器(或者说一个内核)都只会执行一条线程中的指令。因此为了线程切换后能恢复到正确的执行位置,每个线程都有独立的程序计数器。
如果线程正在执行Java中的方法,程序计数器记录的就是正在执行虚拟机字节码指令的地址,如果是Native方法,这个计数器就为空(undefined),因此该内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的区域。

4.3JVM虚拟机栈的结构是怎样的?
Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行时的内存模型,解决的是程序运行时数据的操作问题,即程序如何执行,或者说如何处理数据。而堆解决的是数据存储的问题,即数据怎么放,放哪里。
其结构如图所示:
虚拟机栈结构

4.4JVM虚拟机栈中局部变量表的作用是什么?
局部变量表:Local Variables,被称为局部变量数组或本地变量表 ,处于虚拟机栈中。局部变量存放了编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、 int、float、long、double)、对象引用(reference类型 ,它并不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对相象关的位置)returnAddress类型(指向了一条字节码指令的地址)。
这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double的会占用两个变量槽(volatile关键字修饰这两种变量是可以保证原子性的),其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。这里说的“大小”是指变量槽的数量,虚拟机真正使用多大的内存空间(譬如照1个变量槽占用32个比特、64个比特,或者更多)来实现 一个变量槽,这是完由由具体的虚拟机实现自行决定的事情。

4.5JVM堆的构成是怎样的?
堆:内存占用最大,包括新生代(new space)和老年代(old space),新生代包含伊甸园(Eden)区、From Survivor空间、To Survivor空间.默认情况下按照8:1:1的比例进行分配。
堆是Java虚拟机所管理的内存中最大的一块存储区域,堆内存被所有线程共享.主要存放使用new关键字创建的对象.所有对象实例以及数组都要再对上分配.垃圾收集器就是根据GC算法,收集堆上对象所占用的内存空间(收集的是对象占用的空间,而不是对象本身). 堆中对象都需要考虑线程安全的问题,有垃圾回收机制。
新生代存储“新生对象”,我们新创建的对象存储再年轻代中,当年轻代中内存不足以放下新对象时,则会触发Minor GC,清理年轻代内存空间。
老年代存储长期存活的对象和大对象,年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储,老年代空间不足以存放新晋升的对象时,则会触发Full GC,Full GC是清理整个堆空间,包括年轻代和老年代,如果Full GC后,堆中仍无法储存对象,则会抛出OutOfMemory内存溢出异常。
至于幸存区的两个空间,在垃圾回收阶段解释其作用。

4.6Java对象分配内存的过程是怎样的?
1.编译器通过逃逸分析(JDK8已默认开启),确定对象是在栈上分配还是在堆上分配
2.如果是在堆上分配,则首先检测是否可在TLAB(Thread Local Allocation Buffer)上直接分配。
3.如果TLAB上无法直接分配则在Eden加锁区进行分配(线程共享区)。
4.如果Eden区无法存储对象,则执行Yong GC(Minor Collection)。
5.如果Yong GC之后Eden区仍然不足以存储对象,则直接分配在老年代。
说明:在对象创建时可能会触发Yong GC
6.新生代由Eden 区和两个幸存区构成(假定为s1,s2), 任意时刻至少有一个幸存区是空的(empty),用于存放下次GC时未被收集的对象。
7.GC触发时Eden区所有”可达对象”会被复制到一个幸存区,假设为s1,当幸存区s1无法存储这些对象时会直接复制到老年代。
8.GC再次触发时Eden区和s1幸存区中的”可达对象”会被复制到另一个幸存区s2,同时清空eden区和s1幸存区。
9.GC再次触发时Eden区和s2幸存区中的”可达对象”会被复制到另一个幸存区s1,同时清空eden区和s2幸存区.依次类推。
10.当多次GC过程完成后,幸存区中的对象存活时间达到了一定阀值(可以用参数 -XX:+MaxTenuringThreshold 来指定上限,默认15),会被看成是“年老”的对象然后直接移动到老年代。

4.7JVM年轻代幸存区设置的比较小会有什么问题?
当伊甸园区被回收时,对象需要拷贝到幸存区。假如幸存区比较小,拷贝的对象比较大,对象就会直接存储到老年代,这样会增加老年代GC的频率。而分代回收的思想就会被弱化。

4.8JVM年轻代伊甸园区设置的比例比较小会有什么问题?
伊甸园设置的比较小,会增加GC的频率,可能会导致STW的时间边长,影响系统性能。

4.9JVM堆内存为什么要分成年轻代和老年代?
1.新生代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。
2.老年代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。

4.10如何理解JVM方法区以及它的构成是怎样的?
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。多个线程同时加载统一个类时,只能有一个线程能加载该类,其他线程只能等等待该线程加载完毕,然后直接使用该类,即类只能加载一次。方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:Metaspace。

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
类信息包括对每个加载的类型(类class、接口interface、枚举enum、注解annotation)以及属性和方法信息。
常量信息可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

4.11什么是逃逸分析以及可以解决什么问题?
逃逸分析(Escape Analysis)本质上是JVM(以下的JVM均代表Hotspot虚拟机)即时编译器(JIT)的一种分析对象作用域的算法,或者可以称之为是一种用于优化JVM的分析技术。逃逸分析不是直接用来优化代码的技术,它为JVM编译器其他优化技术提供必要的分析依据。逃逸分析的基本行为——分析对象作用域。

何为逃逸?
逃逸分为两种:方法逃逸和线程逃逸。
方法逃逸:一个对象在方法内被new后,被外部方法所引用,例如作为参数传递到外部方法,这样此对象发生了逃逸。
线程逃逸:一个对象在方法内被new后,被外部线程访问,例如赋值给类变量或直接被其他线程访问,这样此对象发生了逃逸。

既然有了逃逸分析作为依据,对这个对象就可以进行一些高效的优化。
栈上分配(Stack Allocation):让对象分配在栈上,对象所占用的空间随着栈帧出栈而释放,这样能给GC减轻不少的压力。
同步消除(Synchrogazation Elimination):又叫锁消除或同步省略。当确认一个对象不会发生线程逃逸,那么就可以消除耗时的线程同步过程。
标量替换(Scalar Replace):指把一个对象替换为若干基本数据类型来访问。Java中标量指的是类似于int、long及reference等不能再进一步分解的数据类型。相对的,可以再分解的数据类型称为聚合量(Aggregate)也就是最常见的对象。有的对象可能不需要在堆上分配作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是被创建成一个个局部变量在存储在栈上(栈上分配),这样大概率会被分配在CPU高速寄存器中,也为之后的优化提供了条件。

4.12何为内存溢出以及导致内存溢出的原因?
内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。

引起内存溢出的原因有很多种,常见的有以下几种:
内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
代码中存在死循环或循环产生过多重复的对象实体;
使用的第三方软件中的BUG;
启动参数内存值设定的过小;

内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况。

4.13何为内存泄漏以及内存泄漏的原因是什么?
内存泄露是指:内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。
常见的内存泄露造成的原因
1、单例造成的内存泄漏
由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
2、非静态内部类创建静态实例造成的内存泄漏
3、Handler造成的内存泄漏例如创建匿名内部类的静态对象
4、线程造成的内存泄漏例如AsyncTask和Runnable
5、资源未关闭造成的内存泄漏
6、使用ListView时造成的内存泄漏
7、集合容器中的内存泄露
8、WebView造成的泄露

4.14JAVA中的四大引用你知道多少?
引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。
1.强引用(StronglyReference)
强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
2.软引用(SoftReference)
如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。
软引用适合做缓存,在内存足够时,直接通过软引用取值,无需从真实来源中查询数据,可以显著地提升网站性能。当内存不足时,能让JVM进行内存回收,从而删除缓存,这时候只能从真实来源查询数据。
3. 弱引用(WeakReference)
弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4. 虚引用(PhantomReference)
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用

5JVM垃圾回收部分

#5.1何为GC以及为和要GC?
GC(Garbage Collection)即垃圾回收,是对内存中的垃圾对象,采用一定的算法进行内存回收的一个动作。比方说,java中的垃圾回收会对内存中的对象进行遍历,对存活的对象进行标记,其未标记对象可认为是垃圾对象,然后基于特定算法进行回收。
学习GC的目的是更加深入的了解GC的工作机制,可以写出更好的Java应用,并且提供开发的效率,同时GC也是进军大规模应用开发的一个前提。
#5.2你知道哪些GC算法?
1.引用计数算法(Reference counting)
每个对象在创建的时候,就给这个对象绑定一个计数器。每当有一个引用指向该对象时,计数器加一;每当有一个指向它的引用被删除时,计数器减一。这样,当没有引用指向该对象时,该对象死亡,计数器为0,这时就应该对这个对象进行垃圾回收操作。
为每个对象额外存储一个计数器RC,根据RC的值来判断对象是否死亡,从而判断是否执行GC操作。
优点:
简单、计算代价分散、“幽灵时间”短(幽灵时间指对象死亡到回收的这段时间,处于幽灵状态)
缺点:
不全面(容易漏掉循环引用的对象)、并发支持较弱、占用额外内存空间
2.标记–清除算法(Mark-Sweep)
为每个对象存储一个标记位,记录对象的状态(活着或是死亡)。分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行GC操作。
优点
最大的优点是,相比于引用计数法,标记—清除算法中每个活着的对象的引用只需要找到一个即可,找到一个就可以判断它为活的。此外,这个算法相比于引用计数法更全面,在指针操作上也没有太多的花销。更重要的是,这个算法并不移动对象的位置(后面俩算法涉及到移动位置的问题)。
缺点
很长的幽灵时间,判断对象已经死亡,消耗了很多时间,这样从对象死亡到对象被回收之间的时间过长。每个活着的对象都要在标记阶段遍历一遍;所有对象都要在清除阶段扫描一遍,因此算法复杂度较高。没有移动对象,导致可能出现很多碎片空间无法利用的情况。
3.标记–整理算法
标记-整理法是标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是将所有存活的对象整理一下,放到另一处空间,然后把剩下的所有对象全部清除。这样就达到了标记-整理的目的。
优点
该算法不会像标记-清除算法那样产生大量的碎片空间。
缺点
如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低。
4.复制算法
算法思想
该算法将内存平均分成两部分,然后每次只使用其中的一部分,当这部分内存满的时候,将内存中所有存活的对象复制到另一个内存中,然后将之前的内存清空,只使用这部分内存,循环下去。这个算法与标记-整理算法的区别在于,该算法不是在同一个区域复制,而是将所有存活的对象复制到另一个区域内。
优点
实现简单、不产生内存碎片
缺点
每次运行,总有一半内存是空的,导致可使用的内存空间只有原来的一半。
#5.3JVM中有哪些垃圾回收器?
1.Serial收集器
Serial收集器是最基本、发展历史最久的收集器,这个收集器是采用复制算法的单线程的收集器。
Serial收集器的两个特点:一个是采用复制算法,另外一个是单线程收集。
单线程的收集器:单线程一方面意味着他只会使用一个 CPU 或者一条线程去完成垃圾收集工作,另一方面也意味着他进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。
实际上到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器,因为它简单而高效。
2.Parnew收集器
基本概念:Parnew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样,但是他却是 Server 模式下的虚拟机首选的新生代收集器。
Parnew收集器的两个特点:一个是采用复制算法,另外一个是多线程收集。
除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS收集器第一次实现了让垃圾收集器与用户线程基本上同时工作;
Parnew收集器默认开启的收集线程数与CPU数量相同,在CPU数量非常多的情况下,可以使用-XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
3.Parallel Scavenge收集器
基本概念:Parallel Scavenge收集器也是一个新生代收集器,也采用了复制算法,也是并行的多线程收集器。Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。Parallel Scavenge收集器是虚拟机运行在Server模式下的默认垃圾收集器。被称为“吞吐量优先收集器”。
Parallel Scavenge收集器的三个个特点:采用复制算法,多线程收集,达到控制吞吐量的目标。
4.Serial Old 收集器
基本概念:Serial Old收集器同样是一个单线程收集器,作用于老年代,使用“标记-整理算法”,这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。
5.Parallel Old收集器
基本概念:Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理算法”进行垃圾回收。
此收集器在JDK 1.6之后的出现,“吞吐量优先收集器”终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合。
6.CMS收集器
基本概念:CMS(Conrrurent Mark Sweep,连续标记扫描)收集器是以获取最短回收停顿时间为目标的收集器。使用标记-清除算法。
收集步骤:
初始标记:标记GCRoots能直接关联到的对象,时间很短;
并发标记:进行GCRoots Tracing(可达性分析)过程,时间很长;
重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长;
并发清除:回收内存空间,时间很长。其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
7. 基本概念:G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。
特点:
并发和并行:使用多个CPU来缩短Stop The World停顿时间,与用户线程并发执行;
分代收集:独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次 GC 的旧对象,以获取更好的收集效果;
空间整合:基于标记-整理算法,无内存碎片产生;
可预测的停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。
在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region的集合。
#5.4服务频繁fullgc,younggc次数较少,可能原因?
fullgc触发条件是老年代空间不足。
频繁fullgc的原因可能有:
大量对象频繁进入老年代并且老年代空间释放不掉。
系统并发高、执行耗时过长,或者数据量过大,导致younggc频繁,且gc后存活对象太多,但是survivor区存放不下(太小或动态年龄判断)导致对象快速进入老年代 年代迅速堆满。
发程序一次性加载过多对象到内存(大对象),导致频繁有大对象进入老年代造成fullgc。
存在内存溢出的情况,老年代驻留了大量释放不掉的对象,只要有一点点对象进入老年代就达到fullgc的临界值了。
元数据区加载了太多类,满了也会发生fullgc。
堆外内存direct buffer memory使用不当导致。
在代码里搞执行了System.gc();

Younggc次数较少的原因:
1.JVM参数设置不当
2.代码不合理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值