一、程序计数器:程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
程序计数器处于线程独占区
如果线程执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是native方法,这个计数器的值为undefined
此区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
java虚拟机栈: 描述的是java方法执行的动态内存模型
栈帧: 每个方法的执行都会创建一个栈帧,伴随着方法从创建到执行完成。用于存储局部变量表,操作数栈,动态链接,方法出口等。
局部变量表: 存放编译时期可知的各种基本数据类型,引用类型,returnAddress类型
局部表量表的内存空间在编译时期完成分配,当进入一个方法时,这个方法需要在帧分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小的。
方法区: 存储虚拟机加载的类信息,常量、静态变量,即时编译器编译后的代码数据等
运行时常量池:在方法区中 ,数据被放置在了一个hashset中,所以会进行去重
对象的创建过程: new类名-->根据new的参数在常量池中定位一个类的符号的引用-->如果没有找到这个符号的引用,说明类还没有被加载、解析和初始化 -->
虚拟机为对象分配内存(位于堆中)-->将分配的内存初始化为零值(不包括对象头)-->调用对象的(init)方法
内存分配的方式:1、指针碰撞 2、空闲列表
用什么方式进行内存分配,取决于垃圾回收的方式
创建对象时线程安全问题的解决: 1、线程同步 2、本地线程分配缓冲
对象的结构:
Header(对象头)
· 自身运行时数据 (Mark work)
· 哈希值 GC分代年龄 锁状态标志 线程持有的锁 偏向线程ID 偏向时间戳
· 类型指针
InstanceData : 相同宽度的字段会被分配到一起
Padding: 填充内存的作用,HotSpot自动对象管理系统要求对象的大小必须是八个字节的整数倍,所以对象起始部分如果没有对齐就需要padding来进行填充
对象的访问定位:
使用句柄:对象的引用指向堆中的一块区域,该区域叫句柄池,句柄池中保存了实例对象的地址
直接指针 :直接指向对象的内容
垃圾回收:
1、如何判定对象为垃圾对象
· 引用计数法 :在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,这个计数器的值就-1
-verBose:gc 打印垃圾回收机制的信息(简单信息)
--XX:+printGCDetail :打印详细的信息
引用计数法存在的问题:当外部不对对象进行引用,但堆中的对象彼此之间互相引用,这样的对象用引用计数法无法进行回收
· 可达性分析法 :首先定义一个GCroot,然后从GCroot向下走,能够走到就证明这个对象还能被使用,走不到就证明这个对象可以被回收了
可以作为GCroot的对象: ·虚拟机栈 ·方法区的类属性所引用的对象 ·方法区中的常量所引用的对象 ·本地方法栈中所引用的对象
2、 如何进行回收
·回收的策略
·标记清除
·复制算法
·标记-整理法
·分代回收算法
·垃圾回收器(不同的收集器适用的区域和范围不同,新生代和老年代的垃圾回收器是不能相同的)
·serial: 最基本、发展最悠久 单线程的垃圾收集器
·parnew:多线程的,该收集器的关注点是缩短垃圾收集的时间,提高用户的体验度
·parallel: parallel和 parnew都是新生代收集器,都是多线程的收集器。该收集器的目标是达到一个可控制的吞吐量
吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值。
吞吐量 = (执行用户代码的时间)/(执行用户代码的时间+垃圾回收所占用的时间)
-xx:MaxGCPauseMillis 垃圾收集器停顿的时间
-xx:GCTimeTadio: 吞吐量的大小
·CMS:(适用于老年代)
工作过程: 初始标记 并发标记 重新标记 并发清理
优点:并发收集 低停顿
缺点:占用大量的cpu资源 无法处理浮动垃圾 出现Concurrent Mode Failure 空间碎片
·G1: 能够充分的利用多核CPU的优势,实现并行并发,分代收集,空间整合,可预测的停顿
步骤: 初始标记 并发标记 最终标记 筛选回收
3、何时回收
内存分配策略:
1、优先分配到Eden
2、打对象直接分配到老年代
3、长期存活的对象分配到老年代
4、空间分配担保
5、动态对象的年龄判断
当JDK查看主机的运行环境内存大于2G,且为多核时,默认JDK是服务端(Server VM),当JDK所处的环境作为一个服务server运行时,默认的垃圾收集器就是parallel,如果作为客户端,默认的垃圾收集器就是serial收集器。
指定使用什么垃圾回收器: -verbose:gc -XX:+printGCDetails -XX:+UseSerialGC -Xms:20M -Xmx:20M -Xmn:10M(指定新生代内存)
大对象直接进入到老年代: -XX:PretenureSizeThreshold (设置大对象进入老年代的阈值)
长期存活的对象将进入老年代:-XX:MaxTenuringThreshold 15
空间分配担保: -XX:+HandlePromotionFailure ("+"代表开启空间担保,“-”代表禁用空间担保),检测能否容纳新生代对象的标准:老年代剩余的连续的内存空间是否大于历次进入老年代对象的平内存大小
逃逸分析与栈上分配:
通过逃逸分析找出没有发生逃逸的对象,然后就能够对这些对象进行栈上分配:
逃逸分析:分析对象的作用域,如果一个对象只在方法体内部有效,没有被外部引用,就认为这个对象没有发生逃逸,就可以分配到栈上去
注:在使用对象的时候能将对象放到方法体中,就不要对他进行外部的引用,可以提高程序的效率,因为方法体中的对象会被创建到栈中,当方法执行完后栈内存就会被自动的释放
package javaJVM; public class Test02 { public Test02 obj; /** * 方法返回StackAllocation对象,发生逃逸 * @return */ public Test02 getInstance() { return obj == null?new Test02():obj; } /** * 为成员属性赋值,发生逃逸 * */ public void setObj() { this.obj = new Test02(); } /** * 对象的作用域只在当前方法中有效,内有发生逃逸 * */ public void useStackAllocation() { Test02 t = new Test02(); } /** * 引用成员变量的值,发生逃逸 * */ public void useStackAllocation2() { Test02 t1 = getInstance(); } }
虚拟机工具的使用:
jps: 能够查看本地虚拟机的唯一ID,lvmid local virtual machine id
在window的cmd窗口,输入jps -m 查看运行时传入主类的参数
在window的cmd窗口,输入jps -v 查看虚拟机的具体运行参数,以及虚拟机的配置信息
Jstat: 类装载,内存,垃圾收集,jit编译的信息,监视虚拟机运行的各种状态信息,jstat是依赖于jps的,必须先通过jps获得具体进程的进程号,才能查看
使用jstat的命令: jstat -gcutil 线程号 毫秒值 次数
官方文档地址: http://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
注: 元空间的本质和永久代类似,都是对jvm规范中方法区的实现,不过元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制
jinfo: 实时查看和调整虚拟机的各项参数
jinfo -flag UseSerialGC 线程号 (查看使用了什么类型的垃圾回收器 “+”代表使用 “—”代表没有使用)
-XX:[+/-]option
-XX:option=value
jmap:
jmap -dump:format=b,file=c:a.bin 线程号
jhat: 可以用来分析jmap生成的文件,但是这个过程中对cpu的占有率非常的高
提供一个内置的http服务器用来分析数据,可以根据提供的端口号直接访问web页面,查看具体的类生成信息
在web界面的最下面,show heap histogram可以展示每个java实例占用内存的大小及生成的数量,可以用来进行程序分许
Execute Object Query Language (OQL) query :可以用查询sql的形式来进行查询对象
select s from java.lang.String s where a.value.length > 1000 --输入的sql如下
jstack:用于生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的就是分析线程长时间出现停顿的原因
在定时任务处理问题时,当程序正在运行时,是无法定位到线程出现卡顿的具体原因的,但是通过jstack这个工具可以查看到当前线程的一些信息,对于死锁、死循环等问题的查看。
可以分析出某个线程是出于停止状态还是正在等待状态。
在启动线程的时候给线程起一个名称,这样就能用虚拟机监控工具监控线程的具体执行情况
jConsole: 命令: JConsole
jConsole:内存监控
jConsole:线程监控
线程监控源码:
package ThreadMonitor; import java.util.Scanner; public class Test1 { public static void main(String[] args) { Scanner sc = new Scanner(System.in); sc.next(); new Thread(new Runnable() { @Override public void run() { while (true) { } } },"while true").start(); sc.next(); testwait(new Object()); System.out.println("Thread starting ...."); } private static void testwait(Object obj) { new Thread(new Runnable() { @Override public void run() { synchronized (obj) { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } },"wait").start(); } }
jConsole:死锁监控
死锁测试代码:
死锁类:
package ThreadMonitor; import java.security.KeyStore.PrivateKeyEntry; public class DeadLock implements Runnable { private Object obj1; private Object obj2; public DeadLock(Object obj1,Object obj2) { this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized (obj1) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized (obj2) { System.out.println("hello ....."); } } } }
测试类:
package ThreadMonitor; public class Main { public static void main(String[] args) { Object obj1 = new Object(); Object obj2 = new Object(); new Thread(new DeadLock(obj1, obj2)).start(); new Thread(new DeadLock(obj2, obj1)).start(); } }
虚拟机的可视化工具: VisualVM
插件地址: http://visualvm.github.io/pluginscenters.html
首页地址:http://visualvm.github.io/index.html
可以用来监控eclipse和jvm的运行具体信息
安装之后还需要下载安装相应的插件,在工具tools中就可以进行下载安装
虚拟机的性能调优:
1、知识 2、 工具 3、数据 4、经验
虚拟机性能调优案例:
案例1: 场景: 绩效考核系统,会针对每个考核员工,生成一个各考核点的考核结果,形成Excel文档,供用户下载。文档中包含用户提交的考核点信息以及分析结果。Excel文档用于在用户请求的时候生成,下载,并保存到内存服务器一份
环境: 2核 64G内存 CentOS Tomcat7.0 JDK1.7
问题: 经常有客户反映长时间出现卡顿的现象
处理思路:
·优化sql
·监控内存 :经常会发生FULL GC 而且每次发生full GC 都会经历很长的时间 20-30s
`问题出现原因分析:由于将大量的数据分装到一个对象中,生成了一个大对象,而大对象会直接的进入老年代,然后垃圾回收的时候触发Full GC,但由于堆内存设置的过大,导致老年代垃圾回收的时间漫长,系统产生卡顿。
·解决方案:减小堆内存大小的设置,但是这样会浪费物理空间。
最终解决方案: 部署多个web容器,每个web容器的堆内存指定为4G,搭建tomcat集群
总结经验:正常情况使用单个容器的效率要比使用多个容器的效率高,因为如果部署多个容器,每个容器部署的时候还要消耗相应的内存空间,从而使整体可以用来处理用户请求的内存空间减少,见底了系统的效率。但是对于上述情况,明显使用多个容器比使用单一容器更加高效。所以具体的情况,还要按照具体代码逻辑的设计去分析解决。
案例2: 场景:简单的数据抓取系统,抓取网站上的一些数据,分发到其他的应用。
环境:。。。。
出现问题:不定期出现内存溢出,把堆内存加大也无济于事。导出堆转储快照信息,没有任何信息。内存监控正常。
原因分析:系统内存太小,导致触发堆外内存的时候,将系统内存撑爆,出现了内存溢出的现象
处理思路:扩大系统内存
CLASS文件的结构: class文件是一组以8位字节为基础单位的二进制字节流,各个数据项目严格按照顺序紧凑的排列在class文件中,中间没有添加任何分割符,整个class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。
当遇到8位字节以上的空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
class文件中有两种数据类型,分别是无符号数和表。
class文件结构: ·魔数 ·class文件版本 ·常量池 ·访问标志 ·类索引,父类索引,接口索引集合 ·字段表集合 ·方法表集合 ·属性表集合
在cmd窗口的查询命令:
javap -verbose class文件名称 : 查看具体的编译信息
魔数后面四位数字表示jdk的版本号:jdk 1.8 =52 jdk1.7=51 jdk 1.6=50 jdk1.5 = 49 jdk 1.4 = 48
字段表:用于描述接口或者类中申明的变量
方法表:用于描述方法的概要信息,但是具体的方法体信息是存储在属性表中的
属性表:用于描述方法的属性信心。
字节码指令:java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字,称之为操作码,以及跟随其后的零至多个代表此操作所需参数的操作数而构成
操作码的长度为1个字节,因此最大只有256条
是基于栈的指令集架构
字节码指令和数据类型: 在虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。
大多数指令是包含类型信息的,也有不包含类型信息的:goto与类型无关,arraylength操作数组类型
类型多,指令少
加载与存储指令: 加载和存储指令用于将数据在栈帧中的局部变量和操作数之间来回传输
将局部变量表加载到操作数栈:iload lload fload dload aload
将一个数值从操作数栈存储到局部表量表: istore ifda
将一个常量加载到操作数栈: bipush sipush ldc ldc_w ldc2_w aconst_null iconst_m1 iconst
扩充局部变量表的访问索引指令:wide
运算指令: 运算或者算数指令用于对两个操作数栈上的值进行某种特定的运算,并把结果存储到操作数栈顶
加法指令:add 减法指令:sub 乘法指令:mul 除法指令:div 取余指令:rem 取反指令:neg
类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作以及用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。
宽化类型处理和窄化类型处理
对象创建与访问指令: 创建类实例的指令:new 创建数组的指令:newarray anewarray multianewarray
访问字段: getfield putfield getstatic putstatic
把数组元素加载到操作数栈的指令:baload csllfda
将操作数栈的值存储到数组元素:astore
取数组长度的指令:arraylength
检查实例类型的指令:instanceof checkcast
操作数栈管理指令:
操作数栈指令用于直接操作操作数栈。
将操作数栈的一个或两个元素出栈: pop pop2
复制栈顶一个或两个数值并将复制或双份复制值重新压入栈顶:dup dup2 dup_x1 dup_x2
将栈顶的两个数值替换:swap
控制转移指令:控制转移指令可以让java虚拟机有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。可以认为控制转移指令就是在修改pc寄存器的值。
条件分支:ifeq iflt ifle ifne ifgt ifnull ifcmple
复合条件分支:tablewitch lookupswitch
无条件分支:goto goto_w jsr jsr_w ret
方法调用指令: invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
invokestatic指令用于调用类方法(static方法)
方法的返回指令:方法的调用指令和数据类型无关,而方法的返回指令则根据返回值的类型进行区分的,包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、ireturn、freturn、dreturn、和areturn,另外还有一条return指令共申明void方法、实例初始化方法、类和接口的类初始化方法使用。
同步指令:java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管城(monitor)来支持的。
虚拟机的类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
加载类的策略: 懒加载 及时加载
类加载的时机: 加载-->连接-->初始化-->使用-->卸载
初始化:一、遇到new、getstatic、putstatic、或invokestatic这4条字节码指令的时候,如果类没有进行过初始化,则需要先触发其初始化。生成这四条指令最常见的java代码场景是:使用new关键字实例化对象的时候、读取或者设置一个类的静态字段的(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
二、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先出发其初始化。
三、当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
四、当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
不被初始化的例子:
通过子类引用父类的静态字段 ,子类不会被初始化
通过数组定义来引用类
调用类的常量
类加载的过程: 加载-->验证-->准备-->解析-->初始化
加载: 通过一个类的全限定名来获取定义此类的二进制流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的class对象,作为这个类的各种数据的访问入口
验证: 验证是连接的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
文件的格式验证 、 元数据验证 、 字节码验证、 符号引用验证
准备: 准备阶段正式为类变量分配内存并设置变量的初始值。这些变量使用的内存都将在方法区中进行分配。
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
类或者接口的解析 ·字段解析 ·类方法解析 ·接口方法解析
注: <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的的变量,定义在他之后的变量,在前面的语句块中可以被赋值,但是不能被访问。
如果多个线程同时初始化一个类,只有一个线程会执行这个类的<clinit>()方法,其他线程等待执行完毕。如果方法的执行时间过长,那么就会在成多个线程的阻塞。
类加载器: 虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己去决定如何去获取所需要的类。实现这个动作的代码块称作为类加载器。
只有被同一个类加载器加载的类才可能会相等,相同的字节码被不同的类加载器加载的类不相等。
package ClassLoader; import java.io.IOException; import java.io.InputStream; public class ClassLoaderDemo { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException { ClassLoader mycl = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { String fileName = name.substring(name.lastIndexOf(".")+1)+".class"; InputStream ins = getClass().getResourceAsStream(fileName); if(ins ==null) { return super.loadClass(name); } try { byte[] buff = new byte[ins.available()]; ins.read(buff); return defineClass(name, buff, 0,buff.length); } catch (Exception e) { throw new ClassNotFoundException(); } } }; Object c = mycl.loadClass("ClassLoader.ClassLoaderDemo").newInstance(); System.out.println(c.getClass()); System.out.println(c instanceof ClassLoaderDemo); } }
类加载器的分类:
启动类加载器 : 由c++实现,是虚拟机的一部分,用于加载javahome下的lib目录下的类
扩展类加载器: 加载javahome下/lib/ext目录中的类
应用程序类加载器: 加载用户类路径上的所制定的类库
自定义类加载器
多种类加载器之间协同工作的模式:双亲委派模式
从jdk1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(parentsDelagation Modle)进行类加载,其加载过程如下:
(1)如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成
(2)每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器
(3)如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起请求类加载请求的类加载器也无法完成加载请求的时候,将会抛出classNotFountException,而不再调用其子类加载器去进行类加载
双亲委派模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是接触的类,越是被上层的类加载器加载,保证了程序运行的稳定性。
局部变量在使用过程中必须先给他赋一个初始值,否则是不能直接去使用的。
slot: 当一个变量的pc寄存器的值大于slot的作用域的时候,slot是可以进行复用的
方法的调用---静态分派:
静态分派: 在编译阶段就可以进行确定对方法的调用
package ClassLoader; import org.omg.Messaging.SyncScopeHelper; public class Test1 { static class Parent {}; static class Child1 extends Parent { } static class Child2 extends Parent{} public void sayhello(Child1 c) { System.out.println("c1 is call"); } public void sayhello(Child2 c) { System.out.println("c2 is call"); } public void sayhello(Parent p) { System.out.println("p is call"); } public static void main(String[] args) { Parent p1 = new Child1();//p1称为变量的静态类型,Child1称为变量的实际类型 Parent p2 = new Child2(); //方法的调用通过静态类型进行选择的过程称为静态调用 Test1 t = new Test1(); t.sayhello(p1); t.sayhello(p2); //实际类型发生变化 Parent p = new Child1(); p = new Child2(); //静态类型发生变化 t.sayhello((Child1)p1); } }
在方法的执行过程中会选择一个最先匹配的方法进行方法的调用
package ClassLoader; public class Test2 { /*public void sayhello(int a) { System.out.println("int"); }*/ /*public void sayhello(double a) { System.out.println("double"); }*/ /*public void sayhello(char a) { System.out.println("char"); }*/ /*public void sayhello(char ... a) { System.out.println("char ..."); } */ /*public void sayhello(long a) { System.out.println("long"); }*/ public void sayhello(Object a) { System.out.println("object"); } public void sayhello(boolean a) { System.out.println("boolecn"); } public static void main(String[] args) { //在方法调用中会选择一个最先匹配的进行调用 new Test2().sayhello('a'); } }
动态分派调用: 找到操作数栈顶的第一个元素所指向的对象的实际类型
如果在实际类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则直接返回这个方法的直接引用,查找过程结束,如果不通过,抛出异常。
按照继承关系从上往下依次对实际类型的个父类进行搜索与验证
如果始终没有找到,则抛出AbstractMethordError
注: 方法的静态调用主要是针对方法的重载,动态调用主要是针对方法的重写 。
动态类型语言支持:
静态类型的语言在非运行阶段,变量的类型是可以确定的。也就是说变量是由类型的
动态类型的语言在非运行阶段,变量的类型是无法进行确定的,也就是说变量是没有类型的,但是值是由类型的,也就是运行期间可以确定变量的值得类型