菜鸟学习java虚拟机

JVM

1,基本问题

本笔记de博客来源

基本问题:

  • 介绍下 Java 内存区域(运行时数据区)
  • Java 对象的创建过程(五步,建议能默写出来并且要知道每一步虚拟机做了什么)
  • 对象的访问定位的两种方式(句柄和直接指针两种方式)

拓展问题:

  • String类和常量池
  • 8种基本类型的包装类和常量池

2,概述

虚拟机自动内存管理机制下,不易出现内存泄漏和溢出问题,这是因为java程序员把内存控制权交给了Java虚拟机,然而一旦出现内存溢出或泄漏,要求我们程序员必须清楚虚拟机是怎么使用内存的。

3,运行时数据区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域

md文件名格式

线程私有:程序计数器,虚拟机栈,本地方法栈

线程共享:堆,方法区,直接内存

1,程序计数器(私)

md文件名格式

2,虚拟机栈(私)

md文件名格式

3,本地方法栈(私)

md文件名格式

4,堆(公)

md文件名格式

md文件名格式

补充:

图所示的 eden区、s0区、s1区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden区->Survivor 区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

5,方法区(公)

md文件名格式

6,运行时常量池(公)

md文件名格式

7,直接内存

md文件名格式

4,Hotpot虚拟机对象

4.1,创建过程

md文件名格式

md文件名格式

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是”标记-清除”,还是”标记-整理”(也称作”标记-压缩”),值得注意的是,复制算法内存也是规整的

md文件名格式

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB(本地线程分配缓冲): (书上补充:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,即TLAB。哪个线程需要分配内存,就在哪个现成的TLAB上分配,只有TLAB用完并分配心得TLAB时才需要同步锁定。)为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

md文件名格式

4.2,对象的内存布局

md文件名格式

4.3,对象的访问定位

建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄②直接指针两种:

1,句柄: 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

md文件名格式

2,直接指针: 如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。

md文件名格式

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

5,重点补充

5.1,String类和常量池
  • String对象的两种创建方式
     String str1 = "abcd";
     String str2 = new String("abcd");
     System.out.println(str1==str2);//false

md文件名格式

记住:只要使用new方法,便需要创建新的对象

5.2,String 类型的常量池比较特殊,它的主要使用方法有两种:

md文件名格式

	      String s1 = new String("计算机");
	      String s2 = s1.intern();
	      String s3 = "计算机";
	      System.out.println(s2);//计算机
	      System.out.println(s1 == s2);//false,因为一个是堆内存中的String对象一个是常量池中的String对象,
	      System.out.println(s3 == s2);//true,因为两个都是常量池中的String对

  • 字符串的拼接
		  String str1 = "str";
		  String str2 = "ing";
		  
		  String str3 = "str" + "ing";//常量池中的对象
		  String str4 = str1 + str2; //在堆上创建的新的对象	  
		  String str5 = "string";//常量池中的对象
		  System.out.println(str3 == str4);//false
		  System.out.println(str3 == str5);//true
		  System.out.println(str4 == str5);//false
String s1 = new String("abc");//这句话创建了几个对象?

md文件名格式

  • 经典问题:String s1 = new String(“abc”);这句话创建了几个对象?
		String s1 = new String("abc");// 堆内存的地址值
		String s2 = "abc";
		System.out.println(s1 == s2);
		// 输出false,因为一个是堆内存,一个是常量池的内存,故两者是不同的。
		System.out.println(s1.equals(s2));// 输出true

//所以创建了两个对象

解释:先有字符串”abc”放入常量池,然后 new 了一份字符串”abc”放入Java堆(字符串常量”abc”在编译期就已经确定放入常量池,而 Java 堆上的”abc”是在运行期初始化阶段才确定),然后 Java 栈的 str1 指向Java堆上的”abc”。

5.3,8种基本类型的包装类和常量池

md文件名格式

		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);
    }

应用场景,

1,Integer i1=40;Java 在编译的时候会直接将代码封装成Integer i1=Integer.valueOf(40);,从而使用常量池中的对象。

2,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进行数值比较。

6,String.intern()

例子1:

md文件名格式

S是指向堆里面对象的地址,S1是指向常量池里面的地址

md文件名格式

jdk1.7,intern()之后,常量池存的是S3的地址(即该实例的引用),S4在创建的时候根据地址找到了该对象,所以地址相同

md文件名格式

具体补充如下:

md文件名格式

7,内存区域

  • 运行时数据区域变化

1,JDK1.8之前,运行区域如原始类型

2,JDK1.8及以后,方法区变为元空间,元空间使用的是直接内存

线程私有:

1,程序计数器

线程通过改变计数器的值来选取下一跳需要执行的指令,分支、循环、跳转、异常处理、县城恢复等功能都需要计数器来完成。

程序计数器是唯一一个不会出现OutOfMemoryError错误的内存区域

2,虚拟机栈

描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。Java内存可粗糙的分为堆内存和栈内存,其中栈就是现在说的虚拟机栈,或者说虚拟机栈中的局部变量表部分。

局部变量表中存放编译器可知的各种基本类型数据、对象引用

3,本地方法栈

虚拟机栈为虚拟机执行Java方法(字节码)服务

本地方法栈则为虚拟机使用到的Native方法服务

线程公有:

1,堆

存放对象实例、数组

2,方法区

存放已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码等数据。

Hotpot虚拟机方法区被叫做永久代

  • 运行时常量池

是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译器生成的各种字面量和符号引用)

JDK1.7之后版本的JVM将运行时常量池在方法区中移了出来,在Java堆中开辟了一块区域存放运行时常量池

3,直接内存

JDK1.4中新加入了NIO类,引入了一种基于通道与缓存区的I/O方式,直接使用Native函数分配堆外内存,避免了在Java堆和Native堆之间来回复制数据。

8,判断对象是否存活的两种算法

  • 引用计数算法

给对象添加一个引用计数器,引用时加一,引用失效时减一。当引用计数器为0时,对象就是不可能再被使用的。

  • 可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则这个对象是不可用的。

9,引用(强、软、弱、虚)

  • 强引用(正常new的对象)

程序代码中普遍出现的,类似Object obj=new Object(),只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

  • 软引用(SoftReference)

程序中用来描述一些还有用但非必需的对象,内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。

  • 弱引用(WeakReference)

程序中用来描述非必需对象,强度比软更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。垃圾收集器工作时,无论内存是否足够,都会回收只被弱引用关联的对象。

  • 虚引用(PhantomReference)

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

10,垃圾回收算法

10.1 标记-清除算法

如名字所说:分为“标记”和“清除”两个阶段

首先:标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路的不足而对其改进而得到的。

不足:1,效率问题:标记和清除两个过程的效率都不高。2,空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次辣鸡手机动作。

10.2 复制算法

为了解决效率问题

它将内存大小划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

缺点:将内存缩小为了原来的一半,代价未免有点高。

10.3 标记-整理算法

首先:标记出所有需要回收的对象,然后让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。

10.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,就可以根据各个年代的特点采用最适合的收集算法。

新生代:每次垃圾收集时都发现有大批对象死去,只有少量存活,那就采用复制算法。只要付出少量存活对象的复制成本就可以完成收集。

老年代:因为对象的存活率高、没有额外空间对它进行分配担保,就必须使用标记-清除或者标记-整理算法来进行回收。

11,GC收集器与优化

回顾GC的历史,主要有四种GC收集器

11.1,Serial

Serial收集器使用了标记-复制算法,但是GC进行时,程序会进入长时间的暂停,一般不太建议使用

11.2,Parallel

Parallel收集器使用了标记-复制算法,称之为吞吐量优先的收集器,因为其最大的优势在于并行使用多线程去完成垃圾清理工作,这样可以充分利用多核的特性,大幅降低gc时间。例如消息队列,需要保证有效利用cpu,吞吐量达,且可以忍受一定的停顿时间可以使用这种方式。

11.3,CMS

CMS收集器使用了标记-清除算法,重视服务器响应速度,希望停顿时间最短,可以选择。CMS在MinorGC收集时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。

11.4,G1

G1收集器使用标记-压缩清除算法,堆比较大时,减低full gc十分有必要,G1可以大大降低较大内存GC时产生的内存碎片。