JVM学习笔记

10 篇文章 1 订阅

Java虚拟机运行时数据区

在这里插入图片描述

1、为什么要学习Java虚拟机?

答:Java在java虚拟机的帮助下,不需要为每个new操作去写配对delete/free代码,不容易出现内存泄漏和内存溢出的问题。但一旦出现这些问题,如果不了解java虚拟机到底是如何工作的那么便无法很好地修正代码。

2、程序计数器是什么?

答:程序计数器其实就是一块较小的内存,是线程所执行指令行号的指示器,程序计数器是线程隔离的数据区(只允许对应的线程访问)。因为java虚拟机的多线程是通过对线程进行轮流切换来分配使用处理器来实现的。所以对于一个单核处理器,每一个确认的时刻只有一个线程在执行。

但如何使切换的线程快速定位到自己之前执行到的指令位置呢?就需要使用到程序计数器。java虚拟机为了使线程切换后能立刻定位到正确的执行位置,java虚拟机给每个线程都分配了一个独立的程序计数器来存储该线程正在执行的字节码指令地址。

3、java虚拟机栈是什么?

答:每个java线程在创建时都会伴随着创建一个虚拟机栈,java虚拟机栈是线程隔离的数据区(只允许对应的线程访问)。栈中的数据都是以栈帧(Stack Frame)的格式存在,线程上正在执行的每个方法都各自对应一个栈帧,每个方法的执行都伴随着入栈和出栈。

在这里插入图片描述

4、栈帧的内部结构

  • 局部变量表(Local Variables)

    是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
    虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。

  • 操作数栈(Operand Stack)(表达式栈):
    一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

  • 动态链接(Dynamic Linking):一个指向运行时常量池中该栈帧所属方法的符号引用。

  • 方法返回值(Return Address):方法正常退出或者异常退出的定义

  • 一些附加信息

5、栈执行过程举例

public class MethodAreaDemo {
    public static void main(String args[]) {
        int x = 500;
        int y = 100;
        int a = x / y;
        int b = 50;
        System.out.println(a+b);
    }
}

①方法中的参数args存入本地变量表
img
②将操作数500放入操作数栈中
img
③然后存储到局部变量表中,后面的y和b也是如此
img
④将本地变量表中变量1和变量2取出放入操作数栈,再进行除法运算,将运算结果存入本地变量表中
img
img
⑤然后就是输出流,需要调用运行时常量池的常量
img
⑥最后调用invokevirtual(虚方法调用)创建System.out.println方法的栈帧,将操作数栈的运行结果作为参数压入System.out.println方法的操作数栈中,虚拟机转而执行栈顶部的方法。
img

6、栈溢出与内存溢出?

答:java虚拟机允许栈的深度有两种一个是动态的一个是固定不变的

如果采用固定深度的java虚拟机栈,当一个程序递归过深,超过java虚拟机允许的最大深度时,就会抛出StackOverflowError异常表示栈溢出

如果采用可动态拓展的java虚拟机栈,那么如果java虚拟机在尝试拓展时无法申请到足够内存或者创建新的线程时没有足够内存去创建时,也会抛出StackOverflowError异常。 远古时代的Classic虚拟机会抛出OutOfMemoryError异常表示内存溢出。

总而言之java虚拟机栈会在栈深度溢出或者栈拓展失败时,即当栈帧内存不能分配时就会抛出StackOverflowError异常
在这里插入图片描述

7、本地方法栈是什么?与java虚拟机栈有什么区别?

答:本地方法栈与虚拟机栈发挥的作用非常相似,区别是java虚拟机栈是为虚拟机执行的java方法服务,而本地方法栈则是为虚拟机使用到的本地的方法服务。有些虚拟机会将这两种栈合并在一起,使得本地方法栈也能和java虚拟机栈一样有栈溢出和内存溢出。本地方法栈也是线程隔离的数据区(只允许对应的线程访问)。

8、java堆

答:java堆是java虚拟机所管理的内存中最大的一块,且java堆是所有线程共享的一块内存区域(即允许所有java虚拟机线程访问),在虚拟机启动时创建。

java堆用来存放动态产生的数据,比如new出来的对象,详细点就是new出来的对象的实例数据。因为堆中只存储创建出来的对象的实例数据,并不包括成员方法。因为同一个类的不同对象(对象即实例)拥有各自的成员变量,存储在堆中的不同位置,但是同一个类不同实例的他们共享该类的方法(存储在方法区,可以理解为可以共享的数据如方法、类信息、final类型的数据等存储在方法区中,而动态的可以改变的数据存储在堆中),所以并不是每创建一个对象就把这个类解析一遍,成员方法也复制一次。

Java 堆是垃圾收集器管理的主要区域。堆内存分为新生代 (Young) 和老年代 (Old) ;新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor
在这里插入图片描述

9、方法区

答:在旧版本中被称为永久代,在新版本中叫元空间(它们一个在堆空间中,一个在本地内存中)

方法区用于存储已经被虚拟机加载的对象的类型数据、常量、静态变量、静态代码块、即时编译器编译后的代码缓存。

其中类型数据包括

  • 对象的完整有效名称(全名=包名.类名)。
  • 对象的直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)。
  • 对象的修饰符(public,abstract,final的某个子集)。
  • 对象的直接接口的一个有序列表。
  • 方法

如果方法区无法满足新的内存分配需求时,会抛出OOM(OutOfMemoryError)异常

10、元空间与永久代的区别?为什么要用元空间代替永久代?

答:元空间是堆区逻辑的一部分,即元空间逻辑上在堆区中,但物理上不存在于堆中存储于本地内存中

元空间与永久代的最大区别:元空间使用本地内存,而永久代使用的是JVM的内存;

因为永久代空间是固定的,设置太小,永久代会出现永久代OOM异常(OutOfMemoryError:PermGen space)异常,如果设置太大,则会导致虚拟机内存紧张,所以将永久代独立出来,单独放置在本地内存中,元空间的大小仅受本地内存限制就不会出现永久代OOM异常

11、堆、栈、方法区之间的联系

答:执行一个main方法(即开启一个线程),会生成一个程序计数器用于存放指令执行位置,栈中也会压入该main方法的栈帧,且动态链接指向方法区中该方法的引用,并将其转换为直接引用。

不同指令的操作:

对变量的操作:先将局部变量的值存进操作数栈,再取出放进本地变量表中。

new了一个对象:首先去查找这个new的对象能否在常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否已经被加载。如果没有那么必须先执行相应的类的加载过程。加载完成后虚拟机就会为其分配内存来存放对象和其从父类继承过来的实例变量并给这些实例变量赋予默认值。对象名相当于索引,指向堆中该对象的实例,存放在栈中的本地变量表中;方法、类型信息等存储在方法区即元空间中。

执行一个方法:到方法区中获取该方法的动态链接,方法的参数通过本地变量表传入,如果要运算的话压入操作数栈中进行计算(其中一部分可能只是引用,需要到堆中获取变量的实例),在栈中压入该方法的栈帧,java虚拟机转而去执行该方法。

访问对象:通过本地变量表中该对象的引用,有两种方式

一个是通过直接指针访问对象,即该引用可以直接指向堆中对象的实例,从而访问对象,如果要调用对象的方法或常量值,可以通过实例中对象类型的指针去访问方法区中的方法或常量值。

一种是通过句柄访问对象,因为堆中有垃圾回收机制,对象实例数据会不断变动,所以在堆中开辟一个句柄池,当对象位置发生变动时,只需要修改句柄池中的指针即可。

img
在这里插入图片描述
img

12、字符串常量池(String pool)

答:字符串常量池在全局中只有一个,存放的是字符串对象的引用(即堆区该字符串的位置)。一个字符串被创建,先会到字符串常量池中查找这个字符串是否已经被创建过了,如果创建过,那么直接返回字符串常量池中该字符串的引用,若没有被创建过,那么先在堆区开辟一个空间来存储该字符串的实例,再在字符串常量池存储该字符串的引用并返回该引用。

13、class文件常量池(class constant pool)

答:一个java文件被编译为class文件时,除了会生成这个类的基本信息(类的版本,接口信息,字段和方法的描述等)之外还有一个就是常量池,用于存放编译过程中生成的字面变量和符号引用

字面量就是我们所说的常量的概念,如文本字符串,被声明为final的常量等

符号引用是指使用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

这里写图片描述

14、运行时常量池

答:jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在上面我也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用。

运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。

15、内存分配过程

public class HelloWorld {
    public static void main(String []args) {
		String str1 = "abc"; 
		String str2 = new String("def"); 
		String str3 = "abc"; 
		String str4 = str2.intern(); 
		String str5 = "def"; 
		System.out.println(str1 == str3);//true 
		System.out.println(str2 == str4);//false 
		System.out.println(str4 == str5);//true
    }
}

①第一行:首先堆中会存储“abc”的实例,字符串常量池中会存储abc实例的一个引用值,并将此引用值与str1对应,即str1代表的其实是abc的引用值。

②第二行:会生成两个实例,一个是def字符串的实例并在字符串常量池中存储该实例的引用值,一个是new出来的def对象,str2代表的是new出来的def对象。

③第三行:在解析第三行时,发现字符串常量池已经有abc对象的实例,直接将该实例的引用值赋给str3

④第四行:str4在运行时调用intern()函数,返回字符串常量池中def的引用值

注:==的概念

对于基本数据类型之间应用的双等号,比较的是他们的数值。
对于复合数据类型(类)之间应用的双等号,比较的是他们在内存中的存放地址。

16、使用常量池的好处

答:常量池是为了避免频繁的创建和销毁对象而影响系统性能,来实现对象的共享。

如字符串常量池,在编译阶段就将所有相同的字符串合并,只占用一个空间

优点1:节省内存空间,常量池中所有相同的字符串常量被合并,只占用一个空间

优点2:节省运行时间,比较字符串时,== 比equals快,对于两个引用变量,只用 ==判断引用是否相等也就可以判断两个字符串是否相等。

17、基本数据类型的包装类和常量池

答:java中基本类型的包装类的大部分都实现了常量池技术即Byte,Short,Integer,Long,Character,Boolean

前面 4 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean则 直接返回True Or False。

如果超出对应的范围,仍会去创建新的对象

以Integer为例

Integer i1 = 40;
Integer i2 = 40;
System.out.println(i1==i2);//输出TRUE
Integer i3 = 400;
Integer i4 = 400;
System.out.println(i3==i4);//输出false
Integer i5 = new Integer(40);
Integer i6 = new Integer(40);
Integer i7 = new Integer(0);
System.out.println("i5=i6+i7   " + (i5 == i6 + i7));//i5=i6+i7   true
System.out.println("40=i6+i7   " + (40 == i6 + i7));//40=i6+i7   true 

因为i1与i2都为40,而Integer在-128-127之间有相应的缓冲数据,所以直接返回对应常量池中该对象的引用,而一旦超过这个返回就要创建新的对象,所以两个对象的地址不同自然也就返回false了。

至于i4 == i5+i6,因为+不适用与Integer对象,所以i5和i6先进行自动拆箱操作,进行数值相加,即i4 == 40;然后因为Integer无法与数值直接比较,所以i4也拆箱为int值40,所以这条语句最终转换为int类型的40与int类型的40比较。

Integer缓存的原码

//Integer 缓存代码 :
public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
    	return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

18、如何判断对象是否死亡?

①引用计数法(java不使用)

给每一个已创建的对象添加一个计数器,一旦有地方引用了该对象,则该对象的引用计数器加1,如果引用失败了,则计数器减1,这样当计数器值为0时,就说明该对象没有被引用,也就是说这个对象死了。

但引用计数器无法解决对象间互相引用的例子,如:

class Person{
    public Person lover = null;//定义一个爱人

    private String name = "";//姓名
    Person(String name){
        this.name = name;
    }
}
public class Demo {
    public static void main(String[] args) {
        Person liangshanbo = new Person("梁山伯");//创建一个人物:梁山伯
        Person zhuyingtai = new Person("祝英台");//创建一个人物:祝英台
        liangshanbo.lover = zhuyingtai;//设置梁山伯的爱人是祝英台
        zhuyingtai.lover = liangshanbo;//设置祝英台的爱人是梁山伯
    }
}

main方法结束了,按正常来讲两个Person对象都要被回收,但这两个对象间存在着互相引用,导致垃圾回收机制无法回收这两个类。

②可达性分析

通过一系列GC roots的对象作为起点,如果一个对象没有通过引用链接到GC roots节点,那么说明此对象是不可用的。

Java虚拟机的垃圾收集机制如何判断对象是否死亡?

19、GC roots对象可以是什么?

虚拟机栈中栈帧的本地变量表中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象

引用的分类:

强引用: 普遍存在的引用,如new一个对象就是强引用

软引用: 描述一些还有用但并非必须的对象,如果内存足够垃圾回收器是不会回收这个对象的,只有当内存不足时才会对它进行回收。

应用场景——缓存,当缓冲区内存足够时缓存数据,内存不够时释放数据并执行方法,再进行回收。

弱引用: 描述非必需的对象,当进行GC时一定会被回收

虚引用: 和没有引用效果一样,当进行GC时一定会被回收

应用场景——用于对象销毁前的操作,以释放对象占用的资源

20、垃圾收集算法

①起源——分代收集理论

两个假说:

1)弱分代假说: 绝大多数对象都是朝生夕灭的

2)强分代假说: 熬过越多次垃圾回收的对象就越难以消亡

两个分代假说奠定了垃圾收集器设计的一致性原则:将java堆划分出不同区域,将新生对象和熬过垃圾收集次数少的对象放在一个区域,将熬过垃圾收集次数多的对象放在另一个区域,这样垃圾回收时先考虑回收年轻区域的对象就能得到更高的效率

但也可能存在这样的问题,新生代区中的对象可能被老年代所引用,所以为了准确的找到新生代区存活的对象而不产生误判,则必须要遍历整个老年代的所有对象来保证分析结果的准确性,那么之前的分区就毫无意义。

解决问题的第三条假说:

3)跨代引用假说: 跨代引用相对于同代引用来说占比极小。

可以理解,两个存在互相引用关系的对象,应该是倾向于同时生存和同时消亡的。比如如果新生代中某个对象存在跨代引用,由于老年代中的对象难以消亡,那么新生代中的该对象应该也是难以消亡的,随着年龄的递增,晋升到老年代中时,跨代引用也随之消除了。

根据这个假说,我们不应该为了少量的跨代引用而耗费大力气去扫描整个老年代,只需要在新生代建立一个全局的数据结构(记忆集)这个结构存储老年代中哪一块对象的内存会存在跨代引用。当发生新生代垃圾回收(Minor GC)时,只需要将包含跨代引用的内存中的老年代对象加入到垃圾回收器里进行扫描即可。

②标记-清除算法

答:分为标记和清除两个阶段,首先标记出需要回收的对象,标记完成后统一回收所有被标记的对象。也可以反过来,标记所有不需要回收的对象,然后回收没有标记的对象。

缺点:

  • 执行效率不稳定,如果包含大量对象,那么标记和清除过程耗时就会较长;
  • 内存空间碎片化,可能会导致需要分配内存较大的对象无法找到足够大的空间而提前触发垃圾回收。
    在这里插入图片描述

③标记-复制算法

将可以内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存空间用完时就执行垃圾回收算法,将还活着的对象复制到另一块内存中,将当前这块内存空间一次性清理掉。

缺点:会产生大量的内存间复制的开销,但因为大部分对象都会被回收,所以需要复制的只有少数对象;需要两倍的空间。

优点:内存分配是不需要考虑空间碎片的复杂情况,因为复制后的内存是连续的,内存分配只需要移动堆顶指针按顺序分配即可。

在这里插入图片描述

④标记-整理算法

过程与标记-清除一样,清除完成后,所有存活的对象都行内存中的一边移动,然后清除掉边界以外的内存。
在这里插入图片描述

20、堆的内存分配

新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活(一般经历过一次GC没挂,年龄就加一,年龄大于15的对象就要进入老年代)的对象移动到老年代。

老年代存储长期存活的对象,占满时会触发Major GC。

Minor GC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代
所有GCGC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生 GC,避免响应超时。

img

21、为什么要分代?

答:引用了之前的分代收集理论

新生代: 每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成,复制的目的地是幸存者区。

老年代: 因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。

幸存者区: 大家可以参考这篇博客.

22、晋升老年代的几种情况

①当一个对象活过Eden区的垃圾回收,那么这些类将会与幸存者0/1区的对象一起转存到幸存者1/0区,若有对象年龄超过阈值,那么这些对象就晋升到老年区。
②若转存时发现幸存者区内存不够,则会将年龄较大的对象分配到老年区;
什么是年龄较大的对象?:如果在 Survivor空间中相同年龄所有对象所占大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
③如果遇到大对象,那么Java虚拟机会直接将大对象分配到老年区。

23、类加载的过程

答:类的生命周期:加载、验证、准备、解析、初始化、使用和卸载
在这里插入图片描述
1) 加载:
前面已经说过,类加载这个过程一个类只会加载一次,加载完成后生成的.class文件会保存在常量池中,后续类加载过程都会先通过全类名到常量池中定位到这个类的符号引用,并检查这个符号引用代表的类是否已经被加载。
类加载过程:
①通过这个类的全类名获取此类的外部的二进制字节码文件,即.class文件。
②以二进制字节流的形式读入该字节码文件到方法区中。
③在内存中生成一个代表这个类的java.long.class对象,作为方法区中这个类的各种数据访问接口。
在这里插入图片描述
特殊的,对于数组类,本身不通过类加载器创建,而是由java虚拟机直接创建的。
2)验证
验证阶段主要是为了确保被加载的类的正确性 ,即保证class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害到虚拟机自身的安全。
在这里插入图片描述
3)准备
为类的静态变量分配内存并将其初始化为默认值,并存储在方法区中。
如static int i = 3;会被初始化为0,具体的值会在初始化阶段附给变量。
特殊的,对于final和static同时修饰的变量,会在这个阶段就赋予原来的值,如final static int i = 3;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
在这里插入图片描述
4)解析
主要任务是将常量池内的符号引用替换为直接引用,我之前提到过,每个类都有一个自己的class常量池存放编译过程中的字面量和符号引用,当前步骤的主要做用是将class常量池转换为运行时常量池,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
在这里插入图片描述
5)初始化
初始化是为类的静态变量赋予正确的初始值。
在这里插入图片描述
具体类加载过程内容可以参考这篇博客:地址.

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值