JVM面试之多的是我不知道的事
作者:互联网
1:JVM内存模型
Arthas:Alibaba开源的Java诊断工具,采用命令行交互模式,提供了丰富的功能,是排查jvm相关问题的利器。
jvm中每调用一个方法就会生成一个栈帧,放在栈空间
栈区分为虚拟机栈和本地栈
虚拟机栈存储java方法的栈帧,本地栈存储native方法的栈帧
程序计数器:存放每一个线程执行到了方法的哪一步,每个线程都有程序计数器
程序计数器在jvm相当于记录指令的内存地址(指令执行到哪一步了)
本地栈是没有程序计数器的,因为本地方法是C/C++的,java无法计数,程序计数器一直是0
方法区:class类信息,静态变量,常量
在jdk1.8之前,方法区是堆内的一块连续的区域
在jdk1.8之后,方法区从jvm内存移出来了,变成元数据区(MetaSpace)
,移到了操作系统之中
堆:存放对象,数组
堆分为新生代,老年代。默认新生代占堆内存的1/3,老年代占堆内存的2/3。
新生代分为Eden区和两个Surrivor区(2/10),Eden区占新生代的(8/10)
2:java类加载过程,什么是双亲委派机制,有什么作用
public class classLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
//父子关系 AppClassLoader<-ExtClassLoader<-BootStrap ClassLoader
ClassLoader cl1=classLoaderTest.class.getClassLoader();
System.out.println("cl1->"+cl1);
System.out.println("parent of cl1->"+cl1.getParent());
//打印出null,因为BootStrap ClassLoader由C++开发,是JVM虚拟机的一部分,本身不是JAVA类
System.out.println("grandparent of cl1->"+cl1.getParent().getParent());
ClassLoader cl2=String.class.getClassLoader();
//String 和 java.util.List 这两个类是JVM启动的时候就会默认加载到JVM进程中的,它们的ClassLoader打印出来都是null
//这就说明这个null是jvm内存还空无一物的时候,由底层C++初始化的ClassLoader
System.out.println("cl2->"+cl2);
System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader());
}
}
JAVA的类加载器:AppClassLoader<-ExtClassLoader<-BootStrap ClassLoader
每种类加载器都有自己的加载目录
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
根据上面代码解读类加载机制:
每次加载过的类都是有缓存的(通过底层C+代码做的),如果缓存中有,直接跳过if (c == null) ,如果缓存中没有,就去找parent if (parent != null)
,就让父类代替加载这个类,父类也会去找是否自己被加载过
parent为空,就进入下面的BootstrapClassLoader
c = findBootstrapClassOrNull(name);
如果还是没有找到,就自己去找
双亲委派机制的核心如下图
要加载一个类(AppClassLoader),如果已经加载过,就直接返回,否则去找他的父类(ExtClassLoader),看父类有没有加载过,有直接返回,没有,就继续找ExtClassLoader的父类BootStrap ClassLoader,看有没有加载过,有就直接返回,没有的话,就如下图的右边部分,去BootStrap ClassLoader规定的路径去找BootStrap ClassLoader,找不到的话,就向下,去ExtClassLoader的规定加载路径找,再找不到,就交由应用程序自己加载,就去Class.path属性下去找
总结:向上委托查找,向下委托加载
双亲委派的作用:对java底层起保护作用,保护java底层的类不会被应用程序覆盖
比如,你自己再定义一个String类,可以写自己的main方法,如果可以这样,java就乱掉了,因为String还要进行其他的操作,就是对基础的类进行保护
如果我们自己写了一个String类,那么加载的时候,类加载器不会直接找自定义的String,而是向上找,一直找到BootStrap ClassLoader,因为BootStrap ClassLoader里面有一个String类,而不是去class,path下面找String
类加载过程:加载 ->连接 ->初始化
加载:通过双亲委派机制把class文件(字节码)加载到jvm内存之中,并映射成jvm认可的数据结构
连接 :resolveClass方法,是一个native方法
连接分为3个阶段:
- 1:验证:检查加载的字节信息是否符合JAVA虚拟机的规范,否则就会报错(黑客就无法通过自己写class的方式网JVM放入恶意的程序),保证class文件是通过java编译出来的
- 2:准备:创建类或者接口的静态变量并赋予初始值(半初始化状态)
比如定义了静态变量 private final static String str=“123”; 会先在堆区初始化一个字符串占着空间,先给一个默认值(先给个空字符串,占着堆里面的内存),还不知道具体的值 - 3:解析:把常量池中的符号引用替换为直接引用,栈里面的str指针指向堆里面的内存
然后初始化
类加载完执行静态代码块和构造方法
3:一个对象从加载到JVM,再到被GC清除,都经历过什么过程
类的初始化:静态属性,静态代码块是在类的加载过程中加载的
对象的初始化:普通属性,构造方法是在对象创建的时候初始化的
对象的创建:
-
1 用户创建一个对象,JVM首先需要去方法区找对象的类型信息,然后再创建对象
-
2 JVM要实例化一个对象,首先要在堆中先创建一个对象,会涉及半初始化状态,
- 半初始化状态:比如volatile里面的指令重排,就是对象创建的过程分为几个步骤,首先创建一个对象,然后给所有的成员变量分配一个默认值,这就是半初始化状态,然后在栈中创建一个指针,建立好对应关系,最后再执行初始化,把成员变量赋值成初始值,3个步骤就会有指令重排的风险
-
3:对象首先会分配在堆内存的新生代的Eden区,经过一次MinorGC(发生在新生代的GC),对象如果存活,就会从
Eden区
进入Surrivor区
,再经过一次MinorGC,如果是垃圾,就被清理掉,如果不是,就移到另一个Surrivor区
,并且给它的年龄加1,就这样,在两个sURRIVOR区之间来回拷贝,每移动一次,年龄加1。经过多次MinorGC发现这个对象依然存活,超过一定年龄之后,对象就会转入老年代
,进入老年代之后,如果多次GC还是一直存活,直到方法运行结束,栈中的引用被移出, 没有引用指向这个对象,那么就会被老年代的GC进程回收- 年龄是什么?:对象在Surrivor区中的移动次数(4个bit,最大能记录15(1111))
- 多大的年龄会转到老年代?:最大15次,超过15次转到老年代
-
4 当方法执行结束后,栈中的指针会移除掉
-
5堆中的对象,经过FullGC(发生在老年代),就会被标记为垃圾,被GC线程清理掉
GC是推动对象在JVM内存中移动的关键
对象一定会创建在堆区吗?
不一定,在某些情况下,为了优化,会创建在栈区,当对象只在一个地方用的时候,会优先在栈中分配,在栈里分配,生命周期就变得比较简单,方法一结束,栈帧就没了,对象就移除了,就不用GC介入了
正常情况下,对象是要在堆上进行内存分配的,但是随着编译器优化技术的成熟,虽然虚拟机规范是这样要求的,但是具体实现上还是有些差别的。
如HotSpot虚拟机引入了JIT优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。
————————————————
版权声明:本文为CSDN博主「吃饭的时候记得叫我啊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wtmdcnm/article/details/104921377
JVM系列 - Java对象都是创建在堆内存中的吗?
4:怎么确定一个对象到底是不是垃圾,什么是GC Root?
比如拆房子,需要先把要拆的地方标记一个拆字,然后拆迁队就相当于垃圾回收器去拆
有两种定位垃圾的方式
-
1:引用计数:给堆内存中的每个对象记录一个应用个数,有指针指向它就证明它有用,当有另一个对象来引用它,引用计数加1,引用它新的对象被清除后,它的引用计数减1,引用计数大于0就有用。引用个数为0就认为是垃圾(这是早期JDK中使用的方式)
- 引用计数缺点:没办法解决循环引用的情况(造成对象永远不会被清除,内存泄漏,内存泄漏到一定程度可能造成内存溢出(OOM),但是OOM不一定存在内存泄漏)
-
2:根可达算法:在内存中,从引用根对象(
GC Root
)向下一直找引用,能找到的就是存活对象,找不到的,即便有互相的引用,即便引用个数不为0也可以认为是垃圾- 到底哪些才是GC Root?
在Java语言中,可作为GC Roots的对象包括下面几种:
虚拟机栈(栈帧中的本地变量表)中引用的对象。方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。栈区是运行到某个用到的对象的时候必须从栈开始引用,
- 到底哪些才是GC Root?
根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
5:JVM有哪些垃圾回收算法
垃圾回收算法是组成垃圾回收器的工具
- 1:MarkSweep标记清除算法
分为两个阶段,这种算法简单,但是会产生大量的内存碎片,比如下图无法找到连续12个内存,但是可以使用的内存(绿色格子)是超过12个的,申请不到内存就会提前触发垃圾回收,寄希望于在下一次垃圾回收中清理出这样一块连续的内存,就能分配空间了,造成空间利用率低- 标记:把垃圾内存标记出来
- 清除:直接将垃圾内存回收
-
2:Copying 拷贝算法:为了解决标记清除算法的内存碎片问题,就产生了拷贝算法
内存分为大小相等的两半。每次只使用一半,垃圾回收之后,把上一半存活的对象到下一半,拷贝完之后,上面这一块内存就可以全部清除,这样内存全部都是连续的,就不会有内存碎片了
弊端:浪费空间,这么大的内存只能用一半?而且效率跟存活对象的个数有关,如果存活对象多,那么移动的次数就会非常多,因为所有的存活对象都要移动
-
3:MarkCompack 标记压缩算法:为了解决拷贝算法的空间利用率问题,标记之后,在回收的阶段,不是直接清理内存,而是把存活的对象往一端移动 ,然后将端边界以外的所有内存直接清除,就不用分两半了,所有内存都可以用,也解决了拷贝算法每个存活对象必须移动的问题
这三种算法各有利弊,各有各自的适合场景
6:JVM有哪些垃圾回收器,它们都是怎么工作的?什么是STW,它们都发生在哪些阶段 ?什么是三色标记,如何解决错标记和漏标记的问题,为什么要设计这么多的垃圾回收器?
什么是STW
Stop the world .是在垃圾回收算法执行过程中,需要将JVM内存冻结的一种状态(比如要打扫卫生,就让房间里的其他人先停止制造垃圾,等我打扫完)在STW状态下,java的所有线程都是停止执行的,GC线程除外,native方法可以执行(因为调用的是底层C++的代码,跟JVM没有太大的关系),但是这些native方法不能与JVM进行交互,GC各种算法优化的重点,就是减少STW,同时,这也是JVM调优的重点。
JVM的垃圾回收器
Serial:串行,通知所有线程,我要垃圾回收了,开启一个GC线程,垃圾回收之后,这些线程再执行。就像踢足球,需要GC时,直接暂停,GC完了再继续
弊端:只开启了一个GC线程,效率较低,Serial是比较早期的垃圾回收器,在单CPU还可以,在多CPU架构下,性能就会下降,通常只适用于几十兆的内存空间(就像一个人打扫房子,房子大了就打扫不过来)
Parallel:并行,在Serial基础上,多开启几个GC线程进行垃圾回收
PS(Pprallel Scavenge)+PO(Parallel old)是jdk1.8默认的垃圾回收器
在多CPU的架构下,性能会比Serial高很多(很多人一起扫房子)
CMS(Concurrent Mark Sweep并行标记清除):我们发现,很多人一起扫,也扫不过来
CMS是很重要的垃圾回收器,促进了垃圾回收器从分代到不分代的转变过程
核心思想是将STW打散,让一部分GC线程与用户线程并发执行,整个GC过程分为4个阶段
1:初始标记阶段:STW只标记出根对象直接引用的对象
2:并发标记:继续标记其他对象
3:重新标记:STW对并发执行阶段的对象进行重新标记
4:并发清除:并行,将产生的垃圾清除。清除过程中应用程序又会不断产生新的垃圾,这些垃圾叫浮动垃圾,留到下一次GC过程中清除
G1:Garbage First:垃圾优先
对于这个垃圾回收器,堆内存不再分新生代和老年代,而是把堆内存分为一个一个的小块,每一个小块叫做Region。逻辑上不分代,实际上是分代的。每个Region可以隶属于不同的年代
GC分为四个阶段
1:初始标记,标记出GC Root直接引用的对象。STW
2:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region
RSet:Region中的一个小的数据结构,在每一个Region当中会有一个RSet,记录与当前Region有引用关系的Region。 是并发的,没有STW
3:并发标记阶段:跟CMS差不多,不过遍历的范围不再是整个OLD区,只需要标记第2步标记出来的Region
4:重新标记:跟CMS的重新标记差不多,方法不同。把并发标记过程中产生变化了的对象重新标记一下
5:垃圾清理:采用复制算法,直接将整个Region的信息复制到一个新的Region,这个阶段G1只选则垃圾较多的Region进行清理,而不是全部清理
ZGC和shennandoah逗孩子啊优化之中,是未来的垃圾回收器
CMS三色标记
三色标记是CMS中用来标记的机制,是CMS的核心算法
是一种逻辑上的抽象,将每个内存对象分成三种颜色,黑色
表示自己和成员变量都已经标记完毕,灰色
表示自己标记完了,成员变量还没有标记完,白色
表示自己未标记完
错标记:
漏标记
为什么要设计这么多的垃圾回收器?
因为内存逐渐变大
7:如何进行JVM调优?JVM参数有哪些?怎么查看一个JAV进程的JVM参数
JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程序的运行速度
推荐阅读
Java面试200道系统教程助力拿下年薪60万!并发网络通信JVM面试缓存/微服务/spring/MySQL
图解Java 垃圾回收机制
携程面试官问我怎么划分 Java 虚拟机内存区域,相见恨晚!
携程面试官竟然问我 Java 虚拟机栈!
Java虚拟机(JVM)面试题(2020最新版)
JVM相关问题整理
Java垃圾回收、引用计数法、根可达算法
java垃圾回收机制–可达性算法
常见JVM面试题及答案整理
JVM面试题总结
用最直接的大白话来聊一聊Java对象的GC垃圾回收以及Java对象在内存中的那些事
深入理解 JVM 垃圾回收机制及其实现原理
JVM架构和GC垃圾回收机制(JVM面试不用愁)
Java虚拟机(JVM)你只要看这一篇就够了!
垃圾回收和GC调优
深入理解JVM的垃圾回收机制
标签:标记,对象,面试,GC,垃圾,JVM,之多,内存 来源: https://blog.csdn.net/ningmengshuxiawo/article/details/115671006