共计 4104 个字符,预计需要花费 11 分钟才能阅读完成。
方法区
方法区与 Java 堆一样,是各个线程共享的区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译 (JIT) 后的代码等数据。对于 JDK1.8 之前的 HotSpot 虚拟机而言,很多人经常将方法区称为我们上图中所描述的永久代,实际上两者并不等价,因为这仅仅是 HotSpot 的设计团队选择利用永久代来实现方法区而言。同时对于其他虚拟机比如 IBM J9 中是不存在永久代的概念的。
其实,移除永久代的工作从 JDK1.7 就开始了。JDK1.7 中,存储在永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap。但永久代仍存在于 JDK1.7 中,并没完全移除,譬如符号引用 (Symbols) 转移到了 native heap;字面量 (interned strings) 转移到了 java heap;类的静态变量 (class statics) 转移到了 java heap。而在 JDK1.8 之后永久代概念也已经不再存在取而代之的是元空间 metaspace。
常量池其实是方法区中的一部分,因为这里比较重要,所以我们拿出来单独看一下。注意我们这里所说的运行时的常量池并仅仅是指 Class 文件中的常量池,因为 JVM 可能会进行即时编译进行优化,在运行时将部分常量载入到常量池中。
程序计数器
JVM 中的程序计数器和计算机组成原理中提到的程序计数器 PC 概念类似,是线程私有的,用来记录当前执行的字节码位置。还是稍微解释一下吧,CPU 的占有时间是以分片的形式分配给给每个不同线程的,从操作系统的角度来讲,在不同线程之间切换的时候就是依赖程序计数器来记录上一次线程所执行到具体的代码的行数,在 JVM 中就是字节码。
Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈也是线程私有的,用通俗的话将它就是我们常常听说到堆栈中的那个“栈内存”。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧 (Stack Frame) 用于存储局部变量表(局部变量表需要的内存在编译期间就确定了所以在方法运行期间不会改变大小),操作数栈,动态链接,方法出口等信息。每一个方法从调用至出栈的过程,就对应着栈帧在虚拟机中从入栈到出栈的过程。p.s: 关于栈帧这里我们以后讲虚拟机字节码执行引擎的时候再来仔细分析。
本地方法栈
本地方法栈和 Java 虚拟机栈类似,只不过是为 JVM 执行 Native 方法服务,这里就不解释了。
堆
堆是用来存放对象的内存空间, 几乎所有的对象都存储在堆中。
堆的特点:线程共享整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个的。在虚拟机启动时创建垃圾回收的主要场所。可以进一步细分为:新生代、老年代。新生代又可被分为:Eden、From Survior、To Survior。不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,从而更高效。堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出 OutOfMemoryError。
总结
Java 虚拟机的内存模型中一共有两个“栈”,分别是:Java 虚拟机栈和本地方法栈。
两个“栈”的功能类似,都是方法运行过程的内存模型。并且两个“栈”内部构造相同,都是线程私有。
只不过 Java 虚拟机栈描述的是 Java 方法运行过程的内存模型,而本地方法栈是描述 Java 本地方法运行过程的内存模型。
Java 虚拟机的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。方法区本质上是属于堆的一个逻辑部分。堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器编译的代码。
堆是 Java 虚拟机中最大的一块内存区域,也是垃圾收集器主要的工作区域。
程序计数器、Java 虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java 虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样。
而堆、方法区是线程共享的,在 Java 虚拟机中只有一个堆、一个方法栈。并在 JVM 启动的时候就创建,JVM 停止才销毁。
JVM 面试问题
1、内存模型以及分区,需要详细到每个区放什么。
JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面,class 类信息常量池(static 常量和 static 变量)等放在方法区 new: 方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数据堆:初始化的对象,成员变量(那种非 static 的变量),所有的对象实例和数组都要在堆上分配栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针本地方法栈:主要为 Native 方法服务程序计数器:记录当前线程执行的行号
2、GC 的两种判定方法
引用计数法:指的是如果某个地方引用了这个对象就 +1,如果失效了就 -1,当为 0 就会回收但是 JVM 没有用这种方式,因为无法判定相互循环引用(A 引用 B,B 引用 A)的情况
引用链法:通过一种 GC ROOT 的对象(方法区中静态变量引用的对象等 -static 变量)来判断,如果有一条链能够到达 GC ROOT 就说明,不能到达 GC ROOT 就说明可以回收
3、GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?
先标记,标记完毕之后再清除,效率不高,会产生碎片
复制算法:分为 8:1 的 Eden 区和 survivor 区,就是上面谈到的 YGC
标记整理:标记完毕之后,让所有存活的对象向一端移动
4、几种常用的内存调试工具:jmap、jstack、jconsole、jhat
jstack 可以看当前栈的情况,jmap 查看内存,jhat 进行 dump 堆的信息
mat(eclipse 的也要了解一下)
5、类加载的几个过程
加载、验证、准备、解析、初始化。然后是使用和卸载了通过全限定名来加载生成 class 对象到内存中,然后进行验证这个 class 文件,包括文件格式校验、元数据验证,字节码校验等。准备是对这个对象分配内存。解析是将符号引用转化为直接引用(指针引用),初始化就是开始执行构造器的代码
6、双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
Bootstrap ClassLoader: 启动类加载器,负责将 Java_Home /lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。ApplicationClassLoader:它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器双亲委派模型是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。—– 例如类 java.lang.Object,它存在在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的 Bootstrap ClassLoader 进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个 java.lang.Object 的同名类并放在 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将混乱
7、SafePoint 是什么
比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始执行 GC,
循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入 safepoint)
方法返回前
调用方法的 call 之后
抛出异常的位置
8、如和判断一个对象是否存活?(或者 GC 对象的判定方法)
判断一个对象是否存活有两种方法:
引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”, 将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
2. 可达性算法(引用链法)
该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
在 java 中可以作为 GC Roots 的对象有以下几种:
虚拟机栈中引用的对象
方法区类静态属性引用的对象
方法区常量池引用的对象
本地方法栈 JNI 引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达 GC Root 时,这个对象并
不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记
如果对象在可达性分析中没有与 GC
Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。
如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F -Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 F -Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F -Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。