1. JVM内存结构
- Java堆:存放对象实例
- 方法区:存储已被虚拟机加载的类信息、常量、静态变量
- 程序计数器:当前线程所执行的字节码的行号指示器
- JVM栈:虚拟机栈描述的是Java方法的执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
- 本地方法栈:调用本地的native方法。
2. Java堆的结构是什么样子的?什么是堆中的永久代
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old)。新生代 ( Young ) 又被划分为三个区域:Eden、S0、S1。 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
-
年轻代 年轻代用于存放新创建的对象,特点是对象更替速度快,即短时间内产生大量的“死亡对象”。 年轻代的特点是产生大量的死亡对象,并且要是产生连续可用的空间,所以使用复制清除算法和并行收集器进行回收,对年轻代的垃圾回收称作“初代回收” -
老年代 老年代存储的是那些不会轻易“死掉”的对象。 老年代得到垃圾回收算法采用的是标记-清除算法收集垃圾的时候会产生许多的内存碎片(不连续的存储空间),因此此后若有较大的对象要进入老年代而无法找到合适的存储空间,就会提前触发一个GC收集,对内存空间进行整理 -
永久代 永久代是JDK7的特性,JDK8后被取消。永久代是JVM方法区的实现方式之一,JDK 8起,被元空间(与堆不相连的本地空间)取而代之。
JVM的内存回收过程如下:
- 对象在Eden Space创建
- 当Eden Space满了的时候,GC就把所有Eden Space中的对象扫描一遍,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象占用的空间释放。
- 当Eden Space再次变满的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中有效对象复制到第二个Survivor Space。
- 如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象被复制到Permanent Generation中。
3. 类加载器
1、启动类加载器:Bootstrap ClassLoader 负责加载存放在JDK/jre/lib下的类库 2、扩含类加载器:Extension ClassLoader 负责加载JDK/jre/lib/ext下的类库 3、应用程序类加载器:Application ClassLoader 负责加载用户自定义的类 4、用户自定义加载器
4. JVM加载class文件的原理机制
原理:虚拟机(jvm)把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。java中的所有类,都需要有由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把Class文件从硬盘读取到内存中。
机制:java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保存程序运行的基础类(像是基类)完全加载到JVM中,至于其他类,则在需要的时候才才加载。这当然就是为了节省内存开销。
5. Java类加载过程(该过程也是类的生命周期)
我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
JVM将类描述数据从.class文件中加载到内存,并对数据进行,解析和初始化,最终形成被JVM直接使用的Java类型。 类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
Java类加载需要 经历7个过程
- 加载:加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。
- 验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
- 对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
- 对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
- 对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
- 对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
- 准备:准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配
public static int value=123;
- 解析:将常量池内的符号引用替换为直接引用的过程。
- 符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
- 直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
- 初始化:对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。 - 使用
- 卸载
6. 双亲委派模型
从Java 2(JDK 1.2)开始,类加载过程采取了父亲委派机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootrap是根加载器,其他的加载器都有且仅有一个父类加载器。 双亲委派模型 如果一个类加载器收到了类加载的请求,他首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父类加载器无法完成加载请求(它的搜素范围中没找到所需的类)时,子加载器才会尝试去加载类。
7.如何判断对象是否可以回收?
1、引用计数法 在对象中添加一个引用技术区,当有地方引用这个对象的时候,引用计数器就+1,当引用失效的时候,计数器的值减1,当计数器为0的时候,表示这个对象没有了引用就可以回收。
问题:A <---->B 如果在A类中调用B类的方法,B类的方法调用A类的方法,这样当其他所有的引用都消失了之后,A和B还有一个相互的引用,也就是说这两个对象的引用计数器各自加1,而实际上这两个对象都没有为额外的引用了,已经是垃圾,但是该算法却并不会计算出这两个对象是垃圾。
2、可达性分析 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链(Reference Chain), 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的。
注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).
可达性算法中,哪些对象可以作为GC Roots对象
- 虚拟机栈中引用的对象
- 方法区静态成员引用的对象
- 方法区常量引用对象
- 本地方法栈JNI引用的对象
8. 如果对象的引用被置为null,垃圾回收器是否会立即释放对象占用的内存?
不会的,因为垃圾回收器回收垃圾是有周期的,在下一个垃圾回收周期中,这个对象方可被回收。
9. Java的四种引用,强软弱虚
从 JDK1.2 版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
1、强引用(Strong Reference) 强引用就是我们经常使用的引用,其写法如下:
Object o = new Object();
特点:
- 如果内存不足,但只要还有强引用指向一个对象,垃圾收集器就不会回收这个对象。
- 显式地设置 o 为 null,或者超出对象的生命周期,此时就可以回收这个对象。具体回收时机还是要看垃圾收集策略。
- 在不用对象的时将引用赋值为 null,能够帮助垃圾回收器回收对象。比如 ArrayList 的 clear() 方法实现:
public void clear() {
modCount++;
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
2、软引用(Soft Reference) 如果一个对象只具有软引用,在内存足够时,垃圾回收器不会回收它;如果内存不足,就会回收这个对象的内存。
使用场景:
- 图片缓存。图片缓存框架中,“内存缓存”中的图片是以这种引用保存,使得 JVM 在发生 OOM 之前,可以回收这部分缓存。
- 网页缓存。
Browser prev = new Browser();
SoftReference sr = new SoftReference(prev);
if(sr.get()!=null) {
rev = (Browser) sr.get();
} else {
prev = new Browser();
sr = new SoftReference(prev);
}
3、弱引用(Weak Reference) 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
使用场景:
- 在下面的代码中,如果类 B 不是虚引用类 A 的话,执行 main 方法会出现内存泄漏的问题, 因为类 B 依然依赖于 A。
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B(a);
a = null;
System.gc();
System.out.println(b.getA());
}
}
class A {}
class B {
WeakReference<A> weakReference;
public B(A a) {
weakReference = new WeakReference<>(a);
}
public A getA() {
return weakReference.get();
}
}
4、虚引用(Phantom Reference) 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
Object obj = new Object();
ReferenceQueue refQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj,refQueue);
使用场景: 可以用来跟踪对象呗垃圾回收的活动。一般可以通过虚引用达到回收一些非java内的一些资源比如堆外内存的行为。例如:在 DirectByteBuffer 中,会创建一个 PhantomReference 的子类Cleaner的虚引用实例用来引用该 DirectByteBuffer 实例,Cleaner 创建时会添加一个 Runnable 实例,当被引用的 DirectByteBuffer 对象不可达被垃圾回收时,将会执行 Cleaner 实例内部的 Runnable 实例的 run 方法,用来回收堆外资源。
新生代、老年代、永久代都存储哪些东西
- 新生代:new出来的对象先进入新生代
- 老年代
1)新生代经历N次仍存活将放到老年代 2)大对象一般直接放入老年代 3)当Survivor空间不足。需要老年代担保一些空间,也会将对象放入到老年代中。 - 永久代:值的就是方法区
10. 简述Java垃圾回收机制
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
11.JVM的永久代中会发生垃圾回收么?
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的native 内存区。
元数据区的意义 元数据区用于存储类的元数据,在调优或者调查JVM问题的时候就不用和PermGen区域打交道了,也不会有java.lang.OutOfMemoryError: PermGen 这种内存不足的问题来骚扰你。
12. JVM垃圾处理方法
清除阶段(Sweep) 一个对象出现第一次没有被引用的情况,就会被加入到F-Queue队列等待执行finalize()方法判断是否有机会复活或者直接被当作垃圾回收。以便有足够的可用内存空间为新对象分配空间。
1、标记-清除算法(老年代) 该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象. 该算法会有两个问题
- 效率问题,标记和清除效率不高。
- 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
所以它一般用于"垃圾不太多的区域,比如老年代"
2、复制算法(新生代) 该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象(非垃圾)复制到另外一块上面, 然后把已使用过的内存空间一次清理掉。交换两个内存的角色,最后完成垃圾回收。
该算法的优点
- 不用考虑碎片问题,方法简单高效
- 没有标记和清除过程,实现简单,运行高效
该算法的缺点 3. 此算法的缺点也是很明显的,就是需要两倍的内存空间。 4. 时间开销大:毕竟是要把全部存活的对象都复制一遍,对象的地址也要跟着变化,引用它的对象的引用地址也要做相应的地址调整。
划分Eden Space与Survivor的来源 现代商用VM的新生代均采用复制算法,但由于新生代中的98%的对象都是生存周期极短的,因此并不需完全按照1∶1的比例划分新生代空间,而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor。
发生MinorGC时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间。当Survivor空间不够用(不足以保存尚存活的对象)时,需要依赖老年代进行空间分配担保机制,这部分内存直接进入老年代。
复制算法的空间分配担保: 在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空 间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间). 然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平 均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只 好在失败后重新发起一次Full GC(让老年代腾出更多空间).
Minor GC ,Full GC 触发条件 Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行 (2)老年代空间不足 (3)方法区空间不足 (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存 (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
3、标记-压缩算法(老年代) 标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记压缩算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理,而是**让所有存活的对象都向一端移动,**然后清理掉端边界以外的内存。 4、分代收集算法 “分代收集”算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法。我们常用的垃圾回收器一般采用的是分代回收算法。
13. 简述一个分代垃圾回收过程
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的1/3,老年代的默认占比是2/3。
新生代使用的是复制算法,新生代里有3个分区:Eden、To Survivor,、From Survivor,它们的默认占比是8:1:1,,它的执行流程如下: 当年轻代中的Eden区分配满的时候,就会触发年轻代的GC。具体过程如下:
- 在Eden去执行了第一次的GC后,存活的对象会被移动到其中一个Survivor分区
- Eden区再次GC,这时会采用复制算法,将Eden和From Survivor区一起清理。存活的对象会被复制到To Survivor区。接下来,只需要清空From Survivor区就可以了。
14. 你能说出几个垃圾收集器
1、Serial Serial收集器是Hotspot运行在Client模式下的默认新生代收集器,它在进行垃圾回收时,会暂停所有的进程,用一个线程去完成GC工作 2、Parnew ParNew收集器其实是Serial的多线程版本,回收策略完全一样。 3、Cms Cms(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器,一款真正意义上的并发收集器,虽然现在已经有了理论意义上表现很好的G1收集器,但现在主流互联网企业线上选用的仍是Cms,又称多并发暂停的收集器。 从它的英文名字可以看出,它是基于标记-清除算法实现的。整个过程分为4个步骤。
- 初始标记:仅标记一个GC Roots能直接关联到的对象,速度很快
- 并发标记:多线程进行标记
- 重新标记:修改并发标记期间因用户程序运行而导致标记产生变动的那一部分对象的标记记录。
- 并发清除:将已死对象释放
4、G1 同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化分代的概念。合理利用垃圾收集各个周期的资源,解决了其他收集器甚至是CMS的众多缺陷。
15. GC日志的real、user、sys是什么意思
1、real时间花费的时间,指的是从开始到结束所花费的时间 2、user指的是进程在用户态花费的CPU时间,只统计本进程所使用的时间,是指多核。 3、sys指的是进程在核心态花费的CPU时间,值得是内核中的系统调用所花费的时间,只统计本进程所使用的时间。
16. MinorGC、MajorGC、FullGC都什么时候发生?
MinorGC在年轻代空间不足的时候触发的 MajorGC指的是老年代的GC,清理老年代对象,出现MajorGC一般进程伴有MinorGC。
FullGC有三种情况
- 老年代无法再分配内存的时候
- 元空间不足的时候
- 显示调用System.gc的时候。
17. Java会出现内存泄露吗?请简单描述
内存泄露指的是不在被程序使用的对象一直占用空间
Java出现内存泄露的情况: 长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景
例如,缓存系统,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。
18. 垃圾回收器的基本原理是什么?有什么方法主动通知虚拟机进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
程序员可以手动执行System.gc()或者Runtime.gc() ,通知GC运行,但是Java语言规范并不保证GC一定会执行。
19. 调优命令有哪些?
Sun JDK监控和故障处理命令有如下几个
- jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
- jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
- jmap,JVM Memory Map命令用于生成heap dump文件
- jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
- jstack,用于生成java虚拟机当前时刻的线程快照
- jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数
20. 你知道哪些JVM性能调优
- 设定堆内存大小
-Xmx:堆内存最大限制。 - 设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代
-XX:NewSize:新生代大小 -XX:NewRatio 新生代和老生代占比 XX:SurvivorRatio:伊甸园空间和幸存者空间的占比 - 设定垃圾回收器
年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
21. 常见的调优工具
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory AnalyzerTool)、GChisto。
1、jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控 2、jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。 3、MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Javaheap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗 4、GChisto,一款专业分析gc日志的工具
|