JVM

Java从编码到执行的过程

类加载过程

loading

类加载器 双亲委派

引导类加载器(BootStrap)

拓展类加载器(Extension)

系统类加载器 ( Application)

自定义类加载器

Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式

先从自定义的ClassLoader看是否有,没有App中看, 在Extension中看,再Bootstrap中看,都没有,Bootstrap看自己能不能加载,不能就交给Extension加载,也不能就交给App加载,也没有就交给自定义加载器加载。 最后还是没有 抛出异常 ClassNotFound。找到就返回

==这就叫双亲委派!!==

为什么类加载器要使用双亲委派呢?

处于安全考虑, 防止用户自己写一些类覆盖java底层,然后让自定义类加载器加载,并打包成jar包给客户, 这样用户使用就会面临风险,可能覆盖的那个类中有不良行为,采用双亲委派,当自定义类加载器比如想要加载java.lang.String的时候,一定会向上找有没有加载过,都没有加载过,自己才可以加载。

  1. 对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性

两个实例各自对应的同名的类的加载器必须是同一个。比如两个相同名字的类,一个是用系统加载器加载的,一个扩展类加载器加载的,两个类生成的对象将被jvm认定为不同类型的对象。

所以,为了系统类的安全,类似“ java.lang.Object”这种核心类,jvm需要保证他们生成的对象都会被认定为同一种类型。即“通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的”。

为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载

破坏双亲委派

只需要重写ClassLoader类的loadClass()方法

类加载器的一些思考和用处

和反射有啥区别? 本质差不多

利用类加载器加载类,动态代理,热部署(替换内存中的类)

找到一个类

Class aaa=类.class.getClassLoader().loadClass("你想要加载的全类名");

loadClass里面是有一把synchronized锁的

自定义类加载器

加密 解密的一个骚操作方法

==异或==

a 异或一个固定钥匙 进行加密, 再次异或这个固定钥匙 就实现了解密

编译

三种模式

-Xmixed 混合模式 默认

-Xint 解释模式 启动快 执行稍慢

-Xcomp 编译模式 启动慢 执行快

混合模式采用解释器+热点代码编译到本地执行

热点代码检测:

  • 基于采样的热点探测
  • 基于计数器的热点探测(多次被调用的方法)
  • 基于踪迹的热点探测

设置热点代码(JVM使用编译执行的条件)

hotspot会在代码被执行10,000次后将它便以为本地代码

-XX:CompileThreshold=10000

Java中数字可以这么写 提高可读性

其实就是100000的意思,方便阅读

Integer i=100_000;

Linking

1.Verification 验证文件是否符合JVM规定

2.Preparation 静态成员变量赋==默认值==

3.resolution 将类、方法、属性等符号引用解析为直接引用,常量池的各种符号引用解析为指针、偏移量等内存地址的直接引用

Initializing

调用类初始化代码,给静态成员变量赋==初始值==

JMM

Java内存模型

硬件层的并发优化基础知识

多核心CPU 从外部主存获取数据加载到内部L2,L1上, 会存在两个CPU同一个数据不一致问题,

伪共享

cpu读取一个数据时,不是只读这一个的,是以cache line为一个基本单位。通俗的讲就是一次性读出这个数据前后总共64字节的数据块加载到内存中的,也就是一个缓存行的数据。

同一缓存行cache line的不同数据分别被不同cpu使用并修改值, 导致cpu通知其他cpu整个缓存行数据发生变化,而实际上这些cpu都是只用了这个缓存行各自没有交集的数据。 这就是伪共享

缓存行对齐

缓存行为8*8=64字节

一个long为8字节

可以定义7个long类型 来抢占未知

public long p1,p2,p3,p4,p5,p6,p7; // cache line padding

很多开源软件已经开始这么干了 ,比如disrupor

保证cursor一定是单独在一个缓存行,不会和其他真实数据一起, 避免cpu之间产生缓存伪共享

public long p1,p2,p3,p4,p5,p6,p7; // cache line padding
private volatile long cursor=INITIAL_CURSOR_VALUE; 
public long p8,p9,p10,p11,p12,p13,p14; // cache line padding

==使用缓存行对齐的方式可以提高效率, 但是牺牲了一点点的内存空间,具体就看怎么使用了。==

乱序问题

指令之间没有依赖关系的情况,cpu可以做乱序重排,只要保证最终一致性

乱序问题的证明DisOrder(美团的人写的证明demo)

写操作也可以进行合并 合并写技术

WCBuffer WriteCombineBuffer

基于cpu和L1缓存之间,更高级别的缓存,只有4字节

案例是讲循环给一个数组6个位置的赋值, 改为两个循环分别赋值各自三个位置。结果两个的反而更加高效

6个位置一次只能4个, 剩下两个放入后就需要在等后面向上面填充两个 凑满4个才能写

内存屏障

CPU级别内存屏障

Intel的

sfence 写操作屏障

lfence 读操作屏障

mfence 读写操作屏障

Java并发内存模型

查看本机JVM配置(Windows版)

java -XX:+PrintCommandLineFlags -version

对象的创建过程

首先,类加载器通过双亲委派机制依次从Application->Extension->Bootstrap加载Class字节码文件到内存中,然后进行Linking的三步操作 Verification验证字节码文件符合JVM规范;Preparation 静态成员变量赋默认值;resolution将类、方法解析为直接引用。然后Initializing初始化操作,给静态成员变量附上初始值。

然后new对象, 也是三步 new 申请空间 此时类中实例变量的值都为默认值;invokespecial调用构造方法,成员变量按顺序赋初始值,执行构造方法语句;a_store1建立地址关联将变量和引用地址关联。这三步会存在指令重排问题。 所以单例模式DCL设计的时候需要volatile关键字来禁止指令重排。

==DCL单例模式(Double Check Lock)要加volatile(禁止指令重排)==

==有继承时的过程==

==遇到extends,就要知道,先初始化父类数据,然后初始化子类数据==

对象在内存中的存储布局

分为两种 普通对象和数组对象

查看本机JVM配置(Windows版)

java -XX:+PrintCommandLineFlags -version

普通对象

在 hotspot 虚拟机中,对象在内存中布局可以被分为三部分:对象头/实例数据/补位数据

对象头:markword 8字节

类型指针 ClassPointer 4字节

​ -XX:+UseCompressedClassPointers class指针压缩 不压缩就是8字节 默认压缩

实例数据 instance data {对象中的成员 int 等}

  • 引用类型:-XX:+UseCompressedOops 为4字节,不开启为8字节,默认开启
  • Oops: Ordinary Object Pointers Reference普通对象指针

对齐 padding (字节不能被8整除 就自动补齐)

==1字节byte=8位bit==

markword 包含3大信息 锁信息 hashcode GC

类型占用字节范围包装类
byte1字节-2^7~2^7-1Byte
short2字节-2^15~2^15-1Short
int4字节-2^31~ 2^31-1Integer
long8字节-2^63~2^63-1Long
float4字节 Float
double8字节 Double
boolean理论1/8字节,实际1字节 Boolean
char2字节 Character
string(非基本类型)4字节 不压缩就是8字节 String
Object4字节 不压缩就是8字节

数组对象

对象头:markword 8字节

类型指针 ClassPointer 4字节

数组长度 4字节

数组数据

对齐 padding (字节不能被8整除 就自动补齐)

markword里面存放什么

当Java处在偏向锁、重量级锁状态时,hashcode值存储在哪里?

当一个对象已经计算过identity hash code,他就无法进入偏向锁状态

当一个对象正处于偏向锁状态,并且要计算其identity hash code的话,则偏向锁会被撤销,并且膨胀为重量锁

重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的markword,其中可以存储identity hash code的值,简单说就是重量锁可以存下identity hash code

Run-time Data Areas

五大分区: PC、Java栈(虚拟机栈)、本地方法栈、堆、方法区

Java栈(虚拟机栈) 就是我们平时说的栈,栈描述的是Java方法执行的内存模型

1.PC 程序计数器【Program Counter Register】

程序计数器,程序计数器是一块很小的内存空间, 它可以看作是当前线程所执行的字节码的行号指示器 , 字节码解释器工作时就是通过改变该计数器的值来选取下一条需要执行的字节码指令。

==每一个jvm线程都有自己的pc,它是线程私有的。==

这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域

2.虚拟机栈【JVM Stacks】

就是我们平时说的栈,栈描述的是Java方法执行的内存模型。 每个方法被执行的时候,JVM 都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

==它是线程私有的==

==每一个jvm线程都有自己私有的jvm stack。和线程一同创建出来==

每一个栈有很多frames栈帧

2.1栈帧【Stack Frame】

==每个方法对应一个栈帧==

栈帧,存放一下内容

1.Local Variable Table局部变量表

2.Operand Stack 操作数栈

3.Dynamic Linking

​ 简单理解就是 a()调用b(),a需要去找b方法在哪,这个就是dynamic linking的过程

4.return address

​ a方法调用b方法, b方法直接结束后回调a方法,怎么回去,通过return address

==StackOverFlowError 和 OutOfMemoryError 的区别==

StackOverFlowError 表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。
而 OutOfMemoryError 是指当线程申请栈时发现栈已经满了,而且内存也全都用光了

3.本地方法栈【Native Method Stacks】

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

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间。

4.堆【Heap】

Java 堆是虚拟机所管理的内存中最大的一块 ,被所有jvm线程所共享。

此内存区域的唯一目的就是存放对象实例,Java 里所有的对象实例都在这里分配内存。

The heap is the run-time data area from which memory for all class instances and arrays is allocated

5.方法区【Method Area】--非堆

==Method Area 实际上是一个逻辑概率, 为堆的一个逻辑部分 ,而永久代PermSpace和元空间MetaSpace是具体实现==

方法区存放 class,方法编译完的信息,jdk1.7字符串常量存放在这里,1.8之后放在堆中。

jvm 有一个方法区,被所有jvm线程共享

  • 永久代 Perm Space(<1.8) ==巨大问题就是FGC不清理==

字符串常量位于PermSpace,FGC不会清理

   **物理上在堆内存** 
  • 元空间Meta Space (>=1.8)

字符串常量在堆中,会触发FGC清理

   用来替换掉永久代(指HotSpot VM) 

物理上在直接内存

5.1 运行时常量池【Runtime Constant Pool】

运行时常量池是方法区的一部分。

Run-time Constant Pool is a per-class or per-interface run-time representation of the constant_pool table in a clas file

常量池共有三类

  • 运行时常量池(Runtime Constant Pool)
  • 常量池(Constant Pool):也是常说的class文件常量池(class constant pool)
  • 字符串常量池(String Constant Pool) 通过public static final来声明一个常量的就存放在这个里面

直接内存【Direct Memory】

直接内存并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 出现。

Run-time Data Areas各版本之间的变化

jdk 1.6

jdk 1.7

jdk 1.8

来看一个面试题

public static void main(String[] args) throws InterruptedException {
        int i = 8;
        i = (i++);  // i=i 也就是8, 然后i++
        Thread.sleep(2000);
        System.out.println(i);  // fucking 居然是8
        /*---------------------------------------------*/
        int j = 8;
        j++;
        System.out.println(j);  // 9
        /*---------------------------------------------*/
        int t = 8;
        t = ++t; // t++ 后 然后t=t
        System.out.println(t);  // 9
    }

执行结果为

8
9
9

代码指令

局部变量表

而++i的指令是这样的

所以++i是9

以下是结合上面的图详细分析

/**
 * 涉及到底层知识,方法压栈的过程
 * 0 bipush 8
 * 2 istore_1
 * 3 iload_1
 * 4 iinc 1 by 1
 * 7 istore_1
 * 8 getstatic #2 <java/lang/System.out>
 * 11 iload_1
 * 12 invokevirtual #3 <java/io/PrintStream.println>
 * 15 return

 * 分析:
 * 8是因为,
 * 首先i=8,
 * 然后iload_1将局部变量表1位置的值也就是8用重新压入栈,
 * 执行i++操作 也就是 iinc 1 by 1是对i进行i++ 变成9,而压入栈的那个8并没有变化
 * 然后执行赋值动作 将压入栈的8赋值给i, 所以i从9又变成了8
 * 所以结果是8
 */

来看一些图深入理解下栈帧

静态方法 没有this

非静态无参方法 第一个是this 存放对象本身

非静态有参方法 第一个是this 存放对象本身

还有调用的

查看jvm监视

C:Program FilesJavajdk1.8.0_191binjvisualvm.exe

Java内存模型(JMM)

JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,JVM中运行的每个线程都拥有自己的线程栈

堆内存逻辑分区

==针对的是分代垃圾回收器,不分代的(如ZGC等)没有这个概率==

GC算法

GC找到垃圾的算法

  • 根搜索算法 (Root Searching)

也叫根可达、可达性分析算法

通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在Java语言中,可以作为GCRoots的对象包括下面几种:

(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。

(2). 方法区中的类静态属性引用的对象。

(3). 方法区中runtime常量池。

(4). 本地方法栈中JNI(Native方法)引用的对象。

  • 引用计数法【Reference Count】(未被java采纳)

需要专门的空间来记录每个对象的被引用次数。

无法解决循环引用问题(a引用b,b引用c,c引用a)

因为:a b c 互相都是1引用,但是没有人会对象引用这一个整体,无法找到他们进行回收

GC回收垃圾的算法

分代收集

  • 复制算法(新生区)

主要用在新生区,这里存活率很低,适合整体复制,转移From/To。

  • 标记清除

三色标记,标记使用和未使用,清除未使用的,不会压缩,所以会有很多内存碎片

  • 标记-压缩(老年代)

标记清除并压缩,不会有内存碎片,缺点每次清除都压缩,浪费性能

  • 标记清除压缩

标记清除多少次或者碎片达到一定量,开始压缩

JVM调优必备理论

首先是堆内存的逻辑分区

上面已经讲过了,这里略过

对象从出生到消亡的过程

  • 达到固定年龄 可通过MaxTenuringThreshold配置
  • 动态年龄

上JVM计算动态年龄的代码(原因:网上讲的很多都不对)

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
    //survivor_capacity是survivor空间的大小
size_t desired_survivor_size = (size_t)((((double)     survivor_capacity)*TargetSurvivorRatio)/100);
size_t total = 0;
uint age = 1;
while (age < table_size) {
 total += sizes[age];//sizes数组是每个年龄段对象大小
 if (total > desired_survivor_size) break;
 age++;
}
uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
 ...
}

代码中有一个TargetSurvivorRatio的值

# 目标存活率,默认为50%
-XX:TargetSurvivorRatio

根据代码可以看到,动态年龄计算方式为:

  1. 通过这个比率来计算一个期望值,desired_survivor_size 。
  2. 然后用一个total计数器,累加每个年龄段对象大小的总和。
  3. 当total大于desired_survivor_size 停止。
  4. 然后用当前age和MaxTenuringThreshold 对比找出最小值作为结果。

总体表征就是,年龄从小到大进行累加,当加入某个年龄段后,累加和超过survivor区域*TargetSurvivorRatio的时候,就从age和MaxTenuringThreshold中的最小值往上的年龄的对象进行晋升。

// 配置存活多少次到老年区,Parallel Scavenge回收器默认是15,CMS是6,G1是15
-XX:MaxTenuringThreshold

轻GC和重GC

MinorGC/YGC :新生区空间耗尽时触发

MajorGC/FullGC:老奶奶带无法继续分配空间时触发,新生区和老年区都会进行回收

-X指的是非标, 和VMs实现有关的,并不是适用于所有的JVM

-Xms:最开始只有 -Xms 的参数,表示 初始 memory size(m表示memory,s表示size);

-Xmx:为了对齐三字符,压缩了其表示形式,采用计算机中约定表示方式: 用 x 表示 “大”,因此 -Xmx 中的 m 应当还是 memory。既然有了最大内存的概念,那么一开始的 -Xms 所表示的 初始 内存也就有了一个 最小 内存的概念(其实常用的做法中初始内存采用的也就是最小内存)。

-Xmn:n是new的意思 新生区配置

  • JVM 最小分配内存(初始分配内存)由-Xms指定,默认是物理内存的1/64
  • JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4

什么样的对象会在栈上分配(不用调整)

栈上分配是jvm的一个优化技术,对于那些线程私有的对象,可以将它们分配在栈上,而不是堆上。 JVM通过逃逸分析确定该对象不会被外部访问,如果不会逃逸可以将该对象在栈上分配内存

栈上分配的好处是可以在函数调用后自行销毁,而不是GC介入,从而提升了系统的性能。
栈上分配的基础是逃逸分析,逃逸分析的目的是判断对象的作用域是否有可能逃逸出函数体。

  • 线程私有,不会被共享并且是小的对象( 一般几十个bytes)
  • 没有逃逸,只会在某一段代码中使用,外部用不到

无逃逸

有逃逸

  • 支持标量替换 scalar replacement的

标量替换,Java中的原始类型无法再分解,可以看作标量(scalar);指向对象的引用也是标量;而对象本身则是聚合量(aggregate),可以包含任意个数的标量。如果一个Java对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替 ,这就叫做标量替换。原本的对象就无需整体分配空间了。

开启标量替换参数(-XX:+EliminateAllocations)

开启逃逸分析,局部new对象就会在栈上分配,省去了gc回收的消耗

-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC

关闭逃逸分析

 `-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC `

开启标量替换

-XX:+EliminateAllocations

线程本地分配 TLAB(不用调整)

占用eden区,默认1%的空间

能不能构造一种==线程私有的堆空间==,哪怕这块堆空间特别小,但是只要有,就可以每个线程在分配对象到堆空间时,先分配到自己所属的那一块堆空间中,避免同步带来的效率问题,从而提高分配效率

如何开启TLAB

JVM默认开启了TLAB功能,也可以使用-XX: +UseTLAB 显示开启

如何观察TLAB使用情况

JVM提供了-XX:+PrintTLAB 参数打开跟踪TLAB的使用情况

如何调整TLAB默认大小

-XX:TLABSize 通过该参数指定分配给每一个线程的TLAB空间的大小

// 开启逃逸分析
-XX:+DoEscapeAnalysis
// 开启线程本地分配缓冲
-XX: +UseTLAB
// 开启标量替换
-XX:+EliminateAllocations
// 指定进入老年区的触发次数  PS回收器默认是15,CMS是6,G1是15, 最大就是15,调不上去,因为JMM时讲过了,只有4bit!
-XX:MaxTenuringThreshold

GC收集器分类

上述谈到的4种垃圾回收算法,是内存回收的方法论,垃圾收集器就是算法的落地实现

4种主要的垃圾收集器:串行(Serial),并行(Parallel),并发(CMS),G1

java11/12还有一种新的,ZGC

新生区+老年区 GC的四种类型

新生区收集器:serial、PS(Parallel Scavenge) 、perNew

老年区收集器:Serial Old 、 PO(parallel Old)、CMS

整堆收集器:G1

jdk 1.4开始有CMS,jdk1.7开始有G1,但是默认都还是PO,jdk1.9默认就是G1,

诞生过程和内存增长有关,现在内存越来越大,旧的GC无论怎么调优都不能满足需求。发展起来的。

  • serial+Serial Old (串行收集器,单线程)

-XX:+UseSerialGC

  • PS(Parallel Scavenge) + PO(parallel Old) (并行)

-XX:+UseParallelGC

-XX:+UseParallelOldGC

并行,就是多个线程一起回收,但是用户线程还是处于等待状态。

  • perNew (Parallel Scavenge的升级版本,兼容CMS的组合)+ CMS (并发)

-XX:+UseParNewGC 这样写 只能是 parNew +serialOld 已经废弃

-XX:+UseConcMarkSweepGC 这样写才是pernew+CMS的组合

-XX:ParallelGCThreads 限制线程数量

并发:gc多个线程和用户线程一起并发执行,但是不一定是并行的,可能交替。特点是用户线程仍然在工作

  • CMS的不足,但是还是很多人用,JDK默认不是它,而是PO

CMS可以是可以并发的 Concurrent Mark Sweep 并发标记清除(应用程序线程和GC线程交替执行)

​ - 使用标记-清除算法

​ - 并发阶段会降低吞吐量(停顿时间减少,吞吐量降低

​ - 老年代收集器(新生代使用ParNew

​ - 会产生浮动垃圾(在清理的时候,同时也在产生新垃圾)

-XX:+UseConcMarkSweepGC

因为和用户线程一起运行,不能在空间快满时再清理(因为也许在并发GC的期间,用户线程又申请了大量内存,导致内存不够)

-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值 。默认为92%,也就是92%的时候会进行FGC,可以降低这个值比如设置为68%,让CMS保持空余足够的空间。

如果不幸内存预留空间不够,就会引起concurrent mode failure,就会启用Serial Old来清理

为啥不用PO来介入,PO和Serial Old都是采用的复制算法,所以网上说算法不同的都是错的。HotSpot之前根本没考虑实现这个,然后由于有人给他们提单了,现在已经纳入版本需求

  • G1

-XX:+UseG1GC=G1

目前还只能在Linux上用

STW(stop the word)

目前没有一个GC可以完全去掉STW(stop the word)的,也就是gc开始工作的时候,会让工作线程全部停掉,这就会产生一个停顿。这个停掉不是说停就停的,会找safe point线程安全点然后停掉。最牛逼的ZGC也做不到,只是能降到10ms甚至更低。

基本上 STW 阶段都是利用多线程并行来减少停顿时间,而并发阶段不会有太多的回收线程工作,这是为了不和应用线程争抢 CPU,反正都并发了慢就慢点(不过还是得考虑内存分配速率)

并发标记算法

CMS 用的是三色标记(白 灰 黑)算法+Incremental Update算法

G1 用的是三色标记+SATB

ZGC 用的是染色指针+ 读屏障