深入理解JVM
classLoader加载.class完成后,有两种执行方式:
解析执行
编译机器码执行: client 和server两种模式
Java代码执行机制
编译为class文件
Java源码编译机制
A 分析和输入的符号表(parse and enter)
Pase 词法和说法分析
Enter 符号输入到符号表
B 处理注解(annotation processing)
C 语义分析和生成class文件(analyse and generate)
类加载机制:
装载(load) 全限定名+类加载器
链接(link)
初始化(initialize):
调用了new
反射调用类中的方法
子类调用了初始化
Jvm启动过程中指定的初始化类
类加载器有四种:
Bootstrap classLoader
ExtensionClassLoader 加载扩展功能的一些jar包
SystemClass Loader 加载启动参数中指定的classPath中的jar包和目录
User-Defined ClassLoader (用户自定义类加载器)
classLoad提供的几个关键方法
loadClass() 加载指定类名的方法
查找顺序为: ClassLoader先从已加载的类中--------->parent classLoad
------------>systemClassLoad---------->最后覆盖findClass来作
特殊处理
findLoadedClass 是从当前classLoader实例对象的缓存中查询已加载的类 ,
调用的是本方法
findClass 此方法直接抛出classNotFoundException
findSystemClass 从system classLoader 如果未找到继续从bootstrap
classLoader中寻找,未找到返回空
defineClass 将二进制字节码转换为字节对象
resolveClass 完成class 对象的链接,如果链接过则直接返回
类加载过程中常抛出的异常
classNotFoundException 未找到类文件
noClassDefFoundError 加载类中引用的另外的类不存在
linkageError 在用户自定义加载器中容易出现,此类已经在classloader中加载过
了,重复加载会出此异常
classCastException 类型转换异常
类执行机制
字节码解释执行
执行方法的指令:
Invokestatic 调用静态方法
Invokevirtual 调用对象实例的方法
Invokeinterface 调用接口
Invokespecial 调用private 方法和编译原码后生成的init方法
Javac 编译代码
Javap -c 查看字节码
Sun JDK 基于栈的体系结构来执行字节码
示例代码如下:
Java代码
public class Demo {
public static void foo() {
int a = 1;
int b = 2;
int c = (a + b) * 5;
}
}
//字节码如下
public cn.yue.distributed.esb_soa.Demo();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void foo();
Code:
0: iconst_1 //将类型为int , 值为1的常量放入操作数栈
1: istore_0 //将操作数栈中的栈顶的值弹出放入局部变量区
2: iconst_2 //同上
3: istore_1
4: iload_0 //装载局部变量区中第一个值到操作数栈
5: iload_1 //同上装载第二个操作数
6: iadd //执行int类型的add 指令 并将计算出的结果放入操作数栈
7: iconst_5 //将类型为int , 值为1的常量放入操作数栈
8: imul // 执行int 类型的mul方法,将计算出的结果放入操作数栈
9: istore_2 //将操作数栈中弹出放入局部变量
10: return //返回
}
指令解释执行(冯 诺一曼体系中的FDX循环方式)
获取一条指令,解码分派,然后执行
实现FDX循环时有
switch-threading
示例代码:
While(true){
Int code=fetchNextCode();
Switch(){
Case IADD:
//do something
Case ...:
//do something
}
}
Tooken-threading
示例代码如下;
IADD:{
//do add;
fetchNextCode();
Dispatch();
}
Iconst_0:{
Push{0};
fetchNextCode();
Dispatch();
}
Dircet-threading
Subroutime-threading
Inline-threading
栈顶缓存
位于操作数栈顶的值放往寄予存器
部分栈帧共存
一个方法调用另一个方法,传到另一个方法的参数已存放在操作数栈的数据
Sun JDK作了一个优化,就是当调用方法时,后一方法可将前一方法的操作数栈
作为当前方法的局部变量
编译执行
为提升代码的执行性能 ,JDK提供将字节码编译为机器码的支持,编译在运行时执行
通常为JIT编译器
提供的两种模式为:
client compiler
JDK1.6以后采用的是线性扫描寄存器分配算法
其他方法的优化:
方法内联
示例代码如下
Public class demo1(){
....
demo2();
....
}
Public class demo2(){
}
//如果编译后demo2的字节数小于35个字节
会用类以于以下的结梦
Public class demo1(){
//demo2
Public class demo2(){
}
}
可以在JDK的启动参数上加-XX:+PrintlnLining
去虚拟化
在装载class文件后,如果类中的方法只提供一个实现类。对于调用了此方法的代码,也可以进行方法内联
冗余削除
在编译时,根据运行时情况进行代码折叠或削除
private static final Log log=LogFactory.getLog("");
private static final boolean isDebug=log.isDebugEnable();
public void execute(){
if(isDebug){
//do sth
}
//do something
}
如果isDebugEnabled返回的为false
那么以上方法就变为:
public void execute(){
//do something
}
Server compiler 图着色寄存器算法(逃逸分析是进行优化的基础)
标量替换
栈上分配
同步消除
反射执行
Class clazz = Class.forName("类名");
Method method = clazz.getMethod("方法名", null);
Object object = clazz.newInstance();
method.invoke(object, null);
Jvm内存管理
方法区 存放了要加载的类的信息 最小值为16m,最大值为64m
堆: 存储对象实例和数组值,也可以理解为new出来的对象
32位操作系统上最大为2G ,64位机器上没有限制
新生代(new generation):大多数新建的对象从新生代分配内存
旧生代(old generation): 用于存放新生代中多次垃圾回收仍然存在的对象
本地方法栈 支持本地方法的执行
Pc寄存器和JVM方法栈 每个线程都会创建PC寄存器和JVM方法栈
Pc寄存器占用的或操作系统内存
JVM占用操作系统内存
当空间栈空间不足时,会抛stackOverFlowError
内存分配
Java 对象占用的内存主要从堆上分配,堆是线程共享的,在分配内存时要进行加锁
为每个新建线程分配一块独立的空间(TLAB)thread local allocation buffer
内存回收
收集器
引用计数收集器
跟踪收集器
实现方法
复制
标记-清除
标记-压缩
JDK中可用的GC
新生代可用GC
未看完
JVM内存状况查看和分析工具
A 输出gc日志
输出到控制台:
在启动参数中:-XX:+PrintGC - XX:+PrintDetails -XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
输出到指定文件:
-verbose:gc -XX:+PtingTenuringDistribution
B GC Portal
C JConsole
D JVisualVM
E JMap
F JStat
G Eclipse Memory Analyzer
JVM 线程资源同步及交互机制
线程资源同步机制 示例代码如下
int i = 0;
public int getNextId() {
return i++;
}
以上代码的执行的步骤为:
首先在main memory(java堆中) 给i分配内存 ,并存0;
线程启动后,会分配一片working memory(操作数栈)
i ++执行的步骤为: 装载 读取 进行i+1 操作,存储i 写入i
线程交互机制
线程状态分析
查看线程状态
Kill -3 [线程id] 将线程的相关信息输出到控制台
Jstack 可以直接查看线程的运行状况
JConsole 工具
threadXMBean
TDA
序列化和反序列化
性能调优
Cpu消耗分析
Linux中cpu用于中断、内核、用户进程的任务处理
上下文切换
每个cpu只能执行一个线程 采用抢占式调用
运行队列 控制每个cpu核上运行队列为1-3个
利用率
可以通过top查看进程中的CPU的消耗状态
Pidstat 是systat中的工具,需先安装SYSTAT
Cpu的消耗主要体现在两个方面上 us sy
文件IO消耗分析
跟踪线程的文件IO的消耗
方法一:
Pidstat -d -t -p[pid] 100 类似的命令可以查看线程IO 的消耗状况
方法二:
Iostat
网络IO消耗分析
Google修改kernel方法对网卡中断不均的问题进行修复,或是用支持MSI-X的网卡来修复
查看网络IO消耗状况
Sar -n FULL 12
内存消耗分析
通过vmstat sar top pidstat 等方式来查看swap和物理内存消耗状况
Vmstat 和内存相关的信息 其中swpd是大小说明物理内存不够用,将数
据放到硬盘上的数据的大小
Sar 的-r参数可以查看内存消耗状况,可以查询历史状况
以上两者不能分析进程所占用的内存量
Top 可以查看所消耗的内存量
Pidstat pidstat -r -p[pid] [interval][times]
对物理内存的消耗
实现对物理内存的直接操作
public static void main(String[] args) throws InterruptedException {
Thread.sleep(20000);
System.out.println("read to create bytes, so JVM heep will be used");
byte[] bs = new byte[128 * 1000 * 1000];
bs[0] = 1;
bs[1] = 2;
Thread.sleep(10000);
System.out.println("read to allocate & put direct bytebuffer, no JVM heep should be used");
ByteBuffer byteBuffer = ByteBuffer.allocate(128 * 1024 * 1024);
byteBuffer.put(bs);
byteBuffer.flip();
Thread.sleep(10000);
System.out.println("read to gc,JVM heep will be freed");
bs = null;
System.gc();
Thread.sleep(10000);
System.out.println("read to get bs,then JVM heap will bi used");
byte[] resultbytes = new byte[128 * 1000 * 1000];
byteBuffer.get(resultbytes);
System.out.println("resultbytes[1] is: " + resultbytes[1]);
Thread.sleep(10000);
System.out.println("read to gc all");
byteBuffer = null;
resultbytes = null;
System.gc();
Thread.sleep(10000);
}
程序执行慢原回分析
锁竟争激烈
未充分使用硬件资源
数据量增长 jprofiler可以查看记录程序的时间消耗
性能调优
可以从硬件 操作系统 JVM 程序
JVM调优
代大小的调优
在不采用G1 的情况下 通常minor GC 会快于full GC
各个代大小的设置决定了minor GC和full GC触发的时机
程序调优
Cpu us高的解决方法
添加 thread.sleep(),以释放CPU的执行权
Cpu sy高的解决方法
减少线程数
网络IO并发操作,可以采用协程(coroutine)来支撑更高的并发量
实现协程的框架为killim
文件IO消耗严重
异步写文件
批量读写
限流
限制文件大小
网络IO消耗
释放不必要的引用
使用对象缓存池
采用合理的缓存失效算法 建立缓存池 FIFO LRU LFU