JVM学习(一)

jvm学习(一)

运行时数据区

1、由并发中所说的,java虚拟机把它管理的内存划分若干个不同的数据区域。

JDK1.8之前:

  • 线程共享:堆、方法区

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

    img

JDK1.8之后:

将方法区去掉,换成直接内存中的元空间

img

线程私有的数据区都是随着线程的创建而创建,随着线程的结束而死亡

2、虚拟机栈:由一个个栈帧组成,一个栈帧包含局部变量表、操作数栈、动态链接、方法出口信息。其中局部变量表主要存放了编译器可知的各种数据类型(boolean\byte\char\short\int\float\long\double)、对象引用

java虚拟机栈会出现StrackOverFlowError和OutOfMemoryError两种异常

  • StrackOverFlowError:虚拟机内存大小不允许动态扩展,当线程请求栈的深度超过当前java虚拟机栈的最大深度的时候,会抛出StrackOverFlowError异常
  • OutOfMemoryError:虚拟机栈的内存大小运行动态扩张,仅当线程请求栈的内存用完无法动态扩展时,此时抛出异常

方法被执行的时候,在虚拟机栈会创建一个栈帧,用于保存该方法的局部变量表、操作数栈、动态链接以及出口信息,当方法执行完毕之后,就对应了一个出栈的过程。java方法有两种返回方法,对应出栈的过程,return以及抛出异常

对象引用变量指向堆区,访问方式由使用句柄和直接指针

  • 句柄:堆中划分一块内存来作为句柄,对象应用指向这个句柄的堆内存位置,句柄中包含对象实例数据以及类型数据所指向的内存地址

    对象的访问定位-使用句柄

  • 直接指针:引用对象存储的是对象实例在堆区中的地址

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

3、本地方法栈:与虚拟栈相同,只是本地方法栈为本地方法服务(使用到native方法服务)

4、堆区:存放对象的实例,几乎所有的对象实例以及数组都在这里分配内存。堆区可以分为新生代和老年代,新生代存放新生的对象或者年龄不大的对象,老年代存放老年对象。

新生代:分为eden区、s0区、s1区。s0区和S1区也被成为from和to区域,他们是两块大小相等,并且可以互相转换的空间。

大多数情况,对象首先分配到eden区域,在新生代回收后,如果对象还存活,则进入s0区域或s1区域,之后每次经过一次回收,年龄就+1,对象年龄达到一定的值(默认15)后进入老年代,对象进入老年代的年龄阈值可以通过XX:maxTenuringThreshold来设置

新生代与老年代的比例值为1:2.老年代占用堆内存的2/3,新生代又分为三部分,分别为Eden、两个Survivor区域(from、to),新生代的细分占比又为Edum:from:to=8:1:1.JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

举例:A a=new A();首先把对象存放到堆内存当中,发现这个A只用了一次,首先会进入新生代里边去,存放到edn区域里面,因为Java堆自动化管理,垃圾收集机制,发现这个a只用了一次,如果a又被一段代码引用,垃圾回收机制有算法,每引用一次记录一次,就会马上进入到s0或者是s1区域,一旦a这个对象引用次数非常多了,就会存放到老年代里去。

img

5、方法区:存储已被虚拟机加载过得类对象信息、常量、静态变量以及即时编译器编译后的代码等数据。

  • jdk1.8之前,方法区还未被移出时,可以通过-XX:PermSize=N设置方法区初始化大小以及通过-XX:MaxPermSize=N设置方法区的最大容量,超过这个值将会抛出OutOfMemoryError异常
  • JDK1.8之后,将方法区移除,取而代之的是元空间,元空间直接使用内存。通过参数-XX:MetaspaceSize=N设置Metaspace的初始大小,通过-XX:MaxMetaspaceSize=N设置Metaspace的最大容量。如果不指定大小,默认为unlimited,只受系统内存大小的限制。随着类的创建,会耗尽所有可用的系统内存。,并且永远不会得到 java.lang.OutOfMemoryError

6、运行时常量池:运行时产生的新的常量,这些常量就被放大运行时常量池中。JDK1.8之前,运行时常量池存储在方法区内,JDK1.8后把方法去换成元空间后,运行时常量池被分配到了堆区

img

对象创建过程

  1. 创建对象的过程为:类加载检查、分配内存、初始化零值、设置对象头、执行init方法
  • 类加载检查:虚拟机在遇到一个new指令的时候,会首先检查这个指令的参数是否能在常量池中定位这个类的符号引用。检查这个符号引用代表的类是否被加载过、解析过和初始化过。没有则先执行类的加载过程。

  • 分配内存:类加载检查过后,虚拟机为新生的对象分配内存。分配堆内存的方式分为指针碰撞以及空闲列表。选择哪种方式由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

    指针碰撞:把指针往空闲空间那边挪动一段与对象大小相等的距离

    空闲列表:虚拟机维护一个记录空闲可用区地址的列表,在分配内存时,分配一个足够大的内存空间,并跟新空闲列表

内存分配的两种方式

虚拟机采用两种方法来保证内存分配时的线程安全问题

  • CAS+失败重试:保证跟新操作的原子性
  • TLAB:为每一个线程预先在Eden区域分配一个内存,JVM在分配内存时,首先采用TLAB分配,当对象大于TLAB中的剩余内存或者TLAB的内存用尽的时候,再采用CAS+失败重试的方式分配内存
  • **初始化零值:**内存分配完成后,虚拟机需要将分配的内存空间都初始化为零值(不包括对象头),保证了实例字段可以不赋予初始化值就可以直接使用
  • 设置对象头:初始化零值之后,要对对象进行必要的设置,例如这个对象是哪个类的实例、如何能够找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息都存放在对象头中
  • 执行init方法:把对象按照程序员的意愿进行初始化。这样一个真正可用的对象就产生了。

2、对象的内存布局:分为三块区域:对象头、实例数据和对齐填充

对象头包括两部分信息,第一部分用于存储对象自身运行时数据(哈希码、GC分代年龄等),另一部分是类型指针,指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

  • 实例对象数据:对象真正存储的有效信息,也是程序中所定义的各种类型字段的内容
  • 对齐填充:不是必然存在,仅仅起占位作用。因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

8中基本数据类型的包装类

基本数据类型的包装类都实现了常量池技术,即Byte、Short、Integer、Long、Character、Boolean。这几种包装类默认创建了数值在[-128,127]的相应缓存数据,如果超过这个数据就会创建新的对象。两种浮点数的包装类没有实现这中常量池技术

        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 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       //原因:+不适合Integer,首先会进行i5和i6的拆箱,即i4==40,Integer对象无法与数值直接比较,所以又将i4拆箱进行比较
40=i5+i6   true

垃圾回收器

java自动内存管理最核心的就是堆内存的对象分配与回收,常见分配策略为

  • 对象优先在eden区域分配
  • 大对象直接进入老年代(字符串、数组):为了避免大对象分配内存时由于分配担保机制带来的复制而降低效率
  • 长期存活的对象进入老年代

例子:

public class GCTest {

	public static void main(String[] args) {
		byte[] allocation1, allocation2;
		allocation1 = new byte[30900*1024];
		//allocation2 = new byte[900*1024];
	}
}

img

输出:

img

可以看出,优先分配eden区域,此时再分配eden区域的内存

allocation2 = new byte[900*1024];

img

此时,因为eden区域用满,虚拟机发起一次minor GC。GC过后allocation进入Survivor空间,而此时Survivor空间也无法存放,所以通过分配担保机制把新生代的内容提前移到老年代中去。老年代上的空间足够存放 allocation1,所以不会出现 Full GC。

1、引用

引用分为强引用、软引用、弱应用、虚引用

  • 强引用:大部分引用都是强引用。对象具有强引用,代表对象是必不可少的。即使内存空间不足,垃圾回收器也不会回收它,宁愿抛出OutOfMemoryError错误
  • 软引用:类似于可有可无的生活用品。只有在内存空间不足的情况下,垃圾回收器才会回收它。软引用可以和一个引用队列联合使用。软引用对象被回收了,垃圾回收器就会将软引用对象加入到与之关联的引用队列中
  • 弱应用:也相当于可有可无的生活用品,与软引用不同的是,不管内存是否不足,只要垃圾回收器发现对象只具有弱引用,就会回收它的内存。弱引用也可以和一个引用队列联合起来,弱引用对象被回收之后,jvm会将对象加入到与之关联的引用对象队列中
  • 虚引用:就是形同虚设,它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。主要用于跟踪对象被垃圾回收的活动。**虚引用必须与垃圾回收器联合起来,回收之前把这个虚引用加入到与之关联的引用队列中。

在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生

2、判断对象是否死亡

img

  • 引用计数法:给对象添加引用计数器,每当有一个地方引用了对象,计数器的值+1.引用失效则-1.当引用计数器的值为0时,说明对象不可能被引用
    • 缺点:很难解决对象之间相互引用的问题
//obja和objb互相相互引用对方,之后没有引用。所以引用计数器不为0,垃圾回收器无法回收
public class ReferenceCountingGc {
    Object instance = null;
	public static void main(String[] args) {
		ReferenceCountingGc objA = new ReferenceCountingGc();
		ReferenceCountingGc objB = new ReferenceCountingGc();
		objA.instance = objB;
		objB.instance = objA;
		objA = null;
		objB = null;

	}
  • 可达性分析算法

    过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。如果对象在进行可行性分析后发现没有与GC Roots相连的引用链,也不会理解死亡。它会暂时被标记上并且进行一次筛选,筛选的条件是是否与必要执行finalize()方法。如果被判定有必要执行finaliza()方法,就会进入F-Queue队列中,并有一个虚拟机自动建立的、低优先级的线程去执行它。稍后GC将对F-Queue中的对象进行第二次小规模标记。如果这时还是没有新的关联出现,那基本上就真的被回收了。

    可达性分析算法

判断常量是否是废弃常量

​ 运行时常量池主要回收废弃的常量。当没有任何对象引用这个常量的时候,就说明这个常量是废弃常量

判断一个类是否为无用类

  • 该类的所有实例都被回收,也就是java堆中没有任何一个该类的实例
  • 加载该类的类加载器已经被回收
  • 该类所对应的Class对象没有在任何地方被引用,也无法通过反射访问该类的方法

3、垃圾回收算法

垃圾回收算法分为:标记-清除法、复制法、标记-整理法、分代收集算法

  • 标记-清除法:标记需要回收的对象,标记完成后统一回收所有被标记的对象

    • 存在的问题:效率问题、空间问题(产生不连续的碎片)

    img

  • 复制算法:将内存分为大小相等的两块,每次使用其中一块。用完之后将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样每次内存回收都是对内存的一半进行回收。解决了标记-清除法的效率问题。

    公众号

  • 标记-整理法

    将需要回收的对象进行标记,之后让存活的对象向一端移动,然后直接清理掉端边界以外的内存

    标记-整理算法

  • 分代收集法:当前的垃圾收集算法都是采用分代回收算法。根据新生代和老年代的特点选择合适的垃圾回收算法。

    比如新生代,每次收集都有大量的对象死亡,适合复制算法,只需要付出少量对象的复制成本就可以完成垃圾回收。而老年代对象的存活几率高,而且没有额外的空间对它进行担保,所以采用标记-清除算法或者标记-整理算法

4、垃圾回收器

垃圾回收器有Serial收集器、ParNew收集器、Parallel Scavenge收集器、CMS收集器、GI收集器

  • Serial收集器:单线程收集器,它会使用一条垃圾收集线程去完成垃圾收集工作,进行垃圾收集工作的时候,必须暂停所有的工作线程,直到它收集结束。

    • 新生代采用复制算法,老年代采用标记-整理算法。简单高效,没有线程交互的开销,但是运行过程会停止所有的工作线程,会带来不良的用户体验
  • PraNew收集器:Serial的多线程版本,使用多线程进行垃圾回收。其余行为与Serial回收器相同,工作时也会停止所有的工作线程

    • 新生代采用复制算法,老年代采用标记-整理算法
  • Parallel Scavenge收集器

    使用复制算法的多线程收集器,关注点是吞吐率(CPU中用于运行用户代码的时间和CPU总消耗时间的比值)。

    新生代采用复制算法,老年代采用标记-整理法

    Parallel Scavenge 收集器

  • Serial Old收集器:Serial收集器的老年代版本,同样是单线程收集器,它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

  • Parallel收集器:Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

  • CMS收集器:以获取最短回收停顿时间为目标的收集器,注重用户的体验。是HotSpoot虚拟机第一款真正意义上的并发收集器,它第一次让垃圾收集线程和用户线程同时工作

    • CMS采用一种标记-清除算法实现,运作过程如下

      • 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
      • 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
      • 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
      • 并发清除: 开启用户线程,同时 GC 线程开始对为标记的区域做清扫。
    • 优点:并发收集、低停顿

    • 缺点:对CPU资源敏感。无法处理浮动垃圾。使用标记-清除算法,会产生大量空间碎片

    CMS收集器关注点跟多的是用户线程的停顿时间

  • GI收集器

    G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.

    • 特点:

      • 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
      • 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
      • 空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
      • 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
    • GI的运行步骤分为:

      • 初始标记
      • 并发标记
      • 最终标记
      • 筛选标记
    • GI收集器在后台维护了一个优先列表,每次根据运行的收集时间,优先选择回收价值最大的Region。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 GI 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值