Java虚拟机和垃圾回收机制初步学习

为什么Java要在虚拟机里运行?

Java语法非常复杂,抽象程度也很高,因此在硬件上运行这种复杂的程序并不现实,需要在Java程序运行之前对其进行转换。

转换思想:设计一个面向Java语言特性的虚拟机,并通过编译器将Java程序转换成该虚拟机所能识别的指令序列,也称为字节码。(之所以称为字节码是因为Java字节码指令的操作码(opcode)被固定为一个字节)。

Java虚拟机可以由硬件实现,但更为常见的是在各个现有平台软件实现,这么做的意义在于一旦一个程序被转换成Java字节码,那么它便可以在不同平台上的虚拟机实现运行。------即“一次编写,到处运行”。

虚拟机的另一个好处是它带来了一个托管环境(Managed Runtime)。这个托管环境能够代替我们处理一些代码中冗长而且容易出错的部分。其中有 自动内存管理与垃圾回收。

Java虚拟机是怎样运行Java字节码的?

从虚拟机视角来看:执行Java文件首先将编译而成的class文件加载到虚拟机中,存放于方法区,实际运行时,虚拟机会执行方法区中的代码。
JVM是运行在操作系统上的,与硬件没有直接的交互
在这里插入图片描述
在这里插入图片描述

堆可以是连续空间也可以不连续;堆的大小可以固定,也可以在运行时按需扩展。
在这里插入图片描述

程序计数器

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制:顺序执行、选择、循环、异常处理

在多线程的情况下,程序计数器用于记录当前线程执行的位置,以便线程切换时能够知道该线程上次运行到哪了(线程私有:每条线程都有一个独立的程序计数器)

程序计数器的生命周期 随着线程的创建而创建、随着线程的结束而死亡。 是唯一一个不会出现OutOfMemoryError的内存区域

Java虚拟机栈

生命周期:随着线程的创建而创建、线程的死亡而死亡

方法调用的数据需要通过栈来传递,每一次方法调用都会有一个对应的 栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

栈由一个个栈帧组成,先进先出,只支持出栈和入栈两种操作。

栈帧随着方法调用而创建、随着方法结束而销毁。无论是方法正常完成还是异常完成都算方法结束。

在这里插入图片描述

局部变量表

主要用于存放编译期各种数据类型、对象引用(reference类型、可能指向对象起始地址的引用指针,也可能是一个代表对象的句柄)
在这里插入图片描述

操作数栈

用于存放方法执行过程中产生的中间计算结果,以及计算过程中产生的临时变量,作为方法调用的中转站使用。

动态链接
在Java源文件被编译成字节码文件时,所有的变量和方法都作为符号引用(Symbilic Reference)保存在Class文件的常量池里。当一个方法调用其他方法时,所保存的符号引用通过动态链接转化为其在内存地址中的直接引用。	即动态链接就是将符号引用转化为调用方法的直接引用。

在这里插入图片描述

栈异常可能出现的错误

StackOverFlowError: 若栈的内存大小不允许动态扩展,当线程请求栈的深度超过当前Java虚拟机栈的最大深度时,就会抛出该异常;(如函数陷入无限循环)

OutOfMemoryError:若栈的内存大小允许动态扩展,当虚拟机在动态扩展栈时无法申请到足够的内存空间时,则抛出该异常。

本地方法栈

与虚拟机栈作用几乎一致,区别:虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈为虚拟机执行Native方法服务。(一个Native方法就是一个Java调用非Java代码的接口)

Java虚拟机所管理内存中最大的一块,线程共享,在虚拟机启动时创建。
只存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
JDK1.7已经开始默认开启逃逸分析,如果方法中的对象引用没有被返回或者未被外面使用(即未逃逸出去),那么对象可以直接在栈上分配内存。

Java堆,也叫GC堆(Garbage Collected Heap)是垃圾收集器管理的主要区域。可以细分如下
下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。
在这里插入图片描述
JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存

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

堆中最容易出现OutOfMemoryError错误,有以下几种表现形式:

  1. java.lang.OutOfMemoryError: GC Overhead Limit Exceeded :当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间
  2. java.lang.OutOfMemoryError: Java heap space: 当创建新的对象时,堆内存中的空间不足。

方法区

方法区属于JVM运行时数据区域的的一块逻辑 区域,是各个线程共享的内存区域。在不同虚拟机实现上,方法区的实现是不同的。

当虚拟机使用一个类时,需要读取并解析Class文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器后的代码缓存等数据

方法区和永久代以及元空间是什么关系?
方法区如同Java中的接口,永久代和元空间如同Java中的类;类实现了接口;
即永久代和元空间是方法区的两种实现方式。
在这里插入图片描述
在这里插入图片描述
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) ?

  1. 整个永久代有一个JVM本身设置的固定大小上限,无法调整,而元空间使用的是直接内存,受本机可用内存的影响,虽仍旧可能溢出,但几率会更小。

方法区常用参数:
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小。

-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。下面是一些常用参数:

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存

运行时常量池

常量池表:用于存放编译器生成的 各种字面量和符号引用。

(字面量是源代码中的固定值的表示法,即通过字面我们就能知道其值的含义。字面量包括整数、浮点数和字符串字面量,符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用。)

常量池表在类加载后存放到方法区的运行时常量池中。

字符串常量池

字符串常量池:主要是为了避免字符串的重复创建;提升性能和减少内存消耗

HotSpot虚拟机中的实现是:src/hotspot/share/classfile/stringTable.cpp ,stringTable本质上是一个HashSet,容量为 StringTableSize

StringTable中保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

在这里插入图片描述
在这里插入图片描述

对象的创建

步骤:
Step1: 类加载检查
虚拟机遇到一条new指令时,首先去检查该指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,则先执行相应的类加载过程;

Step2:分配内存
在类加载检查通过后,为新生对象分配内存:即将一块确定大小的内存从Java堆中划分出来。
分配方式有:指针碰撞空闲列表两种,选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  • 指针碰撞:
    在堆内存规整(即没有内存碎片)的情况下,将用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没有用过的内存方向将该指针移动对象内存大小位置即可。
    使用该分配方式的GC收集器:Serial、ParNew

  • 空闲列表:
    在堆内存不规整的情况下,虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块足够大的内存块来划分给对象实例,最后跟新列表记录。
    使用该分配方式的GC收集器:CMS

内存分配并发问题
在实际开发中,创建对象是很频繁的事情,虚拟机采用两种方式保证线程安全:

CAS+失败重试: 虚拟机采用CAS配上失败重试的方式保证跟新操作的原子性

TLAB: 为每一个线程预先在Eden区分配一块地址,JVM在给线程中的对象分配内存地址时,首先在TLAB分配,TLAB内存不足时,再采用上述的CAS进行内存分配。

Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
这一步操作保证了对象的实例字段在Java代码中可以不赋初值就可以直接使用。

Step4:设置对象头
初始化零值之后,对对象头进行设置:例如这个对象是哪个对象的实例、对象的哈希码、对象的GC分代年龄等信息。

Step5:执行init方法

对象的内存布局

对象在内存的布局可以分为:对象头、实例数据和对齐填充
对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,另一部分是类型指针(JVM通过这个指针确定这个对象是哪个对象的实例)
实例数据部分是对象真正存储的有效信息
对齐填充部分仅起占位符的作用

对象的访问定位

Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,主流访问方式:使用句柄、直接指针

句柄:

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

在这里插入图片描述
直接指针
在这里插入图片描述

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

垃圾回收机制

堆空间的基本结构

Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java自动内存管理最核心的功能是内存中对象的分配与回收。

Java堆是垃圾收集器管理的主要区域,被称为GC堆(Garbage Collected Heap)。

下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。

在这里插入图片描述

JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存 。

内存分配与回收原则

大多数情况下对象优先在Eden区分配,当Eden区内存不足时,虚拟机将发起一次Minor GC。(从新生代空间回收内存)
测试代码:

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

在这里插入图片描述

空间分配担保

空间分配担保是为了确保在Minor GC之前老年代本身还有能够容纳新生代所有对象的内存空间。

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(数组、字符);直接进入老年代是为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

长期存活的对象进入老年代

虚拟机给每个对象一个对象年龄(Age)计数器。

当对象在Eden区分配,经过第一次Minor GC后仍能存活,并被Survivor容纳的话,将被移动到Survivor空间中,将对象年龄设置为1;
对象在Survivor中每熬过一次Minor GC,年龄就加1,当年龄增加到一定程度(默认值是15),就会晋升到老年代中, 对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

死亡对象判断方法

引用计数法

给每个对象添加一个计数器,被引用时+1,引用失效时-1,当计数器为零时表示死亡。
缺陷:很难解决对象之间相互循环引用的问题

可达性分析算法

通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
在这里插入图片描述

哪些对象可以作为GC Roots?
虚拟机栈中引用的对象
本地方法栈中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁引用的对象

引用类总结

强引用

类似与必不可少的生活用品,垃圾回收器绝不会回收它

软引用

类似于可有可无的生活用品,内存足够就留着,内存不够就回收。可用来实现内存敏感的高速缓存。
同时软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。

弱引用

类似于可有可无的生活用品,与软引用区别在于具有更短的生命周期:当垃圾回收器线程扫描它所管辖的内存区域时,一旦发现弱引用的对象,直接回收。

虚引用

形同虚设。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值