JVM学习(2)-JVM内存模型与内存分配相关

点击查看更多JVM总结

一、Java虚拟机内存模型相关

1.JVM内存区域

JVM内存区域可分为线程私有和线程共享两个大部分,其中线程私有的包括:程序计数器、JVM栈、本地方法栈;而线程共享的包括:Java堆、方法区。

TIP:JDK 1.8中,去除了方法区,以直接内存中的元空间取代。
在这里插入图片描述

2.每一块内存区域的作用

1.程序计数器

程序计数器是一块很小的内存区域,其中存储的是当前线程所执行到的子节码行号;子节码指示器通过改变程序计数器的值来定位到子节码,以控制程序的跳转、循环、分支等操作。所以程序计数器是单个线程私有的,每个线程都有自己的一个程序计数器。程序计数器是唯一不会出现内存溢出的内存区域。

2.JVM栈

Java虚拟机栈是线程私有的,它的生命周期和线程的生命周期相同,它是描述Java方法运行的内存模型。
线程调用方法的过程和结束方法的过程,都对应着一个栈帧的入栈和出栈,每个栈帧中含有描述该方法的所有信息,例如含有局部变量表,用来存储方法的基本变量的值或者一个引用(指向Java堆中实例或者句柄);除了局部变量表,还含有操作数栈、动态链接、方法出口信息等。
在JVM栈区域,会出现StackOverflow异常和OutOfMemory异常,分别表示调用方法次数过多,栈帧的数量已经超过了栈深度、JVM栈已没有足够的空间去申请开辟线程。

3.本地方法栈

本地方法栈和JVM栈一样,不同的是本地方法栈执行的不是子节码,而是Native方法。

4.Java堆

Java堆又被称为GC堆,是线程共享的。大部分的数据实例都在这里分配,所以Java堆也是GC发生最频繁的地方。
Java堆可细分为新生代和老年代,其中新生代又可细分为1个Eden区和2个Survivor区。在Java堆中同时也含有字符串常量池,用以存储字符串。

5.方法区

方法区和Java堆一样是线程共享的,它存放的是类加载信息,包括类信息、常量、静态变量、即时编译器编译后的代码等数据,在HotSpot规范中,将方法区和Java堆描述为一个逻辑部分,但是为了区分它们,方法区又被称为"非堆"。

6.直接内存

并不属于Java虚拟机内存规范中定义,是IO速度最快的一块内存空间。

3. 方法区和永久代之间的关系

方法区是Java虚拟机规范中的一个规范,而永久代上hotSpot对方法区规范上的实现。

4. 方法区和元空间的关系

方法区在JDK 1.7开始被逐渐移出,在JDK 1.8中已删去方法区,而在直接内存中开辟了一块称为"元空间"的区域取代了方法区。

5. 为什么要删去方法区而开辟元空间

因为方法区在过去是内存溢出错误的常发地,由于方法区的空间在虚拟机启动之初即确定,所以会存在溢出;而元空间位于直接内存中,直接内存的空间只受本机内存所限制,不易发生内存溢出。

6. 运行时常量池和字符串常量池在不同JDK版本的位置变化

在这里插入图片描述

运行时常量池:
              - JDK 1.6:位于方法区中 
              - JDK 1.7:位于方法区中
              - JDK 1.8:位于元空间中
              
字符串常量池:
              - JDK 1.6:位于方法区运行时常量池中
              - JDK 1.7:位于Java堆中
              - JDK 1.8:位于Java堆中

7.Java堆空间的基本结构以及参数配置

在这里插入图片描述
Java堆空间可分为新生代和老年代,而新生代中又可分为Eden区和from Survivor区、to Survivor区。可由以下参数进行配置:

  • -Xms 20M:Java堆空间最多为20M
  • -Xmx 20M:Java堆空间最少为20M
  • -Xmn 10M:新生代分配10M
  • -XX:SurvivorRatio = 8: Eden区/1个Survivor区 = 8

例题:-Xms 10M,-Xmx 10M, -Xmn 5M,-XX:SurvivorRatio = 3:问Survivor区总空间为多少?

由-Xms 10M/ -Xmx 10M可知,Java堆空间被限制为10M;由-Xmn 5M得知,新生代空间被分配为5M;由-XX:SurvivorRatio = 3 可知,Eden/一个Survivor区 = 3,故可知Eden区为3M,一个Survivor区为1M,故Survivor区总空间为2M,

8.堆内存中对象的分配策略是什么

1.对象优先在新生代Eden区分配

2.大对象直接进入老年代(避免频繁的分配担保)

3.长期存活对象直接进入老年代

9.垃圾收集过程与对象年龄判定

1.第一次分配
  1. 将对象分配到Eden区,当对象是大对象(超过了-XX:PretenureSizeThresold时),直接分配到老年代
  2. 当Eden区空间不足时,先将Eden区中不需要回收的对象转移到To Survivor中(每次转移到To Survivor的过程都使对象年龄+1)
  3. 发生Minor GC,将整个Eden区清空
  4. 互换From Survivor与To Survivor,保证在下一次Minor GC前To Survivor是空的
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGZxnbwD-1577437696187)(http://note.youdao.com/yws/res/7309/00CB00B3A7D947E0B2C42E7DCA39436E)]
2.第二次及之后的分配
  1. 将对象分配到Eden区,当对象是大对象(超过了-XX:PretenureSizeThresold时),直接分配到老年代
  2. 当Eden区空间不足时,先将Eden区与From Survivor区中不需要回收的对象转移到To Survivor中(每次转移到To Survivor的过程都使对象年龄+1)
  3. 发生Minor GC,将整个Eden区和From Servivor区清空
  4. 互换From Survivor与To Survivor,保证在下一次Minor GC前To Survivor是空的
    在这里插入图片描述

补充:什么是分配担保

分配担保指的是在发生Minor GC前,Eden区中的需要转移到To Survivor区的对象过大,无法被From Survivor区容纳,需要把该对象以“分配担保”的形式“担保”给老年代。这个过程会十分耗时,并且由于大对象在Eden与老年代之间的复制,会造成效率的损失,这也是为什么要将大对象直接分配到老年代的原因。

10.动态年龄判断

其实to Survivor中的对象并不一定需到达15岁才会被转移到老年代如果Survivor空间中相同年龄的所有对象大小大于Survivor空间的一半,那么年龄大于等于该年龄的对象将会被转移到老年代,无需达到MaxTenuringThreshold规定的年龄。

11.如何判断一个对象是否已死

1.引用计数法:无法解决循环引用的问题
2.可达性分析(GC ROOT)

GC ROOT可以是JVM栈中引用的对象、方法区或元空间中的类变量、Java堆中的对象等。

12.如何判断一个类是无用的类

1.该类所有实例都已经被回收,Java堆中不存在该类的任何实例
2.加载该类的类加载器已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射获取该类的变量和方法

13.如何判断一个常量是无用的

没有在任何地方被引用。

14.对象的创建过程是什么

在这里插入图片描述

1.类加载检查,在方法区常量池中检查该类的符号引用是否存在,是否被加载过,若没加载过则进行类加载过程
2.为对象在堆中分配内存:
    1.指针碰撞分配 
    2.空闲列表分配
3.初始化零值
4.设置对象头:
    1.类的元数据
    2.对象的哈希码
    3.GC分代年龄
5.执行<init>方法

15.并发地对象创建过程中,如何保证分配内存不发生冲突

在这里插入图片描述

1.CAS + 失败重试,直到内存分配成功
2.每一个线程在Java堆中 预先分配一小块内存,被称为本地线程分配缓存(TLAB)。每个线程都在各自的TLAB中分配内存,如果使用完了当前的TLAB,在使用CAS + 失败重试去再分配出一块TLAB。

16.对象访问定位的两种方式是什么

在这里插入图片描述

对象访问定位即JVM栈中如何定位到位于Java堆中的实例数据,有两种方式
1.直接指针定位
即JVM栈中存储的是对象的指针,该指针直接指向了Java堆中的实例
2.句柄定位
句柄定位指的是JVM栈中存储的是对象的句柄地址,句柄地址中存储着指向对象实例的指针。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

17.new String(“Hello”)产生了多少个String对象

在这里插入图片描述
若字符串常量池中不存在Hello这个字符串常量,则会在字符串常量池中创建一个,并且在Java堆中也创建一个字符串实例,所以一共创建了两个对象;若字符串常量池中已经含有了Hello,那么只会在堆中创建一个字符串实例。

String a = "hello";
String b = "hello";
String c = new String("hello");

a == b // true,引用的都是位于字符串常量池的hello
a == c // false,new String引用的是Java堆中(非常量池)的hello

18.包装类常量池是什么

Java基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean;这 5 种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。

		Integer i1 = 33;
		Integer i2 = 33;
		System.out.println(i1 == i2);// 输出 true
		Integer i11 = 333;
		Integer i22 = 333;
		System.out.println(i11 == i22);// 输出 false
		Double i3 = 1.2;
		Double i4 = 1.2;
		System.out.println(i3 == i4);// 输出 false

Integer 缓存源代码:

/**
*此方法将始终缓存-128 到 127(包括端点)范围内的值,并可以缓存此范围之外的其他值。
*/
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
应用场景:
Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。
  Integer i1 = new Integer(40);这种情况下会创建新的对象。
  Integer i1 = 40;
  Integer i2 = new Integer(40);
  System.out.println(i1==i2);//输出 false
  
Integer 比较更丰富的一个例子:
  Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);
  
  System.out.println("i1=i2   " + (i1 == i2));
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));
  System.out.println("i1=i4   " + (i1 == i4));
  System.out.println("i4=i5   " + (i4 == i5));
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));   
  System.out.println("40=i5+i6   " + (40 == i5 + i6));     
结果:

i1=i2   true
i1=i2+i3   true
i1=i4   false
i4=i5   false
i4=i5+i6   true
40=i5+i6   true
解释:

语句 i4 == i5 + i6,因为+这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后 Integer 对象无法与数值进行直接比较,所以 i4 自动拆箱转为 int 值 40,最终这条语句转为 40 == 40 进行数值比较。
发布了309 篇原创文章 · 获赞 205 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/pbrlovejava/article/details/103359003