文章目录
1.说一下对象创建过程中的内存分配
一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。
通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。
对于对象在 JVM 中的创建过程具体如下:
- JVM 会先去方法区找有没有所创建对象的类存在,有就可以创建对象了,没有则把该类加载到方法区
- 在创建类的对象时,首先会先去堆内存中分配空间
- 当空间分配完后,加载对象中所有的非静态成员变量到该空间下
- 所有的非静态成员变量加载完成之后,对所有的非静态成员进行默认初始化
- 所有的非静态成员默认初始化完成之后,调用相应的构造方法到栈中
- 在栈中执行构造函数时,先执行隐式,再执行构造方法中书写的代码
- 执行顺序:静态代码库,构造代码块,构造方法
- 当整个构造方法全部执行完,此对象创建完成,并把堆内存中分配的空间地址赋给对象名
对象的内存分配有两种方式,即指针碰撞和空闲列表方式。
指针碰撞方式:
假设Java堆中的内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,那么所有的内存分配就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
空闲列表方式:
如果**Java堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,**在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。
需要注意的是,Java 堆内存是否规整是由所使用的垃圾收集器是否拥有压缩整理功能来决定的。
看到这里,聪明的你肯定想到了内存分配是否也应该考虑线程安全的问题呢?
那么内存的分配如何保证线程安全呢?
- 对分配内存空间的动作进行同步处理,通过“CAS + 失败重试”的方式保证更新指针操作的原子性
- 把分配内存的动作按照线程划分在不同的空间之中,即给每一个线程都预先分配一小段的内存,称为本地线程分配缓存(TLAB),只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。 虚拟机是否使用TLAB,可以通过**-XX: +/-UserTLAB**参数来设定
2.(重点)Java中的类加载机制有了解吗?
Java中的类加载机制指虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。类加载机制的保持则包括前面五个阶段。
-
加载:
加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。 -
验证:
验证的作用是确保被加载的类的正确性,包括文件格式验证,元数据验证,字节码验证以及符号引用验证。 -
准备:
准备阶段为类的静态变量分配内存,并将其初始化为默认值。假设一个类变量的定义为public static int val = 3;那么变量val在准备阶段过后的初始值不是3而是0。 -
解析:
解析阶段将类中符号引用转换为直接引用。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。 -
初始化:
初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
类的加载全过程:
类的加载动作在类装载器中完成,类加载的完整概念是:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
解析:
关于类加载机制的考察也是JVM知识点的重中之重,上边我们介绍了类加载机制的过程以及其基本作用。接下来,我们看下类加载器有哪几种呢?类加载器的职责又是什么呢?
类加载器的分类:
-
启动类加载器(Bootstrap ClassLoader):
启动类加载器负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的类。 -
扩展类加载器(ExtClassLoader):
扩展类加载器负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。 -
应用类加载器(AppClassLoader):
应用类加载器负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。
类加载器的职责:
-
全盘负责:
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。 -
父类委托:
类加载机制会先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
父类委托机制是为了防止内存中出现多份同样的字节码,保证java程序安全稳定运行。
- 缓存机制:
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
最后一个问题:到底什么时候才启用类加载器呢?
其实,类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
更多细节可以看:Java类加载机制,你理解了吗?
3.(重点)JVM如何判定一个对象是否应该被回收?
判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法。
引用计数法:
是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题。
循环引用问题可以看:java对象之间相互循环引用实例
root根搜索方法:
Java和C#都是使用根搜索算法来判断对象是否存活。通过一系列的名为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时(用图论来说就是GC Root到这个对象不可达时),证明该对象是可以被回收的。
-
在Java中哪些对象可以成为GC Root?
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用对象
- 本地方法栈中JNI(即Native方法)的引用对象
(root根搜索算法更加详细一点的描述可以看:Java垃圾收集算法介绍)
解析:
关于对象是否可以被回收的问题也是JVM考察中常见的题目。主要掌握引用计数法的基本原理与优缺点。然后对于root根搜索方法也应该掌握理解。说了这么多,那么我们来看看什么是对象的引用吧。
如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表一个引用。JDK1.2以后将引用分为强引用,软引用,弱引用和虚引用四种。
- 强引用:普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用:通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。
- 弱引用:通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
- 虚引用:也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。
4.(重点)JVM垃圾回收算法有哪些?(注意需要与对象是否存在引用关系的算法区分)
HotSpot 虚拟机采用了root根搜索方法来进行内存回收,常见的回收算法有标记-清除算法,复制算法和标记整理算法。
标记-清除算法(Mark-Sweep):
标记-清除算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,并且会产生内存碎片。
图片说明
复制算法:
复制算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。复制算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
图片说明
标记-整理算法:
标记-整理算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
图片说明
解析:
垃圾回收算法是垃圾收集器的算法实现基础,年轻代垃圾回收一般采用复制算法,老年代垃圾回收一般采用标记-清除和标记-整理算法。希望大家对照着算法示意图,对算法的原理与过程加以理解与掌握。接下来,我们一起来看垃圾回收算法的具体实现,那就是垃圾收集器吧。
5.JVM中的垃圾收集器有了解吗?(重点掌握CMS收集器)
JVM中的垃圾收集器主要包括7种,即Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old以及CMS,G1收集器。如下图所示:
Serial收集器:
Serial收集器是一个单线程的垃圾收集器,并且在执行垃圾回收的时候需要 Stop The World。虚拟机运行在Client模式下的默认新生代收集器。Serial收集器的优点是简单高效,对于限定在单个CPU环境来说,Serial收集器没有多线程交互的开销。
Serial Old收集器:
Serial Old是Serial收集器的老年代版本,也是一个单线程收集器。主要也是给在Client模式下的虚拟机使用。在Server模式下存在主要是做为CMS垃圾收集器的后备预案,当CMS并发收集发生Concurrent Mode Failure时使用。
ParNew收集器:
ParNew是Serial收集器的多线程版本,新生代是并行的(多线程的),老年代是串行的(单线程的),新生代采用复制算法,老年代采用标记整理算法。可以使用参数: -XX:UseParNewGC使用该收集器,使用-XX:ParallelGCThreads可以限制线程数量。
Parallel Scavenge垃圾收集器:
Parallel Scavenge是一种新生代收集器,使用复制算法的收集器,而且是并行的多线程收集器。Paralle收集器特点是更加关注吞吐量(吞吐量就是cpu用于运行用户代码的时间与cpu总消耗时间的比值)。可以通过**-XX:MaxGCPauseMillis参数控制最大垃圾收集停顿时间;通过-XX:GCTimeRatio参数直接设置吞吐量大小;通过-XX:+UseAdaptiveSizePolicy**参数可以打开GC自适应调节策略,该参数打开之后虚拟机会根据系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或者最大的吞吐量。自适应调节策略是Parallel Scavenge收集器和ParNew的主要区别之一。
Parallel Old收集器:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。
CMS(Concurrent Mark Sweep)收集器:
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于标记-清除算法实现的,是一种老年代收集器,通常与ParNew一起使用。
CMS的垃圾收集过程分为4步:
- 初始标记:需要“Stop the World”,初始标记仅仅只是标记一下GC Root能直接关联到的对象,速度很快。
- 并发标记:是主要标记过程,这个标记过程是和用户线程并发执行的。
- 重新标记:需要“Stop the World”,为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(停顿时间比初始标记长,但比并发标记短得多)。
- 并发清除:和用户线程并发执行的,基于标记结果来清理对象。
那么问题来了,如果在重新标记之前刚好发生了一次MinorGC,会不会导致重新标记阶段Stop the World时间太长?
答:不会的,在并发标记阶段其实还包括了一次并发的预清理阶段,虚拟机会主动等待年轻代发生垃圾回收,这样可以将重新标记对象引用关系的步骤放在并发标记阶段,有效降低重新标记阶段Stop The World的时间。
CMS垃圾回收器的优缺点分析:
CMS以降低垃圾回收的停顿时间为目的,很显然其具有并发收集,停顿时间低的优点。
缺点主要包括如下:
- 对CPU资源非常敏感,因为并发标记和并发清理阶段和用户线程一起运行,当CPU数变小时,性能容易出现问题。
- 收集过程中会产生浮动垃圾,所以不可以在老年代内存不够用了才进行垃圾回收,必须提前进行垃圾收集。通过参数**-XX:CMSInitiatingOccupancyFraction的值来控制内存使用百分比。如果该值设置的太高,那么在CMS运行期间预留的内存可能无法满足程序所需,会出现Concurrent Mode Failure失败,之后会临时使用Serial Old收集器做为老年代收集器**,会产生更长时间的停顿。
- 标记-清除方式会产生内存碎片,可以使用参数**-XX:UseCMSCompactAtFullCollection来控制是否开启内存整理(无法并发,默认是开启的)。参数-XX:CMSFullGCsBeforeCompaction**用于设置执行多少次不压缩的Full GC后进行一次带压缩的内存碎片整理(默认值是0)。
接下来,我们先看下上边介绍的浮动垃圾是怎么产生的吧。
浮动垃圾:
由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
G1(Garbage-First)收集器:
G1收集器将新生代和老年代取消了,取而代之的是将堆划分为若干的区域,仍然属于分代收集器,区域的一部分包含新生代,新生代采用复制算法,老年代采用标记-整理算法。
通过将JVM堆分为一个个的区域(region),G1收集器可以避免在Java堆中进行全区域的垃圾收集。G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据回收时间来优先回收价值最大的region。
G1收集器的特点:
- 并行与并发:G1能充分利用多CPU,多核环境下的硬件优势,来缩短Stop the World,是并发的收集器。
- 分代收集:G1不需要其他收集器就能独立管理整个GC堆,能够采用不同的方式去处理新建对象、存活一段时间的对象和熬过多次GC的对象。
- 空间整合:G1从整体来看是基于标记-整理算法,从局部(两个Region)上看基于复制算法实现,G1运作期间不会产生内存空间碎片。
- 可预测的停顿:能够建立可以预测的停顿时间模型,预测停顿时间。
和CMS收集器类似,G1收集器的垃圾回收工作也分为了四个阶段:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
其中,筛选回收阶段首先对各个Region的回收价值和成本进行计算,根据用户期望的GC停顿时间来制定回收计划。
6.JVM常用内存调优命令:(重点掌握)
JVM在内存调优方面,提供了几个常用的命令,分别为jps,jinfo,jstack,jmap以及jstat命令。分别介绍如下:
jps:主要用来输出JVM中运行的进程状态信息,一般使用jps命令来查看进程的状态信息,包括JVM启动参数等。
jinfo:主要用来观察进程运行环境参数等信息。
jstack:主要用来查看某个Java进程内的线程堆栈信息。jstack pid 可以看到当前进程中各个线程的状态信息,包括其持有的锁和等待的锁。
jmap:用来查看堆内存使用状况。jmap -heap pid可以看到当前进程的堆信息和使用的GC收集器,包括年轻代和老年代的大小分配等
jstat:进行实时命令行的监控,包括堆信息以及实时GC信息等。可以使用jstat -gcutil pid1000来每隔一秒来查看当前的GC信息。
如何排查一个线上的服务异常?
-
[1 ] 首先查看当前进程的JVM启动参数,查看内存设置是否存在明显问题。
-
[2 ] 查看GC日志,看GC频率和时间是否明显异常。
-
[3 ] 查看当前进程的状态信息top -Hp pid,包括线程个数等信息。
-
[4 ] jstack pid查看当前的线程状态,是否存在死锁等关键信息。
-
[5 ] jstat -gcutil pid查看当前进程的GC情况。
-
[6 ] jmap -heap pid查看当前进程的堆信息,包括使用的垃圾收集器等信息。
-
[7 ] 用jvisiual工具打开dump二进制文件,分析是什么对象导致了内存泄漏,定位到代码处,进行code review。
一般情况下,我们在测试环境上线新服务的时候,应该重点关注并且查看当前新服务的内存使用以及回收情况,避免新服务种出现内存异常导致服务崩溃的现象发生。
7.JDK8中在内存管理上的变化:
JDK8中出现了元空间代替了永久代。元空间和永久代类似,都是对JVM规范中方法区的实现。区别在于元空间并不在虚拟机中,而是使用本地内存,默认情况下元空间的大小仅受本地内存限制,也可以通过**-XX:MetaspaceSize指定元空间大小。**
为什么要使用元空间代替永久代?
字符串在永久代中,容易出现性能问题和内存溢出的问题。类和方法的信息等比较难确定大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。使用元空间则使用了本地内存。
8.什么是OOM,如何解决OOM问题(注意与内存泄漏问题的区别)
1)什么是OOM
OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。看下关于的官方说明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
2)出现OOM的原因
为什么会没有内存了呢?原因不外乎有两点:
1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
最常见的OOM情况有以下三种:
- java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
- java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
- java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。