Program Counter Register程序计数器(寄存器)
Java Virtual Machine Stacks(java虚拟机栈)
2、栈内存溢出
(1)演示栈内存溢出场景,方法递归调用
/**
* 演示栈内存溢出
*/
public class Demo1 {
private static int count = 0;
public static void main(String[] args) {
try{
//方法入口调用
addCount();
}catch (Throwable e){
e.printStackTrace();
System.out.println(count);
}
}
/**
* 方法递归调用,让其栈内存溢出
*/
private static void addCount(){
count++;
addCount();
}
}
(2)如何设置栈内存的大小
(2)演示json格式转换栈内存溢出
/**
* 部门对象
*/
public class Dept {
//部门名称
private String name;
//员工集合
private List<Emp> empList;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmpList() {
return empList;
}
public void setEmpList(List<Emp> empList) {
this.empList = empList;
}
@Override
public String toString() {
return "Dept{" +
"name='" + name + '\'' +
", empList=" + empList +
'}';
}
}
/**
* 员工对象
*/
public class Emp {
//员工姓名
private String name;
//部门对象
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
@Override
public String toString() {
return "Emp{" +
"name='" + name + '\'' +
", dept=" + dept +
'}';
}
}
public class Main {
public static void main(String[] args) throws JsonProcessingException {
Dept dept = new Dept();
dept.setName("Market");
Emp emp1 = new Emp();
emp1.setName("张山");
emp1.setDept(dept);
Emp emp2 = new Emp();
emp2.setName("李四");
emp2.setDept(dept);
dept.setEmpList(Arrays.asList(emp1,emp2));
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(dept));
}
}
问题定位:
{name :'Marker' , empList:[ {name:'张三'},dept:{name: 'Market',empList:[{name:'张三'},dept:{name:'张三',empList:[...]}]}]}
Dept对象中有Emp的List集合,每个Emp中又持有Dept对象,Dept对象中又持有Emp的List集合,无限循环导致方法调用栈内存溢出。
//在员工实体中把Dept对象加入@JsonIgnore,JsonIgnore是在json序列化时将pojo类中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
@JsonIgnore
private Dept dept;
3、线程运行诊断
案例:cpu占用过多
定位:
本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
1、堆简介
2、堆内存溢出
(1)示例代码
/**
* 演示堆内存溢出
*/
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try{
List<String> list = new ArrayList<>();
String a = "hello";
while(true){
list.add(a);
a = a + a;
i++;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(i);
}
}
}
(2)分析原因
3、堆内存诊断
(1)jps工具
(2)jmap工具
(3)jconsole工具
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10];
System.out.println("2...");
Thread.sleep(30000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(10000000L);
}
}
查看但钱进程使用的堆内存情况:jmap -heap 进程号
使用jconsole图形化界面分析
使用jvisualvm图形化界面分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-
1、方法区简介
方法区(Method Area)也是所有线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。也称作Non-Heap非堆内存。
Java虚拟机规范堆方法区的限制非常宽松,可以选择不实现垃圾收集,但是这部分区域的回收确实是有必要的。
平时,说到永久带(PermGen space)的时候往往将其和方法区不加区别。这么理解在一定角度也说的过去。因为,《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。
同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。
在JDK1.8及以后版本,永久带被移除,新出现的元空间(Metaspace)替代了它。元空间属于Native Memory Space
2、方法区内存溢出
public class Demo4 extends ClassLoader{ //继承ClassLoader可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
Demo4 demo4 = new Demo4();
try{
for (int i = 0; i < 100000; i++,j++) {
//ClassWriter作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
//版本号,public,类名,父类,接口
cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
//返回byte[]
byte[] code = cw.toByteArray();
//执行了类的加载
demo4.defineClass("Class"+i,code,0,code.length); //Class对象
}
}finally {
System.out.println(j);
}
}
}
(1)1.8以前会导致永久代内存溢出
演示永久代内存溢出:java.lang.OutOfMemoryError: PermGen space
设置堆内存: -XX:MaxPermSize=8m
(2)1.8以后会导致元空间内存溢出
演示元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
设置堆内存: -XX:MaxMetaspaceSize=8m
注意:元空间依赖于系统内存的大小,所以一般很难演示出元空间溢出
3、常量池
//二进制字节码存放的有 :类的基本信息、常量池、类方法定义、也包含了虚拟机指令
public class Demo5 {
public static void main(String[] args) {
System.out.println("I am LiXiang");
}
}
(1)类的基本信息
(2)常量池
(3)类方法定义、虚拟机指令
(4)java程序编译成字节码文件的执行过程
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
获取一个静态变量,对应常量池的 #2步骤
再继续寻找常量池的#21、#22步骤
再继续寻找常量池的#28、#29、30步骤
3: ldc #3 // String I am LiXiang
读取字符串,对应常量池#3,#23
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
方法执行,调用#4、#24、#25、#31、#32、#33
4、运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
1、常量池与串池的关系
字符串池在JDK1.7之后存在于堆中的一块区域,String s1 = "abc"
这样声明的字符串会放入字符串池中,String s1 = new String("abcd")
会在字符串池有一个"abcd"的字符串对象,堆中也有1个,2个不同。
示例代码:
public class Demo6 {
//串池是HashTable结构,不能扩容,串池中相同元素只会被添加一次
//常量池中的信息,都会被加载到运行时常量池中,这时a b ab 都是常量池的符号,还没有变为java字符串对象
//ldc #2 会把a符号变为"a"字符串对象,并且放入StringTable串池 StringTable["a"]
//ldc #3 会把b符号变为"b"字符串对象,并且放入StringTable串池 StringTable["a","b"]
//ldc #4 会把ab符号变为"ab"字符串对象,并且放入StringTable串池 StringTable["a","b","ab"]
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1+s2;
}
}
2、字符串变量拼接
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1+s2;
System.out.println(s3 == s4);
}
3、编译器优化
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = "a"+"b";
System.out.println(s3 == s4);
}
4、StringTable特性
5、intern()方法将对象放入串池
public class Demo8 {
//["a","b"]
public static void main(String[] args) { //1983
String s = new String("a") + new String("b"); //new String("ab")
//堆 new String("a") new String("b") new String("ab")
String s2 = s.intern();//将这个字符串尝试放入串池,如果有则并不会被放入,如果没有则放入串池。会把串池中的对象返回
//注意:intern()方法在1.6版本中是将堆中的数据拷贝一份,所以再用s取比较的时候就会出现false
//s2与串池中的ab是相等的
System.out.println("s2 == ab"+s2 == "ab");
//同样s被放入串池中,所以与串池中的ab也是相等的
System.out.println("s == ab"+s == "ab");
}
}
public class Demo9 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a"+"b";
String s4 = s1+s2;
String s5 = "ab";
String s6 = s4.intern();
System.out.println(s3 == s4); //false,s4 = new String("ab")存放在堆中,s3存放在串池中
System.out.println(s3 == s5); //true,s3存放在串池中,s5直接拿串池中的数据
System.out.println(s3 == s6); //1.8中为true,1.6为false,因为1.6复制一份副本存放在串池中
}
}
public class Demo9 {
public static void main(String[] args) {
String x2 = new String("c") + new String("d");
x2.intern();
String x1 = "cd";
System.out.println(x1 == x2); //true,因为x2堆中的对象已经放入串池中,s1为串池中对象
}
}
public class Demo9 {
public static void main(String[] args) {
String x1 = "cd";
String x2 = new String("c") + new String("d");
x2.intern();
System.out.println(x1 == x2); //false
}
}
6、StringTable的位置
StringTable在1.6时存放在永久代中,在1.8中存放在堆中
7、StringTable垃圾回收
字符串常量也会触发垃圾回收机制
8、StringTable调优
通过调整StringTable中桶的个数来提高读取效率。默认是60013个。
调整:-XX:StringTableSize=桶个数
桶的个数调大会明显提升读取速度。
1、直接内存简介
Direct Memory
2、直接内存,内存溢出问题
3、直接内存释放原理
(1)演示直接内存释放过程
(2)直接内存释放原理
Java提供了Unsafe类用来进行直接内存的分配与释放
Unsafe无法直接使用,需要通过反射来获取
(3)分析ByteBuffer.allocateDirect()怎末进行直接内存的创建与释放的
4、禁用显示回收堆直接内存的影响
关闭显示调用GC,会导致直接内存无法释放的问题,我们可以通过Unsafe来释放内存。
1、对象创建的流程
虚拟机遇到一条new指令时,首先检查这个对应的类能否在常量池中定位到一个类的符号引用。
判断这个类是否已被加载、解析和初始化。
为这个新生对象在Java堆中分配内存空间,其中Java堆分配内存空间的方式主要有以下两种
指针碰撞
空闲列表
将分配到的内存空间都初始化为零值
设置对象头相关数据
执行对象方法
图解
2、指针碰撞和空闲列表图解
当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作栈上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用是第二种):
1、使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针。
2、直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。
对比
垃圾回收分析:句柄方式访问对象当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址,直接指针访问对象垃圾回收时需要修改reference中存储的地址。
访问效率分析,直接指针访问对象优于句柄方式访问对象,因为直接指针访问对象只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。
1、什么是垃圾(Garbage)
2、为什么需要GC
对于高级语言来说,不进行垃圾回收,内存迟早会被消耗完。
释放没用的独享,垃圾回收也可以清除内存里的记录碎片,碎片整理将所有占用的堆内存放到堆的一段,以便于JVM将整理出的内存分配给的新的对象。
随着应付业务越来越庞大、复杂、用户越来越多,没有GC就不能保证应用程序的正常进行,经常造成STW的GC又跟不上实际的需求,所以才会不短地尝试对GC进行优化。
3、Java垃圾回收机制
自动内存管理,降低内存泄露和内存溢出的风险
自动内存管理,减轻了Java程序员的内存管理负担,可以更专注于业务的开发
1、引用计数法概念
2、什么是引用计数算法
3、那为什么主流的Java虚拟机里面都没有选用这种算法呢?
其中最主要的原因是它很难解决对象之间相互循环引用的问题。
1、可达性分析算法简介
可达性分析算法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为回收对象,要至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。
2、可作为GC Roots的对象
(1)虚拟机栈(栈帧中的本地变量表)中引用的对象。(引用栈帧中的本地变量表的所有对象)
(2)方法区中静态属性引用的对象(引用方法区该静态属性的所有对象)
(3)方法区中常量引用的对象(引用方法区中常量的所有对象)
(4)本地方法栈中(Native方法)引用的对象(引用Native方法的所有对象)
1、什么是引用
//创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
String s;
String str = new String("abc");
System.out.println(str.toString());
2、强引用
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动设置成null
强引用只能够通过GC Root的引用链找到就不会被回收,也就是说强引用只有当GC Roots全部断开时才会被回收。
案例:设置JVM参数,起始堆内存为2m:-Xms2m 最大堆内存为3m:-Xmx3m
public class Demo13 {
public static void main(String[] args) {
testStrongReference();
}
private static void testStrongReference(){
//当new byte为1M 的时候,程序运行正常
byte [] buff = new byte[1024 * 1024 *1];
}
}
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OOM(OutOfMemoryError),不回去回收。如果想中断强引用与对象之间的关系,可以显示的将强引用赋值成null,这样,JVM就可以在适当的时刻回收对象了。
3、软引用
软引用是用来描述一些非必需但是仍有用的对象。**在内存足够的时候,软引用对象不会被回收,只有在内存不足的时候,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出OOM的异常。**这种特性一般被用来实现缓存技术。
在JDK1.2之后,用java.lang.ref.SoftReference类来表示软引用。
案例:创建多个对象,软引用指向,不会出现内存溢出,当内存不足时,会自动的回收掉
public class Demo13 {
private static List<Object> list = new ArrayList<>();
public static void main(String[] args) {
testStrongReference();
}
private static void testStrongReference(){
for (int i = 0; i < 10; i++) {
byte [] buff = new byte[1024 * 1024];
//指向软引用
SoftReference<byte[]> sr = new SoftReference<>(buff);
list.add(sr);
}
System.gc(); //显示调用gc
for (int i = 0; i < list.size(); i++) {
Object obj = ((SoftReference) list.get(i)).get();
System.out.println(obj);
}
}
}
案例:软引用配合引用队列使用,当内存不足时,会自动的回收掉
public class Demo14 {
private static final int _1GB = 1024*1024;
public static void main(String[] args) {
List<WeakReference<byte[]>> list = new ArrayList<>();
//引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
for (int i = 0; i < 5; i++) {
//关联了引用队列,当软引用所关联的byte数组被回收时,软引用自己会加入到引用队列中
WeakReference<byte[]> reference = new WeakReference<>(new byte[_1GB],queue);
System.out.println(reference.get());
list.add(reference);
System.out.println(list.size());
}
//从队列获取无用的软引用对象并移除
Reference<? extends byte[]> poll = queue.poll();
while (poll!=null){
list.remove(poll);
poll=queue.poll();
}
System.out.println("循环结束");
for (WeakReference<byte[]> reference : list) {
System.out.println(reference.get());
}
}
}
这里就说明了在内存不足的情况下,软引用将会被自动回收。
但是值的注意的一点,即使有byte[] buff引用指向对象,且buff是一个strong reference,但是SoftReference sr指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中,buff已经没有被引用了,所有自动进行了优化。
4、弱引用
弱引用的引用强度比软引用更弱一些,**无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收。**在JDK1.2之后,用java.lang.ref.WeakReference来表示弱引用。
案例:测试弱引用,无论内存是否充足,都会被回收掉
public class Demo13 {
private static List<Object> list = new ArrayList<>();
public static void main(String[] args) {
testStrongReference();
}
private static void testStrongReference(){
for (int i = 0; i < 10; i++) {
byte [] buff = new byte[1024 * 1024];
//指向软引用
WeakReference<byte[]> str = new WeakReference<>(buff);
list.add(str);
}
System.gc(); //显示调用gc
for (int i = 0; i < list.size(); i++) {
Object obj = ((WeakReference) list.get(i)).get();
System.out.println(obj);
}
}
}
5、虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能被回收,在JDK1.2之后,用PhantomReference类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个get()方法,而且它的get()方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和ReferenceQueue引用对列一起使用。
例如:创建ByteBuffer的时候会创建一个名为Cleaner的虚引用对象,当ByteBuffer没有被强引用所引用就会被jvm垃圾回收,虚引用Cleaner就会被放入引用队列,会有专门的线程扫描引用队列,被发现后会调用直接内存地址的方法将直接内存释放掉,保证直接内存不会导致内存泄露
public class PhantomReference<T> extends Reference<T> {
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* null
.
*
* @return null
*/
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
6、终结器引用
创建的时候会关联一个引用队列,当对象没有被强引用所引用时,对象被垃圾回收时,会将终结器引用放入到一个引用队列中(被引用的对象暂时还没有被垃圾回收),有专门的线程(优先级较低,可能会造成对象迟迟不被回收)扫描引用队列并调用finallize()方法,第二次GC的时候才能回收掉被引用的对象。
7、引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现他还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。
8、Reference对象状态变更
1、清除标记算法
定义:Mark Sweep
标记清除算法会把不在GC Root链上的引用进行标记,然后清除,它的速度非常的快,但是有一个问题,它清除后的空间没有进行整理,会造成内存碎片,比如说我清理了一个2mb、一个3mb、一个5mb的内存对象,但是现在有一个对象需要8mb,由于没有进行内存整理,这个8mb的对象不能引用刚刚三个任何一个,所以只能等待,而且效率上不是很高。
2、清除整理算法
定义:Mark Compact
标记整理算法和标记清除算法差不多,只不过在清除标记出的内存空间后,会进行整理,所以相对的效率会低一点,但是不会产生内存碎片,上面说到的8mb的内存就可以申请到,将2mb、3mb、5mb整理成10mb的内存空间。
3、复制算法
定义:Copy
复制算法分为两块相等的内存空间,from存放对象,to为空内存空间,当标记出需要回收的地址空间时,会把当前在GC Root链上的对象放到to内存空间上,将from上的无用的地址空间全部删除,然后再to与from空间进行对调,这样做的好处是不会产生内存碎片,但是需要占用双倍的内存空间。
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx或-XX:MaxHeapSize=size |
新生代大小 | -Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC前MinorGC | -XX:+ScavengeBeforeFullGC |
1、JVM相关参数
(1)-Xms:初始堆大小
(2)-Xmx:最大堆大小
(3)-Xmn:年轻代大小
(4)-XX:NewSize:设置年轻代大小
(5)-XX:MaxNewSize:年轻代最大值
(6)-XX:PermSize:设置持久代初始值
(7)-XX:MaxPermSize:设置持久代最大值
(8)-Xss:每个线程的堆栈大小
(9)-XX:ThreadStackSize:Thread Stack Size
(10)-XX:NewRatio:年轻代(包括Eden和两个Survivor区)与老年代的比值
(11)-XX:SurvivorRatio:Eden区与Survivor区的大小比值
(12)-XX:LargePageSizeInBytes:内存页的大小不可设置过大, 会影响Perm的大小
(13)-XX:+DisableExplicitGC:关闭System.gc()
(14)-XX:+AggressiveOpts:加快编译
(15)-XX:+UseBiasedLocking:锁机制的性能改善
(16)-Xnoclassgc:禁用垃圾回收
(17)-XX:SoftRefLRUPolicyMSPerMB:每兆堆空闲空间中SoftReference的存活时间
(18)-XX:PretenureSizeThreshold:对象超过多大是直接在老年代分配
(19)-XX:+CollectGen0First:FullGC时是否先YGC
2、并行收集器相关参数
(1)-XX:+UseParallelGC:Full GC采用parallel MSC (此项待验证)
(2)-XX:+UseParNewGC:设置年轻代为并行收集
(3)-XX:ParallelGCThreads:并行收集器的线程数
(4)-XX:+UseParallelOldGC:年老代垃圾收集方式为并行收集(Parallel Compacting)
(5)-XX:MaxGCPauseMillis:每次年轻代垃圾回收的最长时间(最大暂停时间)
(6)-XX:+UseAdaptiveSizePolicy:自动选择年轻代区大小和相应的Survivor区比例
(7)-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比
(8)-XX:+ScavengeBeforeFullGC:Full GC前调用YGC
3、CMS相关参数
(1)-XX:+UseConcMarkSweepGC:使用CMS内存收集
(2)-XX:+AggressiveHeap
(3)-XX:CMSFullGCsBeforeCompaction:多少次后进行内存压缩
(4)-XX:+CMSParallelRemarkEnabled:降低标记停顿
(5)-XX+UseCMSCompactAtFullCollection:在FULL GC的时候, 对年老代的压缩
(6)-XX:+UseCMSInitiatingOccupancyOnly:使用手动定义初始化定义开始CMS收集
(7)-XX:CMSInitiatingOccupancyFraction=70:使用cms作为垃圾回收 使用70%后开始CMS收集
(8)-XX:CMSInitiatingPermOccupancyFraction:设置Perm Gen使用到达多少比率时触发
(9)-XX:+CMSIncrementalMode:设置为增量模式
4、辅助信息
(1)-XX:+PrintGC
(2)-XX:+PrintGCDetails
(3)-XX:+PrintGC:PrintGCTimeStamps
(4)-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间.可与上面混合使用
(5)-XX:+PrintHeapAtGC:打印GC前后的详细堆栈信息
(6)XX:+PrintTenuringDistribution:查看每次minor GC后新的存活周期的阈值
测试代码:添加JVM参数 -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public class Demo16 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
}
}
public class Demo16 {
//测试Minor GC回收,当添加的数据在伊甸园的空间范围内
private static final int _512KB = 512 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
}
public class Demo16 {
//当添加的数据超过新生代的总容量时或对象达到年龄阈值,老年代空间足够的情况下,会向老年代中存放
private static final int _512KB = 512 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);
}
}
public class Demo16 {
//当添加的数据大于新生代的整体容量时,老年代空间足够的情况下,会直接存放在老年代,不会触发Minor GC
private static final int _8MB = 8 * 1024 * 1024;
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
}
1、串行(Serial/Serial Old垃圾回收器)
单线程
堆内存较小,适合个人电脑
指定JVM参数:-XX:+UseSerialGC = Serial + SerialOld
图解
2、ParNew 收集器
parnew垃圾收集器的特点?
并发与并行的概念讲解 CMS垃圾回收器
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能
会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上
3、吞吐量优先(Parallel Scavenge垃圾收集器)
4、响应时间优先(CMS垃圾回收器)
1、G1垃圾回收阶段
定义:Garbage First
适用场景
相关JVM参数
2、Young Collection
3、Young Collection + CM
4、Mixed Collection
5、G1收集器的运作大致可分为以下几个步骤
1、什么是跨代引用
跨代引用是指新生代中存在对老年代对象的引用,或者老年代中存在对新生代的引用。
2、跨代引用的问题
YGC时,为了找到年轻代中存活的对象,不得不遍历整个老年代,反之亦然。这种方案存在在极大的性能浪费。因为跨代引用是极少的,为了找出那么一丁点跨代引用,却遍历了整个老年代。
记忆集就是用来记录跨代引用的表,通过引入记忆集避免遍历老年代。以YGC为例说明,要回收年轻代,只需要引用年轻代对象的GC Root+记忆集,就可以判断出Young区对象是否存活,不必在遍历老年代。
存在滞后性,浪费一定的空间,如上图所示,YGC时实际上无引用对象实际是可以被回收的,但是由于老年代中被引用,所以无法被回收。
工作流程上来看,CMS的重新标记,和G1的最终标记之前都是并发标记。
既然时同时运行,用户程序就可能修改对象的引用关系,修改对象引用关系就可能影响GC回收。
所以,CMS重新标记,G1最终标记都是为了解决一件事,那就是并发过程中用户程序修改了对象引用关系后,如何让GC收集器仍旧能正确回收垃圾对象的问题。
1、JDK 8u20字符串去重
2、JDK 8u40并发标记类卸载
所有对象都经过并发标记后,就能直到哪些类不在被使用,当一个类加载器的所有类都不在使用,则卸载它所加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark 默认启用
3、JDK 8u60回收巨型对象
4、JDK9并发标记起始时间的调整
1、查看虚拟机运行参数
java -XX:PrintFlagsFinal -version | findstr "GC"
2、回收器选择问题
【低延迟】还是【高吞吐量】,选择合适的回收器
3、垃圾回收频繁的问题分析?
查看Full GC前后的内存占用,考虑下面几个问题
1、新生代的特点
2、调优参数
晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold :最大晋升值
-XX:+PrintTenuringDistribution :晋升的日志
以CMS为例
CMS的老年代内存越大越好
先尝试不做调优,如果没有Full GC那么就无需调优,否则现场时调优新生代
观察发生FullGC时老年代的占比,将老年代内存预设调大1/4 ~ 1/3
1、Full GC和Minor GC频繁
如果GC发生频繁,说明空间紧张,如果是新生代的空间紧张,当业务高峰期来了,大量对象被创建,很快就会把新生代的空间塞满,塞满之后还会造成一个问题,幸存区空间紧张,导致对象的晋升阈值减小,导致一些周期很短的对象也会被晋生到老年代,老年代存了大量的生命周期少的对象,发生Full GC。
增大新生代内存,新生代内存充裕后,就不会频繁发生GC,这样老年代也不会存储大量周期短的对象。
2、请求高峰期发生Full GC,单次暂停时间特别长(CMS)
CMS,查看GC日志,CMS初始标记和并发标记都是比较快的,耗时主要发生在重新标记上,重新标记不但会扫描老年代的和新生代的垃圾,可以在重新标记之前进行新生代的内存。使用参数-XX:+CMSScavengeBeforeRemark设置重新标记之前进行新生代的一次GC回收。
3、老年代充裕的情况下,发生Full GC(CMS jdk1.7)
在1.8版本之前永久代的空间不足,也会产生Full GC。
1、JVM规范下类文件结构
2、二进制字节码文件
//Hello World 示例
public class Demo16 {
public static void main(String[] args) {
System.out.println("hello world");
}
}
od -t -xC Demo16.class
0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09
0000020 00 15 00 16 08 00 17 0a 00 18 00 19 07 00 1a 07
0000040 00 1b 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
0000140 00 04 74 68 69 73 01 00 14 4c 63 6f 6d 2f 6c 69
0000160 78 69 61 6e 67 2f 44 65 6d 6f 31 36 3b 01 00 04
0000200 6d 61 69 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c
0000220 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 01 00 04
0000240 61 72 67 73 01 00 13 5b 4c 6a 61 76 61 2f 6c 61
0000260 6e 67 2f 53 74 72 69 6e 67 3b 01 00 0a 53 6f 75
0000300 72 63 65 46 69 6c 65 01 00 0b 44 65 6d 6f 31 36
0000320 2e 6a 61 76 61 0c 00 07 00 08 07 00 1c 0c 00 1d
0000340 00 1e 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
0000360 07 00 1f 0c 00 20 00 21 01 00 12 63 6f 6d 2f 6c
0000400 69 78 69 61 6e 67 2f 44 65 6d 6f 31 36 01 00 10
0000420 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74
0000440 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73
0000460 74 65 6d 01 00 03 6f 75 74 01 00 15 4c 6a 61 76
0000500 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
0000520 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e
0000540 74 53 74 72 65 61 6d 01 00 07 70 72 69 6e 74 6c
0000560 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f
0000600 53 74 72 69 6e 67 3b 29 56 00 21 00 05 00 06 00
0000620 00 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00
0000640 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1
0000660 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 04
0000700 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d
0000720 00 00 00 09 00 0e 00 0f 00 01 00 09 00 00 00 37
0000740 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04
0000760 b1 00 00 00 02 00 0a 00 00 00 0a 00 02 00 00 00
0001000 06 00 08 00 07 00 0b 00 00 00 0c 00 01 00 00 00
0001020 09 00 10 00 11 00 00 00 01 00 12 00 00 00 02 00
0001040 13
0001041
1、字节码指令入门
研究两组字节码指令
(1)public cn.itcast.jvm.t5.HelloWord();构造方法的字节码指令
2a b7 00 01 b1
(2)public static void main(java.lang.String[]);之方法的字节码指令
b2 00 02 12 03 b6 00 04 b1
1、原始Java代码
public class Demo17 {
/**
* 演示字节码指令和操作数栈、常量池的关系
* @param args
*/
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE+1;
int c = a + b;
System.out.println(c);
}
}
2、编译后的字节码文件
javap -v Demo17.class
Classfile /D:/ideaworkspace/jvm-demo/target/classes/com/lixiang/Demo17.class
Last modified 2021-12-10; size 604 bytes
MD5 checksum e36477501751b13feb4bebbef30bfa7b
Compiled from "Demo17.java"
public class com.lixiang.Demo17
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // com/lixiang/Demo17
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/lixiang/Demo17;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 Demo17.java
#25 = NameAndType #8:#9 // "":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 com/lixiang/Demo17
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public com.lixiang.Demo17();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1(栈的深度), locals=1(局部变量表的长度), args_size=1(参数的长度)
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/lixiang/Demo17;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
line 13: 10
line 14: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "Demo17.java"
3、图解方法执行流程
(1)常量池载入运行时常量池
Java虚拟机的类加载器,会把main方法所在的类进行类加载操作,会把类的字节码相关信息放入内存中,同时把常量池中的信息放入运行时常量池中,后续查找都在运行时常量池中。一些比较小的值,不会直接放到常量池中,而是存放在字节码指令中。常量池也属于方法区,只不过这里单独提出来了。
(2)方法字节码载入方法区,main线程开始运行,分配栈帧内存
(stack=2,locals=4),两个操作数栈,局部变量表中有四个槽位。
(3)执行引擎开始执行字节码
bipush 10:将10这个数值,压入栈顶,执行引擎执行bipush 10
istore 1:将操作数栈中的元素弹出,放入局部变量表slot 1中,对应的代码a = 10
ldc #3:读取运行时常量池中#3位置,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中
注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的
istore 2:将操作数栈中的元素弹出,放到局部变量表的2号位置
iload1 iload2:将局部变量表中1号位置和2号位置的元素放入操作数栈中,因为只能在操作数栈中进行运算操作
istore 3:将操作数栈中的元素弹出,放入局部变量表的3号位置
getstatic #4:在运行时常量池中找到#4,发现是一个对象,在堆内找到该对象,将其引用放入操作数栈中。
iload 3:将局部变量表中3号位置的元素压入操作数栈中
invokevirtual 5:找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法,生成新的栈帧(分配 locals、stack等)
传递参数,执行新栈帧中的字节码。
执行完毕,弹出栈帧
清除 main 操作数栈内容
return
完成 main 方法调用,弹出 main 栈帧,程序结束
代码:
public class Demo2 {
public static void main(String[] args) {
int i=0;
int x=0;
while(i<10) {
x = x++;
i++;
}
System.out.println(x); //接过为0
}
}
字节码解析:
Code:
stack=2, locals=3, args_size=1 //操作数栈分为两个空间,局部变量表分为3个空间,参数的长度为1
0: iconst_0 //准备一个常数0
1: istore_1 //将常数0放在局部变量表第一个槽位上
2: iconst_0 //准备一个常数0
3: istore_2 //将常数0放在局部变量表第二个槽位上
4: iload_1 //将局部变量表中1号槽位的数据放入操作数栈中
5: bipush 10 //将10 放入操作数栈中,此时操作数栈中有10 和 0两个数
7: if_icmpge 21 //比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到21步
10: iload_2 //将局部变量表中2号槽位的数据放入操作数栈中,放入的数值为0
11: iinc 2, 1 //局部变量表中2号槽位的数加1,自增后,槽位中的数值为1
14: istore_2 //将操作数组栈中的数放入到局部变量表中的2号槽位,2号槽位有变成了0
15: iinc 1, 1 //1号槽位进行+1,1号槽位的数值为1
18: goto 4 //跳转到第四条指令
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_2
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: return
1、
public class Demo3 {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
public static void main(String[] args) {
System.out.println(i); //结果为30
}
}
编译器会按从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法cinit()V:
stack=1, locals=0, args_size=0
0: bipush 10 //将变量10从字节码中压入操作数栈
2: putstatic #3 // Field i:I
5: bipush 20 //将变量20从字节码中压入操作数栈
7: putstatic #3 // Field i:I
10: bipush 30 //将变量30从字节码中压入操作数栈
12: putstatic #3 // Field i:I
15: return //最后返回,以最后一次的取值为准
2、
public class Demo4 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo4(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo4 d = new Demo4("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从上至下的顺序,收集所有{}(实例块)和成员变量复制的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
//原始构造方法在最后执行
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
public class Demo5 {
public Demo5() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Demo5 demo5 = new Demo5();
demo5.test1();
demo5.test2();
demo5.test3();
Demo5.test4();
}
}
Code:
stack=2, locals=2, args_size=1
0: new #2 // class com/nyima/JVM/day5/Demo5
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: invokestatic #7 // Method test4:()V
23: return
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令。
在执行invokevirtual指令时,经历了以下步骤:
1、try-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
Code:
stack=1, locals=3, args_size=1
0: iconst_0 //准备一个常数0
1: istore_1 //将常数0放在局部变量表的1号槽位上
2: bipush 10 //从常量池中拿取10
4: istore_1 //将常数10放在局部变量表的1号槽位上
5: goto 12 //如果不发生异常跳到12步
8: astore_2 //出现异常调到异常表
9: bipush 20 //从常量池中拿取20
11: istore_1 //将常数20放在局部变量表的1号槽位上
12: return
//多出来一个异常表
Exception table:
from to target type
2 5 8 Class java/lang/Exception
2、多个single-catch
public class Demo1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (ArithmeticException e) {
i = 20;
}catch (Exception e) {
i = 30;
}
}
}
Code:
stack=1, locals=3, args_size=1
0: iconst_0 //准备一个常量0
1: istore_1 //将常量0放入本地变量表的1号槽位上
2: bipush 10 //从常量池中获取10这个变量
4: istore_1 //将常量10放入本地变量表中的1号槽位中
5: goto 19 //如果没有发生异常就跳转的19行
8: astore_2 //发生异常匹配异常表中target为8的异常
9: bipush 20 //从常量池中拿出20
11: istore_1 //放入本地变量表中的1号槽位
12: goto 19 //跳转到19号
15: astore_2 //发生异常匹配异常表中target为15的异常
16: bipush 30 //从常量池中拿出30
18: istore_1 //放入本地变量表中的1号槽位
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
3、finally
public class Demo2 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
Code:
stack=1, locals=4, args_size=1
0: iconst_0 //准备一个常量0
1: istore_1 //将常量0放入本地变量表的1号槽位上
//try块
2: bipush 10 //常量池中取出10
4: istore_1 //将常量10放入本地变量表的1号槽位上
//try块执行完后,会执行finally
5: bipush 30 //从常量池中拿出30
7: istore_1 //30放到一号槽位上
8: goto 27 //跳转到27跳指令
//catch块
11: astore_2 //异常信息放入局部变量表的2号槽位
12: bipush 20
14: istore_1
//catch块执行完后,会执行finally
15: bipush 30
17: istore_1
18: goto 27
//出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow //抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
4、finally中的return
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//结果为20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
return i;
} finally {
i = 20;
return i;
}
}
}
Code:
stack=1, locals=3, args_size=0
0: bipush 10 //常量池中拿取10,放到操作数栈中
2: istore_0 //将10放入局部变量表中的0号槽位上
3: iload_0 //将0号槽位的值放入操作数栈中
4: istore_1 //暂存返回值
5: bipush 20 //常量池中拿取20
7: istore_0 //将20放入局部变量表中的0号槽位上
8: iload_0 //将0号槽位的值放入操作数栈中
9: ireturn //ireturn会返回操作数栈顶的整型值20
//如果出现异常,还是会执行finally块中的内容,没有抛出异常
10: astore_2 //出异常的情况,异常会被吞掉,还是拿取20
11: bipush 20
13: istore_0
14: iload_0
15: ireturn //这里没有athrow了,也就是如果在finally块中如果有返回操作的话,且try块中出现异常,会吞掉异常!
Exception table:
from to target type
0 5 10 any
5、finally中return值吞掉异常
public class Demo3 {
public static void main(String[] args) {
int i = Demo3.test();
//最终结果为20
System.out.println(i);
}
public static int test() {
int i;
try {
i = 10;
//这里应该会抛出异常
i = i/0;
return i;
} finally {
i = 20;
return i;
}
}
}
6、finally不带return
public class Demo4 {
public static void main(String[] args) {
int i = Demo4.test();
System.out.println(i);
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
Code:
stack=1, locals=3, args_size=0
0: bipush 10
2: istore_0 //赋值给i 10
3: iload_0 //加载到操作数栈顶
4: istore_1 //加载到局部变量表的1号位置
5: bipush 20
7: istore_0 //赋值给i 20
8: iload_1 //加载局部变量表1号位置的数10到操作数栈
9: ireturn //返回操作数栈顶元素 10
10: astore_2
11: bipush 20
13: istore_0
14: aload_2 //加载异常
15: athrow //抛出异常
Exception table:
from to target type
3 5 10 any
public class Demo5 {
public static void main(String[] args) {
int i = 10;
Lock lock = new Lock();
synchronized (lock) {
System.out.println(i);
}
}
}
class Lock{}
Code:
stack=2, locals=5, args_size=1
0: bipush 10 //常量池中拿取10,放在操作数栈中
2: istore_1 //将10 放到局部变量表中1号槽位
3: new #2 //new对象,对应常量池中#2的位置 // class com/nyima/JVM/day06/Lock
6: dup //复制一份,放到操/作数栈顶,用于构造函数消耗
7: invokespecial #3 // Method com/nyima/JVM/day06/Lock."":()V
10: astore_2 //剩下的一份放到局部变量表的2号位置
11: aload_2 //加载到操作数栈
12: dup //复制一份,放到操作数栈,用于加锁时消耗
13: astore_3 //将操作数栈顶元素弹出,暂存到局部变量表的三号槽位。这时操作数栈中有一份对象的引用
14: monitorenter //加锁
//锁住后代码块中的操作
15: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
//加载局部变量表中三号槽位对象的引用,用于解锁
22: aload_3
23: monitorexit //解锁
24: goto 34
//异常操作
27: astore 4
29: aload_3
30: monitorexit //解锁
31: aload 4
33: athrow
34: return
//可以看出,无论何时出现异常,都会跳转到27行,将异常放入局部变量中,并进行解锁操作,然后加载异常并抛出异常。
Exception table:
from to target type
15 24 27 any
27 31 27 any
所谓的语法糖,其实就是指java编译器把.java源码编译为.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利。
public class Demo19 {
}
public class Demo19 {
//这个无参构造器是java编译器帮我们加上的
public Demo19() {
//即调用父类 Object 的无参构造方法,即调用 java/lang/Object." ":()V
super();
}
}
public class Demo20{
public static void main(String[] args){
Integer x = 1;
int y = x;
}
}
public class Demo20{
public static void main(String[] args){
//基本类型赋值给包装类型,称为装箱
Integer x = Integer.valueOf(1);
//包装类型赋值给基本类型,称谓拆箱
int y = x.intValue();
}
}
public class Demo3 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(10);
Integer x = list.get(0);
}
}
Code:
stack=2, locals=3, args_size=1
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//这里进行了泛型擦除,实际调用的是add(Objcet o)
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
//这里也进行了泛型擦除,实际调用的是get(Object o)
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
//这里进行了类型转换,将Object转换成了Integer
27: checkcast #7 // class java/lang/Integer
30: astore_2
31: return
所有调用get函数取值时,有一个类型转换的操作
Integer x = (Integer) list.get(0);
如果将返回结果赋值给一个int类型的变量,则还有自动拆箱的操作
int x = (Integer) list.get(0).intValue();
public class Demo21{
public static void foo(String... args){
//将args赋值给arr,可以看出String...实际就是String[]
String[] arr = args;
System.out.println(arr.length);
}
public static void main(){
foo("hello","world");
}
}
public class Demo4 {
public Demo4 {}
public static void foo(String[] args) {
String[] arr = args;
System.out.println(arr.length);
}
public static void main(String[] args) {
foo(new String[]{"hello", "world"});
}
}
1、数组使用foreach
public class Demo5{
public static void main(String[] args){
//数组赋初值的简化写法也是一种语法糖
int[] arr = {1,2,3,4,5};
for(int x : arr){
System.out.println(x);
}
}
}
public class Demo5 {
public Demo5 {}
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for(int i=0; i<arr.length; ++i) {
int x = arr[i];
System.out.println(x);
}
}
}
2、集合使用foreach
public class Demo5 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer x : list) {
System.out.println(x);
}
}
}
public class Demo5 {
public Demo5 {}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//获得该集合的迭代器
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()) {
Integer x = iterator.next();
System.out.println(x);
}
}
}
public class Demo6 {
public static void main(String[] args) {
String str = "hello";
switch (str) {
case "hello" :
System.out.println("h");
break;
case "world" :
System.out.println("w");
break;
default:
break;
}
}
}
public class Demo6 {
public Demo6() {
}
public static void main(String[] args) {
String str = "hello";
int x = -1;
//通过字符串的hashcode+value来判断是否匹配
switch (str.hashCode()) {
//hello的hashcode值
case 99162322 :
//判断完hashcode在次比较字符串,因为hashcode有可能一样的情况
if(str.equals("hello")) {
//相等给x付0
x = 0;
}
break;
//world的hashcode值
case 11331880 :
if(str.equals("world")) {
//在比较字符串,相等给x符1
x = 1;
}
break;
default:
break;
}
//另起一个switch,根据x进行判断
switch (x) {
case 0:
//x是0 ,证明匹配hello
System.out.println("h");
break;
case 1:
//x是1 ,证明匹配world
System.out.println("w");
break;
default:
break;
}
}
}
过程说明:
public class Demo7 {
public static void main(String[] args) {
SEX sex = SEX.MALE;
switch (sex) {
case MALE:
System.out.println("man");
break;
case FEMALE:
System.out.println("woman");
break;
default:
break;
}
}
}
enum SEX {
MALE, FEMALE;
}
public class Demo7{
/**
* 定义一个合成类(仅jvm使用,对我们不可见)
* 用来映射枚举的ordinal与数组元素的关系
* 枚举的ordinal表示枚举对象的序号,从0开始
* 即MALE的ordinal()=0,FEMALE的ordinal()=1
*/
static class $MAP{
//定义一个数组,数组大小为枚举类的于元素个数
static int[] map = new int[2];
static {
//ordinal即枚举元素对应所在位置,MALE为0,FEEMALE为1。
map[SEX.MALE.ordinal()] = 1;
map[SEX.FEEMALE.ordinal()] = 2;
}
}
public static void main(String[] args) {
SEX sex = SEX.MALE;
//将对应位置枚举元素的值赋给x,用于case操作,用元素下标进行比较
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("man");
break;
case 2:
System.out.println("woman");
break;
default:
break;
}
}
}
enum SEX {
MALE, FEMALE;
}
enum SEX {
MALE, FEMALE;
}
public final class Sex extends Enum<Sex>{
//对应枚举类中的元素
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
//调用构造函数,传入枚举元素的值及ordinal
MALE = new Sex("MALE",0);
MALE = new Sex("FEMALE",1);
$VALUES = new Sex[]{MALE,FEMALE};
}
//调用父类中的方法
private Sex(String name,int ordinal){
super(name,ordinal);
}
public static Sex[] values(){
return $VALUES.clone();
}
public static Sex valueOf(String name){
return Enum.valueOf(Sex.class,name);
}
}
我们都知道,方法重写时对返回值分两种情况:
class A{
public Number m(){
return 1;
}
}
class B extends A{
@Override
public Integer m(){
return 2;
}
}
对于子类,java编译器会做如下处理:
class B extends A{
public Integer m(){
return 2;
}
//此方法是真正重写了父类 public Number m()方法
public synthetic bridge Number m(){
//调用public Integer m();
return m();
}
}
其中桥接方法比较特殊,仅对JVM可见,并且与原来的public Integer m()没有命名冲突,可以用下面反射代码来验证:
for(Method m : B.class.getDeclaredMethods()){
System.out.println(m);
}
public class Demo21 {
public static void main(String[] args){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("running...");
}
};
}
}
public class Demo21{
public static void main(String[] args){
//用额外创建的类,来创建匿名内部类
Runnable runnable = new Demo21$1();
}
//创建了一个额外的类来实现Runnable接口
final class Demo21$1 implements Runnable{
public Demo21$1(){}
@Override
public void run(){
System.out.println("running...");
}
}
}
public class Demo21 {
public static void main(String[] args) {
int x = 1;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
}
}
public class Demo21 {
public static void main(String[] args) {
int x = 1;
Runnable runnable = new Demo21$2(final x);
}
}
final class Demo21$2 implements Runnable{
//多创建了一个变量
int val$x;
//变为了有参构造器
public Demo21$2(int x){
this.val$x = x;
}
@Override
public void run(){
System.out.println(val$x);
}
}
**注意:**局部变量必须是final的,因为在创建Demo21$2对象时,将x的值赋值给了Demo21 2 对象的 v a l 2对象的val 2对象的valx属性,所以x不能再发生改变秒如果变化 ,那么val$x属性没有机会跟着一起改变。
1、验证
验证类是否符合JVM规范,安全性检查。
2、准备
为static变量分配空间,设置默认值。
3、解析
解析的含义:将常量池中的符号引用解析为直接地址。
验证:
/**
* 验证解析的含义
*/
public class Demo22 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classLoader = Demo22.class.getClassLoader();
classLoader.loadClass("com.lixiang.C");
//用于阻塞主线程
System.in.read();
}
}
class C{
D d = new D();
}
class D{
}
java -cp D:\JDK\jdk1.8\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
/**
* 验证解析的含义
*/
public class Demo22 {
public static void main(String[] args) throws ClassNotFoundException, IOException {
new C();
//用于阻塞主线程
System.in.read();
}
}
class C{
D d = new D();
}
class D{
}
初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的【构造方法】的线程安全。
clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
注意:
编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如
public class Test{
static {
i = 0; //给变量赋值可以正常编译通过
System.out.println(); //这句编译器会提示“非法向前引用”
}
static int i = 1;
}
发生时机
class A{
static int a = 0;
static {
System.out.println("A init");
}
}
class B extends A{
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("B init");
}
}
(1)final修饰的静态变量不会触发初始化
//访问B中final修饰的变量
System.out.println(B.b);
(2)类对象.class不会触发初始化
//访问B的class对象
System.out.println(B.class);
(3)创建类的数组不会触发初始化
//创建B类型的数组
System.out.println(new B[10]);
(4)ClassLoader不会初始化类,但是会加载类
//创建classLoader不会初始化B类,但是会加载B、A类
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
c1.loadClass("com.lixiang.B");
(5)Class.forName(),initizlize参数设置成false不会初始化操作
//Class.forName中initizlize参数设置成false不会初始化类B,但会加载B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("com.lixiang.B",false,c2);
(1)首次访问这个类的静态变量或静态方法时
//访问A中不被final修饰的静态变量
System.out.println(A.a);
(2)子类初始化,如果父类还没初始化,会先引发父类的初始化
//访问B中的静态变量,会先加载A的初始化
System.out.println(B.c);
(3)子类访问父类的静态变量,只会触发父类的初始化
//子类B访问父类A中的静态变量a,只会初始化A
System.out.println(B.a);
(4)Class.forName(“”)会初始化类B,但会先初始化类A
Class.forName("com.lixiang.B");
Java虚拟机设计团队有意把类加载阶段中的 ”通过一个类的全限定名来获取该类的二进制字节流” 这个动作放到Java虚拟机外部去实现,以便让应用程序自己去决定如何获取所需类。实现这个动作的代码被称为**“类加载器”**(ClassLoader)。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:**比较两个是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,**否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那么这两个必定不相等。
JDK 8为例
名称 | 加载的类 | 说明 |
---|---|---|
Bootstrap ClassLoader(启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader(扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader(应用程序类加载器) | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Appliaction |
可以通过在控制台输入指令,使得类被启动类加载器加载。
/**
* 启用Bootstrap类加载器加载类
*/
public class Demo25 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> aClass = Class.forName("com.lixiang.F");
System.out.println(aClass.getClassLoader()); //获取当前的类加载器
}
}
class F{
static {
System.out.println("bootstrap F init");
}
}
如果classpath和JAVA_HOME/jre/lib/ext下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已经将该同名类加载过了,则不会再次加载。
双亲委派模式,即调用类加载器Classloader的loadClass方法时,查找类的规则
loadClass源码:
private final ClassLoader parent;
public ClassLoader(ClassLoader parent) {
this.parent = parent;
}
protected Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)){
//首先要先看下该类是否已经被该类加载器加载过了
Class<?> c = findLoadClass(name);
//如果没有被加载过
if (c == null){
long t0 = System.nanoTime();
try{
//看是否被它的上级加载器加载过了Exception的上级是Bootstrap,但是它显示为null
if(parent != null){
c = parent.loadClass(name,resolve);
}else {
//看是否被启动类加载器加载过
c = finBootstrapClassOrNull(name);
}
}catch (ClassNotFoundException e){
//ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//捕获异常但是不做任何处理
}
if(c == null){
//如果还没有找到,先让拓展类加载器调用findClass方法找到该类,如果还没有会找到,
//就抛出异常,然后让应用类加载器去找classpath下找该类
long t1 = System.nanoTime();
c = findClass(name);
//记录时间
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if(resolve){
resolveClass(c);
}
return c;
}
}
使用场景
步骤
自定义类加载器
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String path = "D:\\MyClassLoader\\"+name+".class";
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path),os);
//得到字节数组
byte[] bytes = os.toByteArray();
//byte[] -> *.class
return defineClass(name,bytes,0,bytes.length);
}catch (IOException e){
e.printStackTrace();
throw new ClassNotFoundException("类文件未找到",e);
}
}
}
破坏双亲委派模式
1、案例
/**
* -XX:+PrintCompilation -XX:DoEscapeAnalysis
*/
public class JIT1 {
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
long start = System.nanoTime();
for (int j = 0; j < 1000; j++) {
new Object();
}
long end = System.nanoTime();
System.out.printf("%d\t%d\n",i,(end - start));
}
}
}
2、JVM将执行状态分成了5个层次
0层:解释执行,用解释器将字节码翻译为机器码
1层:使用C1即时编译器编译执行(不带profiling)
2层:使用C1即时编译器编译执行(带基本的profiling)
3层:使用C1即时编译器编译执行(带完全的profiling)
4层:使用C2即时编译器编译执行
profiling是指在运行中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。
3、即时编译器(JIT)与解释器的区别
解释器
即时编译器
对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行,另一方面,对于仅占据小部分的热点代码,我们则可以编译成机器码,以达到理想的运行速度。执行效率上简单比较一下Interpreter < C1 < C2 ,总的目标是发现热点代码(hotspot名称的由来),并优化这些热点代码。
4、逃逸分析
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot虚拟机可以分析新创建对象的使用范围,并决定是否在Java堆上分配内存的一项技术。
逃逸分析的JVM参数如下:
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数。
5、对象逃逸状态
全局逃逸(GlobalEscape)
参数逃逸(ArgEscape)
没有逃逸
6、逃逸分析优化
针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化
锁消除
我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。
锁消除的JVM参数如下:
锁消除在JDK8中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。
标量替换
首先要明白标量和聚合量,基础类型和对象引用可以理解为标量,他们不能被进一步的分解。而能被进一步分解的量就是聚合量,比如:对象。
对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替。
这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。
标量替换的JVM参数如下:
栈上分配
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了GC压力,提高了应用程序性能。
1、内联函数
内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。
2、JVM内联函数
C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的,如
public final void doSomething() {
// to do something
}
总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数。
第二个原因则更重要:方法内联
如果JVM检测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身,如:
private int add4(int x1, int x2, int x3, int x4) {
//这里调用了add2方法
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
方法调用被替换后
private int add4(int x1, int x2, int x3, int x4) {
//这里调用了add2方法
return x1 + x2 + x3 + x4;
}
//foo.invoke()前面0-15次调用使用的是MethodAccessor的NativeMethodAccessorImpl实现
public class Reflect {
public static void foo(){
System.out.println("foo...");
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method = Reflect.class.getMethod("foo");
for (int i = 0; i < 16; i++) {
method.invoke(null);
}
}
}
1、JPS是什么?
jps (JVM Process Status Tool)是其中的典型jvm工具。除了名字像 UNIX 的 ps 命令之外,它的功能也和 ps 命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class, main()函数所在的类)名称以及这些进程的本地虚拟机唯- ID (Local Virtual Machine Identifier, LVMID),虽然功能比较单一,但它是使用频率最高的 JDK 命令行工具。
2、实战使用
jps -l
1、jstat是什么
jstat(JVM Statistics Monitor Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程-虚拟机进程中的类加载、内存、垃圾收集、JIT编译等运行数据,在没有GU图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
2、jstat命令使用
jstat -gc 2764 250 20 //2764表示进程id,250表示250毫秒打印一次,20表示一共打印20次
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
1、jinfo是什么
jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表,但如果想知道未被显示指定的参数的系统默认值,除了去找资料外,就只能使用info的-flag选项进行查询了。
2、jinfo命令使用
jinfo 1444(进程id)
jinfo -flag CMSInititingOccuancyFraction 1444(进程id)
1、jmap是什么
Jmap (Memory Map for Java)命令用于生成堆转储快照。如果不使用 jmap 命令,要想获取 Java 堆转储快照,还有一些比较“暴力”的手段:-XX: +HeapDumpOnOutOfMemoryError 参数,可以让虚拟机在 OOM 异常出现之后自动生成 dump 文件,用于系统复盘环节
和 info 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的- dump 选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris 下使用。
2、jmap常用命令
-dump
windows: jmap -dump:format=b,file=d:\a.bin 1234
mac: jmap -dump:format=b,file=/Users/daniel/deskTop
-histo more分页去查看
B :byte
C : char
I :Int
1、jhat是什么
Sun JDK 提供 jhat (JVM Heap Analysis Tool)命令常与 jmap 搭配使用,来分析 jmap 生成的堆 转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看
特点:
2、jhat使用
jhat bin文件
1、jstack是什么
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看哥哥线程的调用堆栈,就可以知道没有响应线程到底在后台做些什么事情,或者等待者什么资源。
2、Jstack怎么做
常用命令jstack -l 3500(进程id)
jstack -F 当正常输出的请求不被响应时,强制输出线程堆栈 Force
线上程序一般不能kill进程pid的方式直接关闭