Java教程

JVM

本文主要是介绍JVM,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1 Java虚拟机体系结构

JDK、JRE、JVM

image-20220321145018873

JVM是什么?

image-20220321145114487

JVM内部组成

image-20220321145735881

img

2 类加载机制

什么是类的加载

image-20220321145850073

类加载过程

image-20220321150006702

类加载器的种类

img

image-20220321150112213

  • 启动类加载器 Bootstrap ClassLoader -- 加载jdk安装目录下lib目录中的核心类库
  • 扩展类加载器Extension ClassLoader -- 加载jdk安装目录下lib\ext目录
  • 应用程序加载器Application ClassLoader -- 加载ClassPath环境变量中指定路径中的类,可以理解为加载你写的代码
  • 自定义类加载器CustomClassLoader -- 加载用户自定义路径下的类包

双亲委派机制

20201217213314510

双亲委派模式优势

  • 沙箱安全机制:自己写的String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次

沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。

JDK1.0时期

在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型:

img

JDK1.1时期

JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。

因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限。如下图所示JDK1.1安全模型:

img

JDK1.2时期

在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型:

img

JDK1.6时期

当前最新的安全机制实现,则引入了域(Domain)的概念

虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型(jdk1.6)

img

3 Java虚拟机运行时内存区域

线程私有区域

image-20220321152441598

PC寄存器

程序计数器:Program Counter Register

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

与JVM不同,操作系统底层中指向的是下一条执行的指令地址

在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)

程序计数器的内存区域是唯一一个没有任何内存溢出OutOfMemoryError情况的区域。

栈内存,主管程序的运行,生命周期和线程同步;

线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题

栈:8大基本类型+对象引用+实例的方法

Native 本地方法栈

image-20220427134243600

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务

与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常

使用 native 修饰的指令都是调用本地方法接口

它的具体做法是Native Method Stack中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies

线程共有区域

image-20220321152509202

方法区

Method Area方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

static final,Class,常量池

运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

HotSpot 和 堆

堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

此内存区域的唯一目的就是存放对象实例,所有的对象实例以及数组都应当在堆上分配。

Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”(Garbage Collected Heap)。从回收内存的角度看,由于现代垃圾收集器大部分都是基于分代收集理论设计的,所以Java堆中经常会出现“新生代”、“老年代”、“永久代”、“Eden空间”、“From Survivor空间”、“To Survivor空间”等名词。

Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

Java堆既可以被实现成固定大小的,也可以是可扩展的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError (OOM) 异常。

栈、堆、方法区 的交互示例

数组的内存

image-20220327193425184

两个引用对象指向同一个数组

image-20220327194614132

一个对象的内存图

image-20220327202308049

两个对象使用同一个方法

image-20220327202710643

两个引用指向同一个对象的内存图

image-20220327203116428

4 JVM内存分配与回收策略

4.1 对象优先在Eden区分配

image-20220321152806036

Full GC 一般比 Minor GC 慢 10 倍以上

4.2 大对象直接进入老年代

image-20220321152944592

通过JVM参数设置 -XX:PretenureSizeThreshold 在 Serial和ParNew 收集器下有效,超过改参数大小直接进入老年代

设置原因:为了避免大对象分配内存时的复制

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

image-20220321153107983

在Eden区中坚持过 minor GC 进入survivor 区,age= 1;每坚持一次 minor GC , age += 1

默认 age = 15 进入老年代

image-20220321153127918

4.4 Minor GCEden区存活的对象 Survivor 区 放不下部分进入老年代

4.5 Eden与Survivor区默认8:1:1

image-20220321153643532

Eden 区满了后触发 Minor GC , (回收 99%),剩余的进入 survivor to 区(空的)

4.6 对象动态年龄判断

image-20220321154037785

  • 这个规则其实是希望那些可能长期存活的对象,尽早进入老年代
  • 对象动态年龄判断机制一般是在minor gc之后触发的

4.7 老年代空间分配担保机制

image-20220321154324297

image-20220321154359690

配置担保参数: -XX:-HandlePromotionFailure

如果回收完还是没有足够空间存储新的对象,就会发生OOM

5 OutOfMemoryError (OOM)

遇到OOM 解决方法 -Xms1024m -Xmx1024m -XX:+PrintGCDetails (扩大初始内存)

  • 一般情况下分配的总内存是电脑内存的四分之一,初始化的内存是六十四分之一(如下)
import com.sun.management.OperatingSystemMXBean;

import java.lang.management.ManagementFactory;


public class Test {

    public static void main(String[] args) {

        long maxMemory = Runtime.getRuntime().maxMemory();

        long totalMemory = Runtime.getRuntime().totalMemory();

        System.out.println("虚拟机试图使用的最大内存 maxMemory:" + maxMemory + "字节 \t" + 
                           (maxMemory/(double)1024/1024) + "M");
        
        System.out.println("虚拟机初始化使用的最大内存 totalMemory:" + totalMemory + "字节 \t" + 
                           (totalMemory/(double)1024/1024) + "M");



        OperatingSystemMXBean osmb = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
        long totalPhysicalMemorySize = osmb.getTotalPhysicalMemorySize();
        // long freePhysicalMemorySize = osmb.getFreePhysicalMemorySize();  // 电脑剩余内存

        System.out.println("电脑内存总大小:" + totalPhysicalMemorySize + "字节 \t" +
                (totalPhysicalMemorySize/(double)1024/1024) + "M");

        System.out.println("maxMemory/totalMemory : " + (double) maxMemory/totalMemory);
        System.out.println("totalPhysicalMemorySize/maxMemory : " + (double) totalPhysicalMemorySize/maxMemory);
        System.out.println("totalPhysicalMemorySize/totalMemory : " + (double) totalPhysicalMemorySize/totalMemory);
    }
}
# 正常情况下
虚拟机试图使用的最大内存 maxMemory:3797417984字节 	3621.5M
虚拟机初始化使用的最大内存 totalMemory:257425408字节 	245.5M
电脑内存总大小:17083187200字节 	16291.796875M
maxMemory/totalMemory : 14.75152749490835
totalPhysicalMemorySize/maxMemory : 4.498632300151871
totalPhysicalMemorySize/totalMemory : 66.36169806517312

# -Xms1024m -Xmx1024m -XX:+PrintGCDetails (VM options)
虚拟机试图使用的最大内存 maxMemory:1029177344字节 	981.5M
虚拟机初始化使用的最大内存 totalMemory:1029177344字节 	981.5M
电脑内存总大小:17083187200字节 	16291.796875M
maxMemory/totalMemory : 1.0
totalPhysicalMemorySize/maxMemory : 16.598876082526743
totalPhysicalMemorySize/totalMemory : 16.598876082526743
Heap
 PSYoungGen      total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3294K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K
  
# 305664k + 699392k =  981.5M  # 新生代 + 老年代 = 虚拟机初始最大内存
# Metaspace 元空间在逻辑上存在, 在物理上不存在

# -Xms4m -Xmx4m -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 512K->504K(1024K)] 512K->520K(3584K), 0.0010041 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1014K->504K(1024K)] 1030K->636K(3584K), 0.0011249 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1016K->512K(1024K)] 1148K->788K(3584K), 0.0007922 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
虚拟机试图使用的最大内存 maxMemory:3670016字节 	3.5M
虚拟机初始化使用的最大内存 totalMemory:3670016字节 	3.5M
电脑内存总大小:17083187200字节 	16291.796875M
maxMemory/totalMemory : 1.0
totalPhysicalMemorySize/maxMemory : 4654.799107142857
totalPhysicalMemorySize/totalMemory : 4654.799107142857
Heap
 PSYoungGen      total 1024K, used 699K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000)
  eden space 512K, 36% used [0x00000000ffe80000,0x00000000ffeaec30,0x00000000fff00000)
  from space 512K, 100% used [0x00000000fff00000,0x00000000fff80000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 2560K, used 276K [0x00000000ffc00000, 0x00000000ffe80000, 0x00000000ffe80000)
  object space 2560K, 10% used [0x00000000ffc00000,0x00000000ffc45060,0x00000000ffe80000)
 Metaspace       used 3295K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K
  

6 jprofiler 分析 OOM

分析 OOM 的软件 还有 MAT(基本不用了)

# VM options 
-Xms 	# 设置初始化内存分配大小 /164
-Xmx 	# 设置最大分配内存,默认1/4
 -XX:+PrintGCDetails 	# 打IGc垃圾回收信总
 -XX:+HeapDumpOnOutOfMemoryError  # OOM DUMP
# -Xms4m -Xmx4m -XX:+PrintGCDetails

# 在代码中加入:
        String str = "1234567890";
        while (true) {
            str += str;
        }

[GC (Allocation Failure) [PSYoungGen: 512K->488K(1024K)] 512K->528K(1536K), 0.0011578 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 998K->504K(1024K)] 1038K->616K(1536K), 0.0008813 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1016K->504K(1024K)] 1128K->744K(1536K), 0.0010449 secs] [Times: user=0.14 sys=0.00, real=0.00 secs] 
虚拟机试图使用的最大内存 maxMemory:1572864字节 	1.5M
虚拟机初始化使用的最大内存 totalMemory:1572864字节 	1.5M

[GC (Allocation Failure) [PSYoungGen: 937K->504K(1024K)] 1177K->896K(1536K), 0.0006825 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 504K->419K(1024K)] [ParOldGen: 392K->302K(512K)] 896K->721K(1536K), [Metaspace: 3291K->3291K(1056768K)], 0.0058845 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 832K->504K(1024K)] 1135K->982K(1536K), 0.0012532 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 504K->475K(1024K)] [ParOldGen: 478K->404K(512K)] 982K->879K(1536K), [Metaspace: 3297K->3297K(1056768K)], 0.0058477 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 817K->634K(1024K)] [ParOldGen: 404K->324K(512K)] 1222K->959K(1536K), [Metaspace: 3297K->3297K(1056768K)], 0.0048121 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 634K->622K(1024K)] [ParOldGen: 324K->319K(512K)] 959K->941K(1536K), [Metaspace: 3297K->3297K(1056768K)], 0.0046653 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
# GC GC GC GC FGC ...

Heap
 PSYoungGen      total 1024K, used 685K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000)
  eden space 512K, 74% used [0x00000000ffe80000,0x00000000ffedfb10,0x00000000fff00000)
  from space 512K, 59% used [0x00000000fff00000,0x00000000fff4ba18,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 512K, used 319K [0x00000000ffe00000, 0x00000000ffe80000, 0x00000000ffe80000)
  object space 512K, 62% used [0x00000000ffe00000,0x00000000ffe4fc10,0x00000000ffe80000)
 Metaspace       used 3333K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space		# !!!!!! OOM
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
	at java.lang.StringBuilder.append(StringBuilder.java:136)
	at jvm.Test.main(Test.java:35)

内存设置

# VM options 
-Xms 	# 设置初始化内存分配大小 /164
-Xmx 	# 设置最大分配内存,默认1/4
-XX:+PrintGCDetails 	# 打IGc垃圾回收信总
-XX:+HeapDumpOnOutOfMemoryError  # OOM DUMP

-Xms2m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

String str = "1234567890";
int count = 0;
while (true) {
    str += str;
    count++;
}

使用 jprofiler 分析OOM原因

  • Current Object Set --> Biggest Objects --> 分析内存占用
  • ThreadDump --> main -- > 定位行数

image-20220427184228262

image-20220427184148512

7 JVM 配置参数

-Xms:初始堆大小。只要启动,就占用的堆大小。
-Xmx:最大堆大小。java.lang.OutOfMemoryError:Java heap这个错误可以通过配置-Xms和-Xmx参数来设置。
-Xmn:堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了

-Xss:栈大小分配。栈是每个线程私有的区域,通常只有几百K大小,决定了函数调用的深度,而局部变量、参数都分配到栈上。
# 当出现大量局部变量,递归时,会发生栈空间OOM(java.lang.StackOverflowError)之类的错误。

-XX:NewSize:设置新生代大小的绝对值。
-XX:NewRatio:设置年轻代和年老代的比值。比如设置为3,则新生代:老年代=1:3,新生代占总heap的1/4。

# java.lang.OutOfMemoryError:PermGenspace这个OOM错误 需要合理调大PermSize和MaxPermSize大小。
#-XX:MaxPermSize:设置持久代大小, JDK1.8以后,这个参数被替换成了 MaxMetaspaceSize

# 一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,
# 设置得比初始值要大,对于8G物理内存的机器来说,一般将这两个值都设置为256M	默认 20M 左右
-XX:MaxMetaspaceSize	# 设置元空间最大大小
-XX:MetaspaceSize	# 设置元空间大小

-XX:ThreadStackSize : 设置JVM栈内存

-XX:SurvivorRatio:年轻代中Eden区与两个Survivor区的比值。注意,Survivor区有form和to两个。比如设置为8时,那么eden:form:to=8:1:1。

-XX:HeapDumpOnOutOfMemoryError:发生OOM时转储堆到文件,这是一个非常好的诊断方法。
-XX:HeapDumpPath:导出堆的转储文件路径。

-XX:OnOutOfMemoryError:OOM时,执行一个脚本,比如发送邮件报警,重启程序。后面跟着一个脚本的路径。

8 Java虚拟机如何判断对象是否存活

8.1 引用计数法

当对象引用计数器为0时,即无其他对象引用,判定为死亡,可被回收,但是存在循环引用问题,导致对象一直存活。

image-20220321154629597

image-20220321154721275

8.2 可达性分析法

image-20220321154935886

对象的引用分类:

  • 强引用,程序中普遍存在的的对象引用
  • 软引用,SoftReference实现,内存溢出前回收
  • 弱引用,WeakReference实现,下一次垃圾回收
  • 虚引用,PhantomReference实现,形同虚设

对象的起死回生

image-20220321155150194

9 四种JVM垃圾回收算法

9.1 标记-清除算法

  • 会产生大连不连续的内存碎片,当有大对象需要分配连续空间时,有可能会再次触发垃圾回收

image-20220321155259306

9.2 标记-整理算法

  • 方法同上,在回收后整理空间碎片,缺点是效率较低

image-20220321155555288

9.3 复制算法

  • 优点:效率高,没碎片,适合朝生夕死的内存区域
  • 缺点:内存利用率低,且不适合在对象存活率高的老年代使用

image-20220321155749217

9.4 分代回收算法

image-20220321160005596

10 JVM垃圾收集器

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

以下七种收集器,如果相互之间存在连线,便可以搭配使用

image-20220321160100819

11 Serial 收集器 (单线程收集器)

新生代采用复制算法,老年代采用标记-整理算法
优点:简单而高效

image-20220321160411642

Serial Old 收集器 (单线程收集器)

用途:

  • 在JDK1.5以及以前版本中与Parallel Scavenge收集器搭配使用
  • 作为CMS收集器的后备方案

image-20220321160625611

12 ParNew 收集器

Serial 收集器的多线程版本(回收策略、回收算法等完全一样)

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

特点:

  • 默认的收集线程数与CPU核数相同,可通过参数-XX:ParallelGCThreads 指定线程收集数(不建议修改)

  • 运行在Server模式下的虚拟机的首要选择

  • 除了Serial收集器外,只有它能与CMS收集器配合工作

image-20220321161139812

13 Parallel Scavenge 收集器

Parallel Scavenge 收集器类似于 ParNew 收集器,是 Server 模式下的默认收集器

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

特点:

  • 吞吐量(高效率的利用CPU) 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 提供很多参数进行调优
  • 在优化比较困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成

image-20220321161655868

Parallel Old 收集器

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

image-20220321162129779

14 CMS 收集器

CMS ( Conrrurent Mark Sweep )

以获取最短回收停顿时间为目标,HotSpot虚拟机第一款真正意义上的并发收集器

运作过程:

  • 初始标记:标记GCRoots能直接关联到的对象
  • 并发标记:用一个闭包结构(不包含所有可达对象)去记录可达对象;跟踪记录发生引用更新的地方
  • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
  • 并发清除:开启用户线程,同时GC线程开始对未标记的区域做清扫

优点:

  • 并发收集、低停顿

缺点:

  • 对CPU资源敏感
  • 无法处理浮动垃圾
  • 它使用的回收算法“标记-清除”算法,会有大量空间碎片产生
  • 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况(尤其是并发标记和并发清理阶段)

image-20220321162245773

15 G1 收集器

G1(Garbage First)

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

基本特性:

  1. G1将堆划分为多个大小相等的独立区域(Region)
  2. 一般Region大小等于堆大小除以2048
  3. G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。
  4. 默认年轻代对堆内存的占比是5%
  5. Region的区域功能可能会动态变化

image-20220321162952583

G1 对大对象的处理:

  • G1有专门分配大对象的Region叫Humongous区
  • 在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%

G1 垃圾收集过程:

image-20220321163351857

  • 初始标记(STW):暂停所有的其他线程,并记录下GC Roots直接能引用的对象
  • 并发标记:用一个闭包结构去记录可达对象;跟踪记录发生引用更新的地方
  • 最终标记(STW):修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
  • 筛选回收(STW):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

16 JMM

由于主存与 CPU 处理器的运算能力之间有数量级的差距,所以在传统计算机内存架构中会引入高速缓存来作为主存和处理器之间的缓冲,CPU 将常用的数据放在高速缓存中,运算结束后 CPU 再讲运算结果同步到主存中。

使用高速缓存解决了 CPU 和主存速率不匹配的问题,但同时又引入另外一个新问题:缓存一致性问题

在多CPU的系统中(或者单CPU多核的系统),每个CPU内核都有自己的高速缓存,它们共享同一主内存(Main Memory)。当多个CPU的运算任务都涉及同一块主内存区域时,CPU 会将数据读取到缓存中进行运算,这可能会导致各自的缓存数据不一致。因此需要每个 CPU 访问缓存时遵循一定的协议,在读写数据时根据协议进行操作,共同来维护缓存的一致性。这类协议有 MSI、MESI、MOSI、和 Dragon Protocol 等。

为了使处理器内部的运算单元能够最大化被充分利用,处理器会对输入代码进行乱序执行处理,这就是处理器优化。

除了处理器会对代码进行优化处理,很多现代编程语言的编译器也会做类似的优化,比如像 Java 的即时编译器(JIT)会做指令重排序。

处理器优化其实也是重排序的一种类型,这里总结一下,重排序可以分为三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

JMM的三个特征:

  • 可见性
    • 一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。
  • 原子性
    • 一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化)。
  • 有序性
    • 对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的。这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序。用一句话可以总结为:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

如果从更深层次看这三个问题,其实就是『缓存一致性』、『处理器优化』、『指令重排序』造成的。

image-20220427211716566

JMM规则:

  • 所有的变量都存储在主内存(Main Memory)中

  • 每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的拷贝副本

  • 线程对变量的所有操作都必须在本地内存中进行,而不能直接读写主内存

  • 不同的线程之间无法直接访问对方本地内存中的变量

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

  • 不允许一个线程将没有assign的数据从工作内存同步回主内存

  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作

  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

为了更好的控制主内存和本地内存的交互,Java 内存模型定义了八种操作来实现:

  • lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read:读取。作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load:载入。作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write:写入。作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

关键词synchronized与volatile总结

synchronized的特点

一个线程执行互斥代码过程如下:

  • 获得同步锁;
  • 清空工作内存;
  • 从主内存拷贝对象副本到工作内存;
  • 执行代码(计算或者输出等);
  • 刷新主内存数据;
  • 释放同步锁。

所以,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性

volatile是第二种Java多线程同步的手段,根据JLS的说法,一个变量可以被volatile修饰,在这种情况下内存模型确保所有线程可以看到一致的变量值

class Test {  
    static volatile int i = 0, j = 0;  
    static void one() {  
        i++;  
        j++;  
    }  
    static void two() {  
        System.out.println("i=" + i + " j=" + j);  
    }  
}  

加上volatile可以将共享变量i和j的改变直接响应到主内存中,这样保证了i和j的值可以保持一致,然而我们不能保证执行two方法的线程是在i和j执行到什么程度获取到的,所以volatile可以保证内存可见性,不能保证并发有序性(不具有原子性)

如果没有volatile,则代码执行过程如下:

  • 将变量i从主内存拷贝到工作内存;
  • 刷新主内存数据;
  • 改变i的值;
  • 将变量j从主内存拷贝到工作内存;
  • 刷新主内存数据;
  • 改变j的值;

17 一些关于 JVM 的问题

请你谈谈你对JVM的理解?

JVM的内存模型和分区详细到每个区放什么?

堆里面的分区有哪些?Eden,form,to,老年区,说说他们的特点

引用计数器,怎么用的?

轻GC和重GC分别在什么时候发生?

什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?

JVM的常用调优参数有哪些?

-Xms:初始堆大小。只要启动,就占用的堆大小。
-Xmx:最大堆大小。java.lang.OutOfMemoryError:Java heap这个错误可以通过配置-Xms和-Xmx参数来设置。
-Xmn:堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了

-Xss:栈大小分配。栈是每个线程私有的区域,通常只有几百K大小,决定了函数调用的深度,而局部变量、参数都分配到栈上。

-XX:NewSize:设置新生代大小的绝对值。
-XX:NewRatio:设置年轻代和年老代的比值。比如设置为3,则新生代:老年代=1:3,新生代占总heap的1/4。

-XX:SurvivorRatio:年轻代中Eden区与两个Survivor区的比值。注意,Survivor区有form和to两个。比如设置为8时,那么eden:form:to=8:1:1。

# java.lang.OutOfMemoryError:PermGenspace这个OOM错误 需要合理调大PermSize和MaxPermSize大小。
#-XX:MaxPermSize:设置持久代大小, JDK1.8以后,这个参数被替换成了 MaxMetaspaceSize

# 一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,
# 设置得比初始值要大,对于8G物理内存的机器来说,一般将这两个值都设置为256M	默认 20M 左右
-XX:MaxMetaspaceSize	# 设置元空间最大大小
-XX:MetaspaceSize	# 设置元空间大小

-XX:ThreadStackSize : 设置JVM栈内存



-XX:HeapDumpOnOutOfMemoryError:发生OOM时转储堆到文件,这是一个非常好的诊断方法。
-XX:HeapDumpPath:导出堆的转储文件路径。

-XX:OnOutOfMemoryError:OOM时,执行一个脚本,比如发送邮件报警,重启程序。后面跟着一个脚本的路径。

内存快照如何抓取,怎么分析Dump文件?知道吗?

抓取内存快照

-XX:HeapDumpOnOutOfMemoryError:发生OOM时转储堆到文件,这是一个非常好的诊断方法。
-XX:HeapDumpPath:导出堆的转储文件路径。

使用 jprofiler 分析OOM原因

  • Current Object Set --> Biggest Objects --> 分析内存占用
  • ThreadDump --> main -- > 定位行数

谈谈JVM中,类加载器你的认识?

种类

  • 启动类加载器/跟加载器
  • 扩展类加载器
  • 应用类加载器/系统类加载器
  • 用户自定义加载器

双亲委派机制

沙箱安全机制

java8虚拟机和之前的变化更新?

jdk1.8 之前 jdk1.8
image-20220427215617892 image-20220427215646067

堆和方法区连在了一起,但这并不能说堆和方法区是一起的,它们在逻辑上依旧是分开的。但在物理上来说,它们又是连续的一块内存。也就是说,方法区和前面讲到的Eden和老年代是连续的

image-20220427212939695

image-20220427214418582

在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)。

方法区和永久代的关系很像Java中接口和类的关系,永久代是HotSpot虚拟机对虚拟机规范中方法区的一种实现方式。

# JDK1.8之前调节方法区大小:
-XX:PermSize=N //方法区(永久代)初始大小
-XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError 

# JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是内存。参数设置:
-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小

永久代和元空间内存使用上的差异:

  • 永久代有一个JVM本身设置固定大小上限,无法进行调整
  • 元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。

永久代为什么被替换了

表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误

当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制

更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。同时也不用担心运行性能问题了,在覆盖到的测试中, 程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。

这篇关于JVM的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!