我们先从Minor GC
说起吧,当对象分配到Eden
区发现Eden
区空间满了,此时就会触发Minor GC
,将非存活对象回收,再将存活对象放到From区(S1区)
,再将新创建的对象放到Eden
区。
随着时间推移,Eden
区再次满了,此时再次触发Minor GC
,将非存活对象清理,存活对象放到S2区
。
然后我们再来说说对象升级到老年代的4种情况:
Minor GC
后,S区
的toSpace
区无法容纳的存活的对象。PretenureSizeThreshold
知晓值的大小。JVM
默认设置为15(即在年轻代经过15次Minor GC)
From
空间中,相同年龄所有对象的大小总和大于From
和To空间
总和的一半,那么年龄大于等于该年龄的对象就会被移动到老年代,而不用等到15岁(默认)
接下来就是Full GC
了因:
因为上述原因需要将对象放到老年代,但是老年代的空间不够存放对象时就会触发Full GC
,如果Full GC
完还是无法容纳新对象就会报OOM
的异常。
补充:空间分配担保
在发生Minor GC
前,JVM会检查老年代最大连续可用空间是否大于年轻代所有对象的总空间:
若大于,则说明本次垃圾回收是安全,继续执行Minor GC。
若小于,则看参数HandlerPromotionFailure
,若为true,则无视这个风险,继续检查老年代的最大连续空间大小是否大于年轻代对象的平均大小:
2.1 若大于,则继续minor GC
2.2 若小于则升级为Full GC
内存区域有堆区、虚拟机栈、本地方法栈、程序计数器、方法区。
其中方法区和堆区为线程共享的。其余都是线程隔离的。
而各个组成部分的作用为:
程序计数器(Program Counter Register)
也被称为 PC 寄存器,是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。它指向当前线程要执行的下一条指令的地址。Java 虚拟机栈(Java Virtual Machine Stack)
:也是线程私有的,它的生命周期与线程相同。Java 虚拟机栈描述的是 Java 方法执行的线程内存模型,方法执行时,JVM 会同步创建一个栈帧,用来存储局部变量表、操作数栈、动态连接等。本地方法栈(Native Method Stacks)
:与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。Java 堆(Java Heap)
是虚拟机所管理的内存中最大的一块。Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java 里“几乎”
所有的对象实例都在这里分配内存。“GC 堆”(Garbage Collected Heap)
。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以 Java 堆中经常会出现新生代、老年代、Eden空间、From Survivor空间、To Survivor空间等名词,需要注意的是这种划分只是根据垃圾回收机制来进行的划分,不是 Java 虚拟机规范本身制定的。方法区
:是比较特别的一块区域,和堆类似,它也是各个线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。它特别在 Java 虚拟机规范对它的约束非常宽松,所以方法区的具体实现历经了许多变迁,例如 jdk1.7 之前使用永久代作为方法区的实现。JDK8
则是用元数据区实现作为方法区的实现。去掉了永久代这么个东西,而元数据区存放的仍然是运行时常量池和类常量池。答: 方法区主要是用于存储类信息、静态变量以及常量信息的。是各个线程共享的一个区域。我们都知道JVM中有个区域叫堆区,所以有时候人们也会称方法区为Non-Heap(非堆)
。
在JDK8
之前方法区存放在一个叫永久代的空间里。
在JDK8
之后由于HotSpot 和JRockit 的合并,所以方法区就被作为元数据区了。
答: 其实方法区并不是一个实际的区域,他不过是JVM定义的一个规范而已。在HotSpot 实现方法区的方式就在JVM
内存中划分一个区域作为永久代来存放这些数据。
在JDK8之前我们可以用下面的参数来调整永久代的大小
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
JDK8
之后要把永久代 (PermGen)换成`元数据区(MetaSpace) ?答: 将数据放在永久代固然没问题,但是随着时间的推移,方法区使用的空间可能会逐渐变大,若我们分配大小不当很可能造成线上OOM
问题,所以设计者们就在方法区移动到本地内存中,通过本地内存来存放数据。并且元数据区默认分配值为unlimited(我们也可以通过-XX:MetaspaceSize
来动态调整),理论上是没有明确大小,是可以动态分配空间的,这样一来由于元数据区就不会受到JVM内存分配的约束了,所以理论上发生OOM的概率会小于永久代。
首先我们需要了解一下类常量池
类常量池:主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)。
答: 我们都知道Class
文件包含字段描述符
、方法描述符
、接口
等描述信息,还有编译器生成的字面量和符号引用,都会被存放到JVM
方法区的运行时常量池中。
答: runtime constant pool
(而不是interned string pool / StringTable之类的其他东西)的话,其中的引用类型常量(例如CONSTANT_String、CONSTANT_Class、CONSTANT_MethodHandle、CONSTANT_MethodType之类)
都存的是引用,实际的对象还是存在Java heap
上的。
答: 在JDK1.4中 NIO(New Input/Output)
类提供的一个名为MappedByteBuffer
的内存映射文件的方式,直接调用Native
操作本机内存,通过这种方式避免操作数据从JVM堆
到Native
堆的开销,从而提高程序执行效率。这就意味着这种这个操作会受到本机内存大小以及处理器寻址空间的限制。
答: 整体过程大抵是一下几个步骤:
(除了对象头)
。哈希码
、对象的GC分代信息
、元数据
信息、以及是否使用偏向锁等都放到对象头中。
完成对象的创建。宏观来说有这么几个模块,大抵可以分为:
先来说说对象头,它由两个部分组成,第一个部分则是记录自身信息的,包含哈希码、gc分代年龄、锁状态标志、线程持有的锁、偏向锁id、偏向时间戳等,它也叫markword。需要补充的是,如果这个是属于数组类型的话
第二个部分则是类型指针,指向对象的类元数据类型,这个类型指针的存在使得我们可以知晓它是哪个类。
实例数据用来存储对象中各自类型的字段内容,即使是从父类继承来的,它也会记录。
对齐填充不是必须的,仅仅作为占位符使用的。
答: 一种是指针碰撞
、还有一种是空闲列表
。
指针碰撞使用是堆区空间规整的情况下,例如你使用复制算法
、或者标记-整理
算法时,堆区空间就是规整的。而空闲列表则适用于空间不完整的情况,例如标记-清除
算法。
Java
内存对象多线程并发分配问题吗?答: 在分配空间时JVM
首先会预先为Eden区
为每个线程分配一个TLAB
空间。每次线程都只能操作自己的TLAB
区以及读取其他线程的TLAB区(但是不能操作),若TLAB
空间满了或者不够分配当前对象时,则基于CAS+失败重试在堆区其他空间尝试分配空间。
答: 有两种方式,一种是使用句柄
,一种是直接指针
。句柄方式则将对象地址、以及对象对应的类的地址信息存放到一个句柄中,引用直接通过句柄得到对象的实际地址,进而去操作要访问的对象。
直接指针方式则是引用中直接记录对象的地址,我们直接通过引用的地址得到对象的地址进而直接操作对象。而对象类型数据信息则都存储在堆中的对象头里。
所以前者优势是稳定,即引用无需因为对象的移动而改变则动态修改。后者优势则是略去了访问句柄的一步,效率更高一些。
答: 对象优先会被分配在eden区
,如果是大对象直接分配到老年区(避免分配担保机制的负担)
,当对象存活时间达到-XX:MaxTenuringThreshold
的值时也会到老年区。
答: 如果我们创建了一个大对象,Eden
区不足以分配该对象,就会将Eden
区的对象移到Survivor
区,然后将大对象放到Eden
区。
内存泄露指的无用的对象未能实时的清除,导致堆区内存被一些无用的垃圾占用。而导致内存泄漏的大概会有以下几个原因:
public class OOM {
static List list = new ArrayList();
public void oomTests(){
Object obj = new Object();
list.add(obj);
}
}
public class Simple {
Object object;
public void method1(){
object = new Object();
//...其他代码
//由于作用域原因,method1执行完成之后,object 对象所分配的内存不会马上释放
object = null;
}
}
而内存溢出则是当前堆区空间无法容纳新对象导致OOM问题。代码如下所示
/**
* VM参数: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List list = new ArrayList();
while (true) {
list.add(new OOMObject());
}
}
}
可以说内存泄漏会导致内存溢出。
Minor GC/Young GC
指的是年轻代的垃圾收集。Major GC/Old GC
指的是老年代的GC,目前只有CMS收集器会有单独收集老年代的行为。Mixed GC
:混合收集,指的是新生代和老年代的垃圾收集,目前只有G1收集器会有这种行为。Full GC
:收集整个Java堆和方法去的垃圾收集。当年轻代空间不足的时候就会触发Minor GC
Young GC
的时候,发现老年代可用的连续内存空间 < 新生代历次Young GC
后升入老年代的对象总和的平均大小,说明本次 Young GC
后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC
。Minor gc
后老年代空间不足:执行 Young GC
之后有一批对象需要放入老年代,此时老年代就是没有足够的内存空间存放这些对象了,此时必须立即触发一次 Full GC
system.gc()
这个可以通过下面这个参数进行设置
- XX:MaxTenuringThreshold
-XX:PretenureSizeThreshold
- XX:MaxTenuringThreshold
才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。答: 被强引用(StrongReference)
指向的对象无论如何都不会被垃圾回收器回收,宁可被OOM也不会被回收。软引用(SoftReference)
相较于强引用地位低一些,当内存空间不足的时候,它就会被直接回收,一旦它引用的对象被回收,他就被存放到一个与之关联的引用队列中。弱引用(WeakReference)
地位比强引用更低,只要被垃圾回收器线程扫描到就会被直接回收,一旦被回收该引用也被被存放到与之关联的一个队列中。虚引用(PhantomReference)没
有任何地位,任何时间段都能够被回收。
当然,在平时工作中弱引用和虚引用使用的就不是很多,更多是使用软引用,因为软引用不会在没必要的时候被回收,只有内存不足时才能回收,这样对于JVM
性能开销来说回更节约一些。
答: 我们以字符串为例,如果字符串常量没有任何引用指向的话,那么在垃圾回收阶段这个常量就会被回收。
答: 这里说到的是类吧?判断类是否无用大概是从以下3点判断:
1. 这个类的所有实例都被垃圾回收器回收了,也就是Java堆中没有任何该类的实例。
2. ClassLoader 被回收了。
3. java.lang.Class类没有被被引用,无法通过任何地方完成反射操作了。
如果符合上述三点,就说明这个类可以被回收了,注意仅仅是可以,不代表真的就要被回收了。
答: 主要是为了提高GC效率,对年轻代和老年代采用不同的回收算法,利用好每个内存区域空间。
答: class文件即字节码文件,是面向虚拟机的一种文件,它解决传统解释器语言效率低的问题。也正是由于它是面向虚拟机的文件,所以Java代码只需编译一次即可在任何有虚拟机的平台使用。
答: 加载,连接(验证、准备、解析、初始化)、初始化
大抵分为以下三步:
方法区
生成Class
对象,作为访问这些数据的入口。答:知道,大概有以下三个:
%JAVA_HOME%/lib
目录下的 jar 包和类或者或被 -Xbootclasspath
参数指定的路径中的所有类。答: 如下所示,从源码中我们就可以了解双亲委派机制了,可以看到loadClass方法会先去查看方法区中是否有该类的加载信息。若没有则会先让根加载器先尝试加载,若没有则再找扩展类加载器,最后才是应用程序类加载器。
private final ClassLoader parent;
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经被加载过
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {//父加载器不为空,调用父加载器loadClass()方法处理
c = parent.loadClass(name, false);
} else {//父加载器为空,使用启动类加载器 BootstrapClassLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//抛出异常说明父类加载器无法完成加载请求
}
if (c == null) {
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;
}
}
答: 有两点好处:
答: 如果想自定义类加载器的话,继承ClassLoader
类就好了。如果想破坏双亲委派机制的话,就重写我们上文所说的loadClass
方法。
jdk8之后,静态成员变量存储在哪?有说存在元数据区,有说迁移到堆中?希望大佬能给个详细的解答,多谢? - 红尘修行的回答 - 知乎
https://www.zhihu.com/question/324306038/answer/688264413
jdk8之后,静态成员变量存储在哪?有说存在元数据区,有说迁移到堆中?希望大佬能给个详细的解答,多谢? - 红尘修行的回答 - 知乎
https://www.zhihu.com/question/324306038/answer/688264413
java static GC 回收问题:https://blog.csdn.net/kangojian/article/details/5186530
JVM内存分配担保机制
运行时常量池(JVM06)
剖析面试最常见问题之JVM(下)