您的位置:首页 > 其它

JVM相关知识总结整理

2018-02-05 16:34 567 查看

JVM启动流程



JVM基本结构



PC寄存器

每个线程拥有一个PC寄存器

在线程创建时创建

指向下一条指令的地址

执行本地方法时,PC的值为undefined

方法区

保存装载的类信息

类型的常量池(JDK6时,String等常量池置于方法,JDK7时,已经移动到了堆)

字段、方法信息

方法字节码

通常和永久区(Perm)关联在一起

Java堆

和程序开发密切相关

应用系统对象都保存在Java堆中

所有线程共享Java堆

对分代GC来说,堆也是分代的

GC的主要工作区间



Java栈

线程私有

栈由一系列帧组成(因此Java栈也叫做帧栈)

帧保存一个方法的局部变量、操作数栈、常量池指针

每一次方法调用创建一个帧,并压栈

Java栈——局部变量表 包含参数和局部变量

public static int runStatic(int i, long l, float f, Object o, byte b) {
return 0;
}


静态方法的局部变量表如下图所示



public int runInstance(char c, short s, boolean b) {
return 0;
}


实例方法的局部变量表如下图所示



Java栈——函数调用组成帧栈

public static int runStatic(int i, long l, float f, Object o, byte b) {
return runStatic(i, l, f, o, b);
}




Java栈——操作数栈

java没有寄存器,所有参数传递使用操作数栈

public static int add(int a, int b) {
int c = 0;
c = a + b;
return c;
}


对应的操作为:

0: iconst_0 // 0 压栈
1: istore_2 // 弹出int,存放于局部变量2
2: iload_0  // 把局部变量0压栈
3: iload_1  // 局部变量1压栈
4: iadd     // 弹出2个变量,求和,结果压栈
5: istore_2 // 弹出结果,放于局部变量2
6: iload_2  // 局部变量2压栈
7: ireturn  // 返回




Java栈——栈上分配

小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上

直接分配在栈上,函数调用完成自动清理空间,减轻GC压力

大对象或者逃逸对象无法栈上分配

栈、堆、方法区交互

// 运行时,JVM把AppMain的信息都放入方法区
public class AppMain {
// main方法本身放入方法区
public static void main(String[] args){
// test1 是引用,所以放到栈区里,Sample是自定义对象应该放到堆里面
Sample test1 = new Sample("测试1");
Sample test2 = new Sample("测试2");

test1.printName();
test2.printName();
}
}

// 运行时,JVM把Sample的信息都放入方法区
public class Sample {
private String name;

// new Sample实例后,name引用放入栈区里,name对象放入堆里
public Sample(String name){
this.name = name;
}

// print方法本身放入方法区里
public void printName(){
System.out.println(name);
}
}




内存模型

每一个线程有一个工作内存和主内存

工作内存存放主内存中变量的值的拷贝

当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作



每一个操作都是原子的,即执行期间不会被中断

对于普通变量,一个线程中更新的值,不能马上反应在其他线程中



如果需要在其他线程中立即可见,需要使用volatile关键字

volatile

public class VolatileStopThread extends Thread {
private volatile boolean stop = false;
public void stopMe(){
stop = true;
}

@Override
public void run(){
int i = 0;
while(!stop){
i++;
}
System.out.println("Stop thread");
}

public static void main(String args[]) throws InterruptedException {
VolatileStopThread t = new VolatileStopThread();
t.start();
Thread.sleep(1000);
t.stopMe();
Thread.sleep(1000);
}
}


如果没有volatile关键字,server运行就无法停止

volatile不能代替锁,一般认为volatile比锁性能好(不绝对)

选择使用volatile的条件是:语义是否满足应用

可见性

一个线程修改了变量,其他线程可以立即知道的方法:

volatile

synchronized(unlock之前,写变量值回主内存)

final(一旦初始化完成,其他线程就可见)

有序性

在本线程内,操作都是有序的

在线程外观察,操作都是无序的(指令重排或主内存同步延时)

指令重排

线程内串行语义

写后读 a = 1; b = a; 写一个变量之后,再读这个位置

写后写 a = 1; a = 2; 写一个变量之后,再写这个变量

读后写 a = b; b = 1; 读一个变量之后,再写这个变量

以上语句不可重排

编译器不考虑多线程间的语义

可重排: a = 1; b = 2;

指令重排——破坏线程间的有序性

class OrderExample {
int a = 0;
boolean flag = false;

public void writer(){
a = 1;
flag = true;
}

public void reader(){
if(flag){
int i = a + 1;
...
}
}
}


线程A首先执行writer()方法

线程B接着执行reader()方法

线程B在int i = a + 1是不一定能看到a已经被赋值为1,因为在writer中,两句话顺序可能打乱



指令重排——保证有序性的方法

class OrderExample {
int a = 0;
boolean flag = false;

public synchronized void writer(){
a = 1;
flag = true;
}

public synchronized void reader(){
if(flag){
int i = a + 1;
...
}
}
}


同步后,即使做了writer重排,因为互斥的缘故,reader线程看writer线程也是顺序执行的



指令重排的基本原则

程序顺序原则:一个线程内保证语义的串行性

volatile规则:volatile变量的写,先发生于读

锁规则:解锁(unlock)必然发生在随后的加锁(lock)前

传递性:A先于B,B先于C,那么A必然先于C

线程的start方法先于它的每一个动作

线程的所有操作先于线程的终结(Thread.join())

线程的中断(interrupt())先于被中断线程的代码

对象的构造函数执行结束先于finalize()方法

解释运行

解释执行以解释方式运行字节码

解释执行的意思是:读一句执行一句

编译运行(JIT)

将字节码编译成机器码

直接执行机器码

运行时编译

编译后性能有数量级的提升

常用JVM配置参数

Trace跟踪参数

-verbose:gc

输出虚拟机中GC的详细情况

使用后输出如下:

[Full GC 168K->97K(1984K), 0.0253873 secs]


168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,数据1984K为堆内存的总容量,收集所需要的时间是0.0253873秒

-XX:+PrintGC

同-verbose:gc

-XX:+PrintGCDetails

打印GC详细信息



-XX:+PrintGCTimeStamps

[GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]


-Xloggc:log/gc.log

指定GC log的位置,以文件输出

帮助开发人员分析问题

-XX:+PrintHeapAtGC

每次一次GC后,都打印堆信息



-XX:+TraceClassLoading

监控类的加载



-XX:+PrintClassHistogram

按下Ctrl+Break后,打印类的信息



分别显示:序号、实例数量、总大小、类型

堆分配参数

-Xmx -Xms

指定最大堆和最小堆

-Xmx20m -Xms5m

System.out.print("Xmx = ");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");

System.out.print("free mem = ");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");

System.out.print("total mem = ");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");

// 结果为
Xmx = 19.375M
free mem = 4.342750549316406M
total mem = 4.875M


Java会尽可能维持在最小堆

-Xmn

设置新生代大小(官方推荐新生代占堆的3/8)

-XX:NewRatio

新生代(eden + 2 * s)和老年代(不包含永久区)的比值

4表示新生代:老年代=1:4,即新生代占堆的1/5

-XX:SurvivorRatio

设置两个Survivor区和eden的比

8表示两个Survivor:eden=2:8,即一个Survivor占新生代的1/10(官方推荐)

如下例

public static void main(String[] args){
byte[] b = null;
for(int i = 0; i < 10; i++){
b = new byte[1 * 1024 * 1024];
}
}


如果设置

-Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails




则没有触发GC,数据全部分配在老年代

如果设置

-Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails




则没有触发GC,数据全部分配在eden,老年代没有使用

如果设置

-Xmx20m -Xms20m -Xmn7m -XX:+PrintGCDetails




则进行了2次新生代GC,s0 s1太小需要老年代担保

如果设置

-Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails




则进行了3次新生代GC,s0 s1增大

如果设置

-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+PrintGCDetails




则进行了2次新生代GC,新生代空间增大

如果设置

-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3 -XX:+PrintGCDetails




则进行了1次新生代GC,新生代空间增大,s0 s1增大

-XX:+HeapDumpOnOutOfMemoryError

OOM时导出堆到文件

-XX:+HeapDumpPath

导出OOM的路径

示例:

-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpPath

Vector v = new Vector();
for(int i = 0; i < 25; i++){
v.add(new byte[1 * 1024 * 1024]);
}




-XX:OnOutOfMemoryError

在OOM时,执行一个脚本

“-XX:OnOutOfMemoryError=D:/tools/printstack.bat %p”,printstack.bat的内容为

D:/tools/jdk1.7_40/bin/jstack -F %1 > D:/a.txt


当程序OOM时,在D:/a.txt中会生成线程的dump

可以在OOM时,发送邮件,甚至是重启程序

永久区分配参数

-XX:PermSize -XX:MaxPermSize

设置永久区的初始空间和最大空间

他们表示,一个系统可以容纳多少个类型

使用CGLIB等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致OOM

for(int i = 0; i < 100000; i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean" + i, new HashMap()); // 不断地产生新的类
}




栈大小分配

-Xss

通常只有几百K

决定了函数调用的深度

每个线程都有独立的栈空间

局部变量、参数分配在栈上

如下例

public class TestStackDeep {
private static int count = 0;

public static void recursion(long a, long b, long c) {
long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10;
count++;
recursion(a, b, c);
}

public static void main(String args[]) {
try {
recursion(0L, 0L, 0L);
} catch (Throwable e) {
System.out.println("deep of calling = " + count);
e.printStackTrace();
}
}
}


设置-Xss128K,抛出java.lang.StackOverflowError时,deep of calling = 292

设置-Xss256K,抛出java.lang.StackOverflowError时,deep of calling = 1080

JIT及其相关参数

字节码执行性能较差,所以可以对于热点代码编译成机器码再执行,在运行时的编译,叫做JIT Just-In-Time

JIT的基本思路是,将热点代码,就是执行比较频繁的代码,编译成机器码



相关参数

Xint

解释执行

Xcomp

全部编译执行

Xmixed

默认,混合

GC算法与种类

引用计数法

老牌垃圾回收算法

通过引用计算来回收垃圾

使用者

COM

ActionScript

Python

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能被再被使用



引用计数法的问题

引用和去引用伴随着加法和减法,影响性能

很难处理循环引用



标记-清除

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法是将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根结点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象,然后,在清除阶段,清除所有未被标记的对象



标记-压缩

标记-压缩算法适合用于存活对象较多的场合,如老年代,它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端,之后,清理边界外所有的空间



复制算法

与标记-清除算法相比,复制算法是一种相对高效的回收方法

不适用于存活对象较多的场合,如老年代

将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收



分代思想

依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代

根据不同代的特点,选取合适的收集算法

少量对象存活,适合复制算法

大量对象存活,适合标记清理或者标记压缩

GC算法总结

引用计数

没有被Java采用

标记-清除

标记-压缩

复制算法

新生代

所有的算法,需要能够识别一个垃圾对象,因此需要给出一个可触及性的定义

可触及性

可触及的

从根节点可以触及到这个对象

根节点包括

栈中引用的对象

方法区中静态成员或者常量引用的对象(全局对象)

JNI方法栈中引用对象

可复活的

一旦所有引用被释放,就是可复活状态

因为在finalize()中可能复活该对象

不可触及的

在finalize()后,可能会进入不可触及状态

不可触及的对象不可能复活

可以回收

public class CanReliveObj {

public static CanReliveObj obj;

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanReliveObj finalize called");
obj = this;
}

@Override
public String toString() {
return "I am CanReliveObj";
}

public static void main(String[] args) throws
InterruptedException {
obj = new CanReliveObj();
obj = null;   //可复活
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
System.out.println("第二次gc");
obj = null;    //不可复活
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj 是 null");
} else {
System.out.println("obj 可用");
}
}
}

// 输出结果为:
CanReliveObj finalize called
obj 可用
第二次gc
obj 是 null


应该尽量避免使用finalize(),操作不慎可能导致错误,因为它的优先级低,何时被调用不确定,何时发生GC也不确定,可以使用try-catch-finally来替代它

Stop-The-World

Stop-The-World

java中一种全局暂停的现象

全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互

多半由于GC引起

Dump线程

死锁检查

堆Dump

GC时为什么会有全局停顿

类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净

危害

长时间服务停止,没有响应

遇到HA系统,可能引起主备切换,严重危害生产环境

GC参数

串行收集器

最古老,最稳定

效率高

可能会产生较长的停顿

-XX:+UseSerialGC

新生代、老年代使用串行回收

新生代复制算法

老年代标记-压缩



并行收集器

ParNew

-XX:+UseParNewGC

新生代并行

老年代串行

Serial收集器新生代的并行版本

复制算法

多线程,需要多核支持

-XX:ParallelGCThreads 限制线程数量



Parallel收集器

类似ParNew

新生代复制算法

老年代标记-压缩

更加关注吞吐量

-XX:+UseParallelGC

使用Parallel收集器+老年代串行

-XX:+UseParallelOldGC

使用Parallel收集器+老年代并行



-XX:MaxGCPauseMills

最大停顿时间,单位毫秒

GC尽力保证回收时间不超过设定值

-XX:GCTimeRatio

0-100的取值范围

垃圾收集时间占总时间的比

默认99,即最大允许1%时间做GC

XX:MaxGCPauseMills和XX:GCTimeRatio,这两个参数是矛盾的,因为停顿时间和吞吐量不可能同时调优

CMS收集器

CMS收集器

Concurrent Mark Sweep 并发(与用户线程一起执行)标记清除

标记-清除算法

并发阶段会降低吞吐量

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

-XX:+UseConcMarkSweepGC

CMS运行过程比较复杂,着重实现了标记过程,可分为

初始标记

根可以直接关联到的对象

速度快

并发标记(和用户线程一起)

主要标记过程,标记全部对象

重新标记

由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正

并发清除(和用户线程一起)

基于标记结果,直接清理对象



特点

尽可能降低停顿

会影响系统整体吞吐量和性能

比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半

清理不彻底

因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理

因为和用户线程一起运行,不能在空间快满时再清理

-XX:CMSInitiatingOccupancyFraction设置触发GC的阀值

如果不幸内存预留空间不够,就会引起concurrent mode failure



-XX:+UseCMSCompactAtFullCollection Full GC后,进行一次整理

整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction

设置进行几次Full GC后,进行一次碎片整理

-XX:ParallelCMSThreads

设定CMS的线程数量

-XX:CMSInitiatingPermOccupancyFraction

当永久区占用率达到这一百分比时,启动CMS回收

XX:UseCMSInitiatingOccupancyOnly

表示只在到达阀值的时候,才进行CMS回收

类装载器

class装载验证流程

加载

取得类的二进制流

转为方法区数据结构

在Java堆中生成对应的java.lang.Class对象

链接

验证

目的:保证Class流的格式是正确的

文件格式的验证

是否以0xCAFEBABE开头

版本号是否合理

元数据验证

是否有父类

是否继承了final类

非抽象类是否实现了所有的抽象方法

字节码验证(很复杂)

运行检查

栈数据类型和操作码数据参数是否吻合

跳转指令是否指定到合理的位置

准备

分配内存,并为类设置初始值(方法区中)

public static int v = 1;

在准备阶段中,v会被设置为0

在初始化的<clinit>中才会被设置为1

对于static final类型,在准备阶段就会被赋上正确的值

解析

符合引用(字符串)替换为直接引用(指针或者地址偏移量)

初始化

执行类构造器<clinit>

static变量赋值语句

static{}语句

子类的<clinit>调用前保证父类的<clinit>被调用

<clinit>是线程安全的

什么是类装载器ClassLoader

ClassLoader是一个抽象类

ClassLoader的实例将读入Java字节码将类装载到JVM中

ClassLoader可以定制,满足不同的字节码流获取方式

ClassLoader负责类装载过程中的加载阶段

ClassLoader的重要方法

public Class <?> loadClass(String name) throws ClassNotFoundException

载入并返回一个Class

protected final Class<?> defineClass(byte[] b, int off, int len)

定义一个类,不公开调用

protected Class<?> findClass(String name) throws ClassNotFoundException

loadClass回调该方法,自定义ClassLoader的推荐做法

protected final Class<?> findLoadedClass(String name)

寻找已经加载的类

ClassLoader的分类

BootStrap ClassLoader(启动ClassLoader)

Extension ClassLoader(扩展ClassLoader)

App ClassLoader(应用ClassLoader/系统ClassLoader)

Custom ClassLoader(自定义ClassLoader)

ClassLoader的协同工作



对象头Mark

Mark Word,对象头的标记,32位

描述对象的hash、锁信息,垃圾回收标记,年龄

指向锁记录的指针

指向monitor的指针

GC标记

偏向锁线程ID

偏向锁

大部分情况是没有竞争的,所以可以通过偏向来提高性能

所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程

将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark

只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步

当其他线程请求相同的锁时,偏向模式结束

-XX:+UseBiasedLocking

默认启用

在竞争激烈的场合,偏向锁会增加系统负担

轻量级锁

普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法

如果对象没有被锁定

将对象头的Mark指针保存到锁对象中

将对象头设置为指向锁的指针(在线程栈空间中)

如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)

在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗

在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降

自旋锁

当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)

JDK1.6中-XX:+UseSpinning开启

JDK1.7中,去掉此参数,改为内置实现

如果同步块很长,自旋失败,会降低系统性能

如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

JVM中获取锁的步骤

偏向锁可用会尝试偏向锁

轻量级锁可用会先尝试轻量级锁

以上都失败,尝试自旋锁

再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

锁优化方法

减少锁持有时间

public synchronized void syncMethod(){
othercode1();
mutextMethod();
othercode2();
}

=>

public void syncMethod2(){
othercode1()
synchronized(this){
mutextMethod();
}
othercode2();
}


减小锁粒度

将大对象,拆成小对象,大大增加并行度,降低锁竞争

偏向锁,轻量级锁成功率提高

ConcurrentHashMap

若干个Segment:Segment<K,V>[] segments

Segment中维护HashEntry<K,V>

put操作时,先定位到Segment,锁定一个Segment,执行input

在减小锁粒度后,ConcurrentHashMap允许若干个线程同时进入

锁分离

根据功能进行锁分离

ReadWriteLock

读多写少的情况,可以提高性能



锁粗化

如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化

public void demoMethod(){
synchronized(lock){
// do sth
}

// 做其他不需要同步的工作,但能很快执行完毕
synchronized(lock){
// do sth
}
}

=>

public void demoMethod(){
// 整合成一次锁请求
synchronized(lock){
// do sth
// 做其他不需要同步的工作,但能很快执行完毕
// do sth
}
}


锁消除

在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作

无锁

锁是悲观的操作

无锁是乐观的操作

无锁是一种实现方式

CAS(Compare And Swap)

非阻塞的同步

CAS(V,E,N)

在应用层面判断多线程的干扰,如果有干扰,则通知线程重试
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: