jvm(1)深入理解java虚拟机笔记

时间:2022-02-24 09:58:14

1.基础

(1)查看jdk版本java –version可以看到虚拟机Hotspot

         HotSpot(TM)64-Bit Server VM (build 24.79-b02, mixed mode)

(2)openjdk的手工编译(java.c)(open jdk与sun jdk)  

2.java内存区域与内存溢出异常

 jvm(1)深入理解java虚拟机笔记

(补充)①java方法:java代码

              ②native方法:java调用非java代码,如C/C++

程序计数器Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每条线程被CPU执行之后,需要切换下一条,为了使线程能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。(java方法时,程序计数器会显示正在执行的字节码指令地址;如果正在执行native方法,程序计数器为空)

这一块内存区域为线程私有的内存。


Java虚拟机栈Java Virtual Machine Stacks)也是线程私有的,生命周期与线程相同,因为它描述的是Java方法执行的内存模型。Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

Java虚拟机的规范中,对这个区域规定了两种异常情况:

·        *Error 
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 *Error异常

·        OutOfMemoryError 
如果虚拟机栈在扩展的时无法申请到足够的内存,就会抛出 OutOfMemoryError异常


本地方法栈NativeMrthod Stack)与虚拟机栈发挥的作用非常相似,他们之间的区别:

·        虚拟机栈为虚拟机执行java方法(也就是字节码)服务

·        本地方法栈则为虚拟机使用到的Native方法服务

与虚拟机一样,本地方法栈区域也会抛出 *Error OutOfMemeoryError异常。


JavaJava Heap)是Java虚拟机锁管理的内存中最大的一块, Java堆是被所有线程共享的一块内存区域 ,在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例 Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续即可。在实现时,既可以实现成固定大小的,也可以是可扩展的(通过-Xmx -Xms 控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError异常。

此外,Java堆是垃圾收集器器管理的主要区域。(也称GC堆,不要翻译垃圾堆)


方法区MethodArea)与Java一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码等数据。

方法区是堆的一个逻辑部分,但是它与java堆又是不同的,所以它有了一个别名——非堆(Non-Heap)。

方法区中的内存一般不会被 GC回收,GC也难回收,所以被取名为永久代,意思是永久存在。 这区域的内存回收目标主要是针对常量池的回收和对类的卸载 。但是永久代中的数据并非真的永久存在,只是回收比较麻烦。

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError异常。

 

2)常量池


jvm(1)深入理解java虚拟机笔记

有三个概念需要清楚:

  • 常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。
  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
  • 运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。

JDK1.6之前字符串常量池位于方法区之中。 
JDK1.7
字符串常量池已经被挪到堆之中。


String s1 = "Hello";

String s5 = new String("Hello");
String s6 = s5.intern();    //intern()属于native方法
System.out.println(s1 == s6);  // true


    至此,我们可以得出三个非常重要的结论: 

          必须要关注编译期的行为,才能更好的理解常量池。

          运行时常量池中的常量,基本来源于各个class文件中的常量池。

          程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。

实际上还有整型常量池、浮点型常量池等等,但都大同小异,只不过数值类型的常量池不可以手动添加常量,程序启动时常量池中的常量就已经确定了,比如整型常量池中的常量范围:-128~127,只有这个范围的数字可以用到常量池。

 

3Hotspot虚拟机在java对中对象分配、布局和访问

1、对象的创建:

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查。这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任

务等同于把一块确定大小的内存从Java堆中划分出来。

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。

 

2、对象的内存布局:

对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但从数组的元数据中却无法确定数组的大小。

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

对齐填充并不是必然存在的,对齐填充是为了使对象的大小满足为8字节的整数倍的要求。

 

4)实战:OutOfMemoryError

jvm(1)深入理解java虚拟机笔记

/**

 * VM Args-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError

* -verbose:gc-Xms20M-Xmx20M -Xmn10M-XX:+PrintGCDetails-XX:SurvivorRatio=8

 */

publicclassHeapOOM {

    staticclass OOMObject {

    } 

    publicstatic void main(String[]args) {

        List<OOMObject> list =newArrayList<OOMObject>();

           while (true) {

             list.add(new OOMObject());

         }

    }

}    

会打印java.lang.OutOfMemoryError信息

java.lang.OutOfMemoryError: Javaheap space

Dumping heap to java_pid6760.hprof...

Heapdump file created [27927225 bytes in 1.607 secs]

使用MAD分析


4.2)虚拟机栈和本地方法栈溢出(Hotspot不区分二者)

(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出*Error 异常; 

(2)如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。 
(3)与虚拟机栈一样,本地方法栈区域也会抛出 *Error 和OutOfMemoryError 异常。

 

(第三章)垃圾收集器与内存分配策略

哪些内存需要回收?什么时候回收?如何回收?

1)内存运行区域中:程序计数器、虚拟机栈、本地方法栈3个区域岁线程随线程而生,随线程而灭。无需考虑回收;

java堆和方法区内存分配是动态的,垃圾收集器关注这部分。(方法区永久代)

 

对象是否存活的算法:

①引用计数算法:引用时计数器加1,引用是失效时,计数器减1,计数器为0就不可能再被使用。(java并不是)。

②可达性分析算法:

当一个对象到GC Roots没有任何引用链相连 (用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图,对象object 5、 object 6、 object 7会被判定为是可回收的对象。

jvm(1)深入理解java虚拟机笔记

finalize()在什么时候被调用?
有三种情况
1.
所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候.
2.
程序退出时为每个对象调用一次finalize方法。
3.
显式的调用finalize方法

finalize()方法的重写

权限(Access)需要是protected或者是public ,不能是private

finalize()方法不需要显示地调用, 在垃圾回收(GC)时会被自动先行调用的。

需要显示地调用垃圾回收方法(System.gc()),并且需要有new出来的尚未被销毁的匿名对象的存在(调用其它语言暂不作考虑),finalize()方法才会被调用

public class F {

   public static void main(String args[]) {

      new F();  

      //int a[] = new int[3];  

      //F f= new F();  

      System.gc();  

  }

   public void finalize() throws Throwable{           

        //super.finalize();  

        System.out.println("finalize method was called!");

    }

} 

 

 

  

finalize ( ) 能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及时,所以笔者建议大家完全可以忘掉Java语言中有这个方法的存在。

 

回收方法区

很多人认为方法区(或者HotSpot虚拟机中的永久代)有垃圾收集

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常类似。

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁 自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。


分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集” ( GenerationalCollection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法

 
(第四章)虚拟机性能监控与故障处理工具

 

JDK的命令行工具 

jps:虚拟机进程状况工具

jstat:虚拟机统计信息监视工具

jinfo:Java配置信息工具

jinfo命令格式:  jinfo [ option ] pid

jmap:Java内存映像工具

jmap命令格式:jmap [ option] vmid

jmap(Memory Map for Java)命令用于生成堆转储快照(一般称为heapdump或dump文件)。如果不使用jmap命令,要想获取Java堆转储快照,

还有一些比较“暴力”的手段:譬如在第2章用过的-XX:+HeapDumpOnOutOfMemoryError参数,可以让虚拟机在OOM异常出现之后自动生成dump文件,通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成dump文件,又或者在Linux系统下通过Kill -3命令发送进程退出信号“吓唬”一下虚拟机,也能拿到dump文件

(4.3)JDK可视化工具

①JConsole : Java监视与管理控制台

通过JDK/bin目录下的“jconsole.exe”启动JConsole

②VisualVM

    首先到JDK安装目录/bin目录下,双击jvisualvm.exe文件启动