JVM
深色为所有线程共享数据区;浅色为线程隔离区。
简述JVM内存模型
线程私有的运行时数据区:程序计数器、Java虚拟机栈、本地方法栈。
线程共享的运行时数据区:Java堆、方法区
程序计数器
内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。
此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
虚拟机栈
Java虚拟机栈用来描述Java方法执行的内存模型。线程创建时就会分配一个栈空间,线程结束后栈空间被收回。
栈中元素用于支持虚拟机方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和返回地址等信息。
动态链接:主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。
局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)
虚拟机栈会产生两类异常:
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。
OutOfMemoryError:如果JVM栈内容可以动态扩展,虚拟机栈占用内存超出抛出。
本地方法栈
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为本地方法服务。可以将虚拟机栈看作普通的java函数对应的内存模型,本地方法栈作由native关键词修饰的函数对应的内存模型。
本地方法栈会产生两类异常:
StackOverflowError:线程请求的栈深度大于虚拟机允许的深度抛出。
OutOfMemoryError:如果JVM栈内容可以动态扩展,虚拟机栈占用内存超出抛出。
JVM中的堆
堆的主要作用是存放对象实例,Java里几乎所有对象实例都分配在堆分配内存,堆也是内存管理中最大的一块。Java内存回收主要就是针对堆这一区域进行。可以通过-Xms和-Xmx设置堆的最小和最大容量。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
方法区
方法区用于存储被虚拟机加载的类信息、常量、静态变量等数据。
JDK6之前使用永久代实现方法区,容易内存溢出。JDK7把放在永久代的字符串常量池、静态变量等移出,JDK8抛弃永久代,改用在本地内存中实现的元空间来实现方法区,把JDK7中永久代内容移到元空间。
方法区会抛出OutOfMemoryError异常。
运行时常量池
运行时常量池存放常量池表,用于存放编译器生成的各种字面量与符号引用。一般除了保存Class文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。除此之外,也会存放字符串基本类型。
JDK8之前,放在方法区,大小受限于方法区。JDK8将运行时常量池存放堆中。
简述直接内存
直接内存也称为堆外内存,就是把内存对象分配在JVM堆外的内存区域。这部分内存不是虚拟机管理,而是由操作系统来管理。Java通过DirectByteBuffer对其进行操作,避免了在Java堆和Native堆来回赋值数据。
简述Java创建对象的过程
1.检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
2.通过检查通过后虚拟机将为新生对象分配内存。
3.完成内存分配后虚拟机将成员变量设为零值。
4.设置对象头,包括哈希码、GC信息、锁信息、对象所属类的类元信息等。
5.执行init方法,初始化成员变量,执行实例化代码,调用类的构造方法,并把堆内信息对象的首地址赋值给引用变量。
对象访问定位
Java 堆中会分配一块内存作为句柄池。reference 存储的是句柄地址。 reference 中直接存储对象地址
String s = new String;
s在Java虚拟机栈,new后面的String为实例对象在Java堆,类型前面的String存储在方法区。
使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)是只改变实例数据指针地址,reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。如果是对象频繁 GC 那么句柄方法好,如果是对象频繁访问则直接指针访问好。
简述JVM给对象分配内存的策略
1.指针碰撞:这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。
2.空闲列表:对于Java堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
Java对象内存分配是如何保证线程安全的
1.对分配内存空间采用CSA机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。
2.每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这个”私有“内存中分配。一般采用这种策略。
简述对象的内存布局
对象在堆内存的存储布局可以分为对象头、实例数据和对齐填充。
对象头主要包含两部分数据:MarkWord、类型指针。MarkDown用于存储哈希码(HashCode),GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。类型指针即对象指向它的类元数据指针,如果对象是一个Java数组,会有一块用于记录数组长度的数据。
实例数据存储代码中定义各种类型的字段信息。
对齐填充起占位作用。HotSpot虚拟机要求对象的起始地址必须是8的整数倍,因此需要对齐填充。
如何判断对象是否是垃圾
引用计数法:设置计数器,对象被引用计数器加一,引用失效时计数器减一,如果计数器为0则被标记为垃圾。会存在对象间循环引用问题,一般不使用这种方法。
可达性分析:通过GC Roots 的根对象作为起始点,从这些节点开始,根据引用关系向下搜索,如果某个对象没有被搜到,则会被标记为垃圾。可作为 GC Roots 对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。
简述Java的引用类型
强引用:被强引用关联的对象不会被回收。一般采用new方法创建强引用。
软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。一般采用SoftReference类来创建软引用。
弱引用:垃圾回收期碰到即回收,也就是说它只能存活到下一次垃圾回收之前。一般采用WeakReference类来创建弱引用。
虚引用:无法通过该引用获取对象。唯一目的就hi为了等在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用。
简述标记清除算法、标记整理算法和标记复制算法
标记清除算法:先标记清除的对象,之后统一回收。这种方法效率不高,会产生大量不连续的碎片。
标记整理算法:先标记存活对象,然后让所有存活对象向一端移动,之后清理端边界以外的内存。
解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,
标记复制算法:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已经使用过的内存空间一次清理掉。
简述分代收集算法
根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。一般将堆分为新生代和老年代,对这两块采用不同的算法。新生代使用:标记复制算法。
老年代使用:标记清除算法或者标记整理算法。
常见内存分配策略
大多数情况下对象在新生代Eden区分配,当Eden没有足够空间时将发起一次Minor GC。
大对象需要大量连续内存空间,直接进入老年代分配。
如果经历过一次Minor GC 仍然存活且能够被Survivor容纳,该对象就会移动到Survivor中并将年龄设置为1,并且每经过一次Minor GC年龄就加一,当增加到一定程度(默认15)就会被晋升到老年代。
空间分配担保。Minor GC前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次Minor GC 确定安全。如果不满足,JVM会查看HandleProtionFailure参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将Minor GC,否则改一次Full GC。
简述JVM类加载过程
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
(1)遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时没初始化触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。
(2)使用 java.lang.reflect 包的方法对类进行反射调用的时候。
(3)当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
(4)当虚拟机启动时,用户需指定一个要加载的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
(5)当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
加载:
1.通过全类名获取的二进制字节流
2.将类的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成类的Class对象,作为方法区数据的入口
验证:对文件格式,元数据。字节码,符号引用等验证安全性。是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。
准备:在方法区为类变量分配内存并设置初始值。
解析:这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化:执行类构造器clinit方法,真正初始化。
简述JVM中的类加载器
BootsrapClassLoader启动类加载器:加载/lib下的jar包和类。C++编写。
ExtensionClassLoader扩展类加载器:/lib/ext目录下的jar包和类。Java编写
AppClassLoader应用类加载器:加载当前当前ClassPath下的jar包和类。Java编写。
简述双亲委派机制
一个类加载器收到类加载请求后,首先判断当前类是否被加载过。已经被加载的类会直接返回,如果没有被加载,首先将类加载请求转发给父类加载器,一直转发到启动类加载器,只有当父类加载器无法完成时才尝试自己加载。
加载类顺序:BootstrapClassLoader -> ExtensionClassLoader -> AppClassLoader -> CustomClassLoader
检查类是否加载顺序: CustomClassLoader -> AppClassLoader -> ExtensionClassLoader -> BootstrapClassLoader
双亲委派机制优点:
1.避免类的重复加载。相同的类被不同的类加载器会产生不同的类,双亲委派保证了Java程序的稳定运行。
2.保证核心API不被修改。
优点:双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
调用system.gc()一定会发生垃圾收集吗?
调用system.gc()的时候,其实并不会马上进行垃圾回收,只会把这次gc请求记录下来。需要配合System.runFinalzation()才会进行真正回收。
内存溢出和内存泄漏
内存溢出:程序在申请内存时,此时已用内存过多,没有足够的剩余空间供其使用。
内存泄漏:程序在申请内存后,不能完全释放已申请的内存空间。
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
标签:面试题,Java,对象,虚拟机,引用,内存,JVM,加载 From: https://www.cnblogs.com/baifeili/p/16494432.html