Java 面试题(3)—— JVM

JVM的内存结构。


JVM主要结构:堆内存、栈、方法区,程序计数器,永久代)(jdk 8采用元空间替代)。
堆内存又分成年轻代和年老代。
年轻代由三部分组成,Eden、From Survivor 和 To Survivor,这三者默认分配的比例是8:1:1。
方法区主要存储类信息、常量、静态变量等数据。
栈又分为java虚拟机栈和本地方法栈,每创建一个线程对应一个java栈,
每调用一个方法就会向栈中创建并压人一个栈帧,栈帧是用来存储方法数据和部分过程结果的数据结构,
每一个方法从调用到最终返回结果的过程,就对应一个栈帧从入栈到出栈的过程。
本地方法栈主要用于native方法的调用。
程序计数器(Program Counter Register)是JVM中一块较小的内存区域,保存着当前线程执行的虚拟机字节码指令的内存地址。
所有线程共享的内存数据区:方法区,堆。而虚拟机栈,本地方法栈和程序计数器都是线程私有的。


JVM方法栈的工作过程,方法栈和本地方法栈有什么区别。


每个线程拥有自己的栈,栈包含每个方法执行的栈帧。
栈是一个后进先出(LIFO)的数据结构,因此当前执行的方法在栈的顶部。
每次方法调用时,一个新的栈帧创建并压栈到栈顶。
当方法正常返回或抛出未捕获的异常时,栈帧就会出栈。
除了栈帧的压栈和出栈,栈不能被直接操作。
所以可以在堆上分配栈帧,并且不需要连续内存。


JVM的栈中引用如何和堆中的对象产生关联。


在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,
让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。
Java的堆是一个运行时数据区,类的(对象从中分配空间。
这些对象通过new、newarray、anewarray和multianewarray等指令建立,它们不需要程序代码来显式的释放。
堆是由垃圾回收来负责的,堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,
因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。
但缺点是,由于要在运行时动态分配内存,存取速度较慢。 

栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。
但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
栈中主要存放一些基本类型的变量(, int, short, long, byte, float, double, boolean, char)和对象句柄。 

https://www.cnblogs.com/langren1992/p/4738391.html
https://www.jianshu.com/p/ac162726d7de

可以了解一下逃逸分析技术。


方法逃逸:当一个对象在方法中被定义后,因为可能被外部方法引用,比如作为调用参数被传递到其他的方法里。
线程逃逸:当一个对象在方法中被定义后,可能被外部线程访问到,比如给类变量或者在其他线程中访问的实例变量。
栈上分配:就是把没发生逃逸的对象,在栈分配空间。(一般对象分配空间是在堆)
jvm根据对象是否发生逃逸,会分配到不同(堆或栈)的存储空间。如果对象发生逃逸,那会分配到堆中。

https://blog.csdn.net/qq_32575047/article/details/81214178
https://blog.csdn.net/somnusrush/article/details/76027122
https://www.jianshu.com/p/3ecc626ce304


GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。
几种算法的区别体现在对年老代的回收。

PS
mark-copy(stp)
年轻代的垃圾算法
标记-复制,将Eden区的和Survior区复制到另一个S区,并且标记存活次数。

CMS(current mark sweep)
年老代的垃圾算法
initial mark(stp): 第一次标记,从root节点出发,寻找第一个节点停止
concurrent mark(不stp): 并发标记,从initial mark的节点开始标记非空节点
concurrent-preclean(不stp):预清理过程。
remark(stp):由于concurrent mark 不stp,没有完全标记所有的可达对象,需要重新标记一次
concurrent sweep(不stp):将所有未标记的对象清理
concurrent-reset(不stp): 重置内部数据结构
cms是会产生内存碎片的。
cms是基于“标记-清除”算法的收集器,这意味着垃圾回收完成后会产生大量的内存碎片,
当大对象没有足够的连续空间来分配时,不得不提前触发一次Full GC,增加stp的时间。

G1
garbage first的算法:
initial mark(stp): 和cms是一样的操作,同时发生是minor gc。
root region scanning(stp):在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。(共用Minor gc的操作)
concurrent mark(不stp): 并发标记,从initial mark的节点开始标记非空节点,
计算每个 region的对象存活率,方便后面的clean up阶段使用
remark(stp):由于concurrent mark 不stp,没有完全标记所有的可达对象,需要重新标记一次
cleanup(不stp): 清除空Region(没有存活对象的),加入到free list。只是回收没有对象的region,不需要stp。
g1和cms的区别:
g1的内存组织方式变了,不是连续的内存空间,
而是一个个region(分为E(Eden)、S(Survivor)、O(Old)、H(Humongous))。
remark阶段再标记的算法变了,g1的算法是SATB(snapshot-at-the-begining)。
jdk 11 g1作为默认的垃圾回收器。
优势:
G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

https://tech.meituan.com/g1.html


标记清除和标记整理算法的理解以及优缺点。

标记 -清除算法(Mark-Sweep)
  “标记-清除”算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:
首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。
它的主要缺点有两个:
(1)效率问题:标记和清除过程的效率都不高;
(2)空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,
碎片过多会导致大对象无法分配到足够的连续内存,从而不得不提前触发GC,甚至Stop The World。
标记-整理(Mark-Compact)
   复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。
更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,
以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。    
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,
而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

https://blog.csdn.net/wuzhiwei549/article/details/80563134

eden survivor区的比例,为什么是这个比例,eden survivor的工作过程。

默认比例是8:1:1。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法。
年轻代的出生在eden区,随着一次gc,由Eden区copy到From Survivor区,年龄+1。
再次回收,由From Survivor和Eden区copy到To Survivor区。年龄超过8岁,进入年老代。

https://www.jianshu.com/p/534ab3c8335f
 

JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型。

GC roots。如果从一个对象没有到达根对象的路径,或者说从根对象开始无法引用到该对象,该对象就是不可达的。
gc roots:
    1. 虚拟机(JVM)栈中引用对象 
        每个方法执行的时候,jvm都会创建一个相应的栈帧(栈帧中包括操作数栈、局部变量表、运行时常量池的引用),
        栈帧中包含这在方法内部使用的所有对象的引用(当然还有其他的基本类型数据),当方法执行完后,该栈帧会从虚拟机栈中弹出
    2.方法区中的类静态属性引用对象
    3.方法区中常量引用的对象(final 的常量值)
    4.本地方法栈JNI的引用对象

https://blog.csdn.net/u012941811/article/details/52427372

强软弱虚引用的区别以及GC对他们执行怎样的操作。

强引用:
垃圾回收器绝不会回收它
当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用:
如果内存空间不足了,就会回收这些对象的内存
可用来实现内存敏感的高速缓存。如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用:
更短暂的生命周期,不管当前内存空间足够与否,都会回收它的内存。
如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
虚引用:
虚引用不会决定对象的生命周期,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列(ReferenceQueue)联合使用。

https://blog.csdn.net/panyongcsd/article/details/46605613

Java是否可以GC直接内存。

NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理内存的类——DirectBuffer。
 DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer仍在JVM堆上分配内存,
 其最大内存受到最大堆内存的限制;而DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
(Note:DirectBuffer并没有真正向OS申请分配内存,其最终还是通过调用Unsafe的allocateMemory()来进行内存分配。不过JVM对Direct Memory可申请的大小也有限制,可用-XX:MaxDirectMemorySize=1M设置,这部分内存不受JVM垃圾回收管理。)
在JVM堆分配内存(allocate)相比,直接内存分配(allocateDirect)的访问性能更好,但分配较慢。

https://www.cnblogs.com/z-sm/p/6235157.html?utm_source=itdadao&utm_medium=referral
http://www.importnew.com/21998.html

Java类加载的过程。

加载、链接(验证、准备、解析)、初始化。
加载:把class字节码文件从各个来源通过类加载器装载入内存中。
    字节码来源。一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络,以及动态代理实时编译
    类加载器。一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。    
准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。是Java虚拟机根据不同变量类型的默认初始值。
解析:将常量池内的符号引用替换为直接引用的过程。
    符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
    直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;
    而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
初始化:主要是对类变量初始化,是执行类构造器的过程。对static修饰的变量或语句进行初始化。

https://blog.csdn.net/ln152315/article/details/79223441
https://www.cnblogs.com/xiaoxian1369/p/5498817.html

双亲委派模型的过程以及优势。

实现双亲委派模型的代码都集中在java.lang.ClassLoader的loadClass()方法中:  
首先会检查请求加载的类是否已经被加载过;  
若没有被加载过:  
递归调用父类加载器的loadClass();  
父类加载器为空后就使用启动类加载器加载;  
如果父类加载器和启动类加载器均无法加载请求,则调用自身的加载功能。
优点;
Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。  
保证了运行的安全性,防止不可信类扮演可信任的类。

https://blog.csdn.net/inspiredbh/article/details/74889654
https://blog.csdn.net/qq_38182963/article/details/78660779

常用的JVM调优参数。


trace 跟踪:
1. 打印GC的简要信息:
-verbose:gc
-XX:+PrintGC
2. 打印GC详细信息:(生产不要用)
-XX:+PrintGCDetails
3. 指定GC log的位置:
-Xloggc:log/gc.log
4. 类加载信息:
-XX:+TraceClassLoading
5. 堆内存设置:
-Xms1g   -Xmx1g
6. 导出OOM的路径:
-XX:HeapDumpPath=d:/a.dump
7. 初始堆大小
-Xms
8. 最大堆大小
-Xmx


dump文件的分析。


jmap -dump:file=文件名.dump [pid]
jdk自带的visualvm


Java有没有主动触发GC的方式(没有)。


System.gc();
// 或者下面,两者等价
Runtime.getRuntime().gc();
只是通知gc,并没有执行。


内存泄漏和内存溢出的区别和联系

常发性内存泄漏:
发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏:
发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。
对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。
比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。
严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。
不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
内存溢出的原因及解决方法:

内存溢出原因: 
内存中加载的数据量过于庞大,如一次从数据库取出过多数据; 
集合类中有对对象的引用,使用完后未清空,使得JVM不能回收; 
代码中存在死循环或循环产生过多重复的对象实体; 
使用的第三方软件中的BUG; 
启动参数内存值设定的过小
内存溢出的解决方案: 
修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
对代码进行走查和分析,找出可能发生内存溢出的位置。


https://blog.csdn.net/ruiruihahaha/article/details/70270574

发布了115 篇原创文章 · 获赞 58 · 访问量 23万+

猜你喜欢

转载自blog.csdn.net/Angry_Mills/article/details/82107305