深入理解JVM
classLoader加载.class完成后,有两种执行方式:
解析执行
编译机器码执行:client和server两种模式
Java代码执行机制
编译为class文件
Java源码编译机制
A分析和输入的符号表(parseandenter)
Pase词法和说法分析
Enter符号输入到符号表
B处理注解(annotationprocessing)
C语义分析和生成class文件(analyseandgenerate)
类加载机制:
装载(load)全限定名+类加载器
链接(link)
初始化(initialize):
调用了new
反射调用类中的方法
子类调用了初始化
Jvm启动过程中指定的初始化类
类加载器有四种:
BootstrapclassLoader
ExtensionClassLoader加载扩展功能的一些jar包
SystemClassLoader加载启动参数中指定的classPath中的jar包和目录
User-DefinedClassLoader(用户自定义类加载器)
classLoad提供的几个关键方法
loadClass()加载指定类名的方法
查找顺序为:ClassLoader先从已加载的类中--------->parentclassLoad
------------>systemClassLoad---------->最后覆盖findClass来作
特殊处理
findLoadedClass是从当前classLoader实例对象的缓存中查询已加载的类,
调用的是本方法
findClass此方法直接抛出classNotFoundException
findSystemClass从systemclassLoader如果未找到继续从bootstrap
classLoader中寻找,未找到返回空
defineClass将二进制字节码转换为字节对象
resolveClass完成class对象的链接,如果链接过则直接返回
类加载过程中常抛出的异常
classNotFoundException未找到类文件
noClassDefFoundError加载类中引用的另外的类不存在
linkageError在用户自定义加载器中容易出现,此类已经在classloader中加载过
了,重复加载会出此异常
classCastException类型转换异常
类执行机制
字节码解释执行
执行方法的指令:
Invokestatic调用静态方法
Invokevirtual调用对象实例的方法
Invokeinterface调用接口
Invokespecial调用private方法和编译原码后生成的init方法
Javac编译代码
Javap-c查看字节码
SunJDK基于栈的体系结构来执行字节码
示例代码如下:
Java代码
publicclassDemo{
publicstaticvoidfoo(){
inta=1;
intb=2;
intc=(a+b)*5;
}
}
//字节码如下
publiccn.yue.distributed.esb_soa.Demo();
Code:
0:aload_0
1:invokespecial#1;//Methodjava/lang/Object."<init>":()V
4:return
publicstaticvoidfoo();
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){
Intcode=fetchNextCode();
Switch(){
CaseIADD:
//dosomething
Case...:
//dosomething
}
}
Tooken-threading
示例代码如下;
IADD:{
//doadd;
fetchNextCode();
Dispatch();
}
Iconst_0:{
Push{0};
fetchNextCode();
Dispatch();
}
Dircet-threading
Subroutime-threading
Inline-threading
栈顶缓存
位于操作数栈顶的值放往寄予存器
部分栈帧共存
一个方法调用另一个方法,传到另一个方法的参数已存放在操作数栈的数据
SunJDK作了一个优化,就是当调用方法时,后一方法可将前一方法的操作数栈
作为当前方法的局部变量
编译执行
为提升代码的执行性能,JDK提供将字节码编译为机器码的支持,编译在运行时执行
通常为JIT编译器
提供的两种模式为:
clientcompiler
JDK1.6以后采用的是线性扫描寄存器分配算法
其他方法的优化:
方法内联
示例代码如下
Publicclassdemo1(){
....
demo2();
....
}
Publicclassdemo2(){
}
//如果编译后demo2的字节数小于35个字节
会用类以于以下的结梦
Publicclassdemo1(){
//demo2
Publicclassdemo2(){
}
}
可以在JDK的启动参数上加-XX:+PrintlnLining
去虚拟化
在装载class文件后,如果类中的方法只提供一个实现类。对于调用了此方法的代码,也可以进行方法内联
冗余削除
在编译时,根据运行时情况进行代码折叠或削除
privatestaticfinalLoglog=LogFactory.getLog("");
privatestaticfinalbooleanisDebug=log.isDebugEnable();
publicvoidexecute(){
if(isDebug){
//dosth
}
//dosomething
}
如果isDebugEnabled返回的为false
那么以上方法就变为:
publicvoidexecute(){
//dosomething
}
Servercompiler图着色寄存器算法(逃逸分析是进行优化的基础)
标量替换
栈上分配
同步消除
反射执行
Classclazz=Class.forName("类名");
Methodmethod=clazz.getMethod("方法名",null);
Objectobject=clazz.newInstance();
method.invoke(object,null);
Jvm内存管理
方法区存放了要加载的类的信息最小值为16m,最大值为64m
堆:存储对象实例和数组值,也可以理解为new出来的对象
32位操作系统上最大为2G,64位机器上没有限制
新生代(newgeneration):大多数新建的对象从新生代分配内存
旧生代(oldgeneration):用于存放新生代中多次垃圾回收仍然存在的对象
本地方法栈支持本地方法的执行
Pc寄存器和JVM方法栈每个线程都会创建PC寄存器和JVM方法栈
Pc寄存器占用的或操作系统内存
JVM占用操作系统内存
当空间栈空间不足时,会抛stackOverFlowError
内存分配
Java对象占用的内存主要从堆上分配,堆是线程共享的,在分配内存时要进行加锁
为每个新建线程分配一块独立的空间(TLAB)threadlocalallocationbuffer
内存回收
收集器
引用计数收集器
跟踪收集器
实现方法
复制
标记-清除
标记-压缩
JDK中可用的GC
新生代可用GC
未看完
JVM内存状况查看和分析工具
A输出gc日志
输出到控制台:
在启动参数中:-XX:+PrintGC-XX:+PrintDetails-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
输出到指定文件:
-verbose:gc-XX:+PtingTenuringDistribution
BGCPortal
CJConsole
DJVisualVM
EJMap
FJStat
GEclipseMemoryAnalyzer
JVM线程资源同步及交互机制
线程资源同步机制示例代码如下
inti=0;
publicintgetNextId(){
returni++;
}
以上代码的执行的步骤为:
首先在mainmemory(java堆中)给i分配内存,并存0;
线程启动后,会分配一片workingmemory(操作数栈)
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的消耗主要体现在两个方面上ussy
文件IO消耗分析
跟踪线程的文件IO的消耗
方法一:
Pidstat-d-t-p[pid]100类似的命令可以查看线程IO的消耗状况
方法二:
Iostat
网络IO消耗分析
Google修改kernel方法对网卡中断不均的问题进行修复,或是用支持MSI-X的网卡来修复
查看网络IO消耗状况
Sar-nFULL12
内存消耗分析
通过vmstatsartoppidstat等方式来查看swap和物理内存消耗状况
Vmstat和内存相关的信息其中swpd是大小说明物理内存不够用,将数
据放到硬盘上的数据的大小
Sar的-r参数可以查看内存消耗状况,可以查询历史状况
以上两者不能分析进程所占用的内存量
Top可以查看所消耗的内存量
Pidstatpidstat-r-p[pid][interval][times]
对物理内存的消耗
实现对物理内存的直接操作
publicstaticvoidmain(String[]args)throwsInterruptedException{
Thread.sleep(20000);
System.out.println("readtocreatebytes,soJVMheepwillbeused");
byte[]bs=newbyte[128*1000*1000];
bs[0]=1;
bs[1]=2;
Thread.sleep(10000);
System.out.println("readtoallocate&putdirectbytebuffer,noJVMheepshouldbeused");
ByteBufferbyteBuffer=ByteBuffer.allocate(128*1024*1024);
byteBuffer.put(bs);
byteBuffer.flip();
Thread.sleep(10000);
System.out.println("readtogc,JVMheepwillbefreed");
bs=null;
System.gc();
Thread.sleep(10000);
System.out.println("readtogetbs,thenJVMheapwillbiused");
byte[]resultbytes=newbyte[128*1000*1000];
byteBuffer.get(resultbytes);
System.out.println("resultbytes[1]is:"+resultbytes[1]);
Thread.sleep(10000);
System.out.println("readtogcall");
byteBuffer=null;
resultbytes=null;
System.gc();
Thread.sleep(10000);
}
程序执行慢原回分析
锁竟争激烈
未充分使用硬件资源
数据量增长jprofiler可以查看记录程序的时间消耗
性能调优
可以从硬件操作系统JVM程序
JVM调优
代大小的调优
在不采用G1的情况下通常minorGC会快于fullGC
各个代大小的设置决定了minorGC和fullGC触发的时机
程序调优
Cpuus高的解决方法
添加thread.sleep(),以释放CPU的执行权
Cpusy高的解决方法
减少线程数
网络IO并发操作,可以采用协程(coroutine)来支撑更高的并发量
实现协程的框架为killim
文件IO消耗严重
异步写文件
批量读写
限流
限制文件大小
网络IO消耗
释放不必要的引用
使用对象缓存池
采用合理的缓存失效算法建立缓存池FIFOLRULFU