Knight-Wu / articles

博客
3 stars 1 forks source link

learning JVM #4

Closed Knight-Wu closed 6 years ago

Knight-Wu commented 6 years ago

运行时数据区域

程序计数器(programme counter register)

若执行的是非native方法, 则保存下条指令的地址; 若是native方法, 则为空;每个线程独有, 互不影响

虚拟机栈(virtual machine stacks), 本地方法栈(native method stack)

每个线程独有, 每个方法创建的时候都会创建一个栈帧(stack frame),用于存储方法的局部变量, 操作数栈等. , 虚拟机栈和本地方法栈的不同是,前者执行java方法, 后者执行native方法

java 堆(heap)

存放对象的实例和数组, 所有线程所共有; 如果堆中没有内存完成实例的分配, 并且堆也无法再扩展时,抛出 OutOfMemoryError

方法区(Method Area)

线程间共享, 存储每个类的结构,包括运行时常量 (包括string pool) ,静态变量,即时编译器编译后的代码等数据

本地内存(native memory, C heap)

  1. 管理java heap的状态数据(用于GC);
  2. JNI调用,也就是Native Stack;
  3. JIT(即使编译器)编译时使用Native Memory,并且JIT的输入(Java字节码)和输出(可执行代码)也都是保存在Native Memory;
  4. NIO direct buffer。对于IBM JVM和Hotspot,都可以通过-XX:MaxDirectMemorySize来设置nio直接缓冲区的最大值。默认是64M。超过这个时,会按照32M自动增大。 DirectBuffer访问更快,避免了数据从heap memory 拷贝到本地堆。DirectBuffer byte array 实际是保存在native heap中,但是操作该byte array的对象保存在java heap中。 GC时不会直接回收native memory, 通过释放heap memory中的对象来释放native memory, 但是通常java heap没达到gc 的条件.
  5. 对于IBM的JVM某些版本实现,类加载器和类信息都是保存在Native Memory中的。

虚拟机对象

内存泄漏原因

  1. 因为jvm会尽量保持堆是初始化的大小, 所以设置最大堆(-Xmx) 和堆的初始化值(-Xms)一样, 以减小gc 频率
  2. heap dumps

    反映对象的数量和类文件所占用的字节数, -XX:+HeapDumpOnOutOfMemoryError, 在快oom的时候打印日志.

  3. 堆转储分析:live objects

    使用jmap并且加上-histo参数可以为你产生一个直方图,它显示了从程序运行到现在所有对象的数量和内存消耗,并且包含了已经被回收的对象和内存。如果使用-histo:live参数会显示当前还在堆中得对象数量及其内存消耗,不论这些对象是否要被垃圾搜集器进行回收。

  4. 堆转储分析:跟踪引用链

    浏览堆转储引用链具有两个步骤:首先需要使用-dump参数来使用jmap,然后需要用jhat来使用转储文件。查看对象的引用链

  5. 堆转储分析:内存分配情况

    可以找到对象使用的情况, 以及这些对象的引用被哪里的代码使用的, 但是有时候这种方式还是不够的, 例如string对象会很多,

解决永久代的问题通常都是比较痛苦的。一般可以先考虑加上-XX:+TraceClassLoading和-XX:+TraceClassUnloading命令行选项以便找出那些被加载了但是没有被卸载的类。如果你加上了-XX:+TraceClassResolution命令行选项,你还可以看到哪些类访问了其他类,但是没有被正常卸载。

java heap分析工具

jmap -dump:format=b,file-fileName pid
// 然后用 Eclipse memory analyzer 打开

问题

  1. linux内存分配策略, 每个进程如何分配内存, 和windows有何不同

三. 垃圾回收

可达性分析

若不存在从对象到GC root的引用链, 则在下次gc时, 该对象会被回收, 下次gc可能是minor gc(日志中称作 gc, 发生在新生代 ), major gc(日志中称作 full gc, 发生在老年代)

引用

垃圾收集策略

需要回收的对象进行一次标记,标记完成后统一回收

将内存分配为一块eden和两块survivor, 比例是8:1, 每次使用新生代内存的90%, gc时将存活对象复制到空闲的survivor, 剩余对象一次性清理

新生代收集器, 注重控制吞吐量来控制GC的停顿时间, 虚拟机运行100分钟, GC一分钟, 吞吐量 99% 重要参数 : -XX:MaxGCPauseMills , -XX:GCTimeRatio, -XX:UseAdaptiveSizePolicy(自适应调节) 与ParNew的最大区别

是Serial 的老年代版本, 与Parallel Scavenage配合使用, 作为CMS的备案, 在发生concurrent mode failure使用

是Parallel Scavenage的老年代版本, 可与Parallel Scavenage 配合使用

主要是将回收时间降至最短,基于标记-清除算法,

问题

java heap 分代(基于jdk1.8)

GC回收过程

无论是Minor GC还是CMS GC,都会’Stop-The-World’,即停止用户的一切线程,只留下gc线程回收垃圾对象。其中Minor GC的STW时间主要耗费在复制阶段,CMS GC的STW时间主要耗费在标示垃圾对象阶段

GC调优案例

hotSpot的算法实现

开发中的GC优化

  1. 尽量少使用临时对象, 局部变量尽量使用基本数据类型, 也可以避免装箱; 用StringBuffer, 不用string做累加.

StringBuffer是线程安全的; StringBuilder 不是线程安全的(所以内部没有一个缓存的数组), 适合单线程快速使用后丢弃,

  1. 对象不用时显式置为null
  2. 尽量少用静态对象变量

    static变量被class 引用, class被classloader引用, 除非classloader is reloaded, 例如webapp reload, 否则static变量不会被垃圾回收.

// 如果只是想临时用一下static, 可以用static block, 在block结束之后, 就会被GC; 或者在static reference不使用之后, 显式赋为null
class MyUtils {
   static
   {
      MyObject myObject = new MyObject();
      doStuff(myObject, params);
   }

   static boolean doStuff(MyObject myObject, Params... params) {
       // do stuff with myObject and params...
   }
}

类加载机制

参考自 link

预加载, 虚拟机启动的时候加载rt.jar的class, 像java.lang.、java.util.、java.io.*等等, 可以设置虚拟机参数 -XX+TraceClassLoading 来获取类加载信息

运行时加载: 在用到一个class文件的时候, 如果内存中没有则按类的全限定名来加载.

加载阶段:

  1. 获取class文件的二进制流 , 例如从zip包中获取,这就是以后jar、ear、war格式的基础 从网络中获取,典型应用就是Applet 运行时计算生成,典型应用就是动态代理技术 由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件 从数据库中读取,这种场景比较少见
  2. 将类信息, 静态变量, 字节码, 常量等内容放到方法区
  3. 内存中生成java.lang.Class的对象, 作为访问入口
  1. 验证

    这个地方要说一点和开发者相关的。.class文件的第5~第8个字节表示的是该.class文件的主次版本号,验证的时候会对这4个字节做一个验证,高版本的JDK能向下兼容以前版本的.class文件,但不能运行以后的class文件(向后兼容),即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的.class文件。举个具体的例子,如果一段.java代码是在JDK1.6下编译的,那么JDK1.6、JDK1.7的环境能运行这个.java代码生成的.class文件,但是JDK1.5、JDK1.4乃更低的JDK版本是无法运行这个.java代码生成的.class文件的。如果运行,会抛出java.lang.UnsupportedClassVersionError,这个小细节,务必注意。

  2. 准备

    为类变量(static 变量, 不是实例变量)分配内存并设置其初始值, 均在方法区分配

这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如”public static int value = 123;”,value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如”public static final int value = 123;”就不一样了,在准备阶段,虚拟机就会给value赋值为123。

  1. 解析

    将符号引用替换为直接引用的过程,

符号引用, 包括: 类和接口的全限定名; 字段的名称和描述符; 方法的名称和描述符

例如下面这串代码:

package com.xrq.test6;

public class TestMain
{
    private static int i;
    private double d;

    public static void print()
    {

    }

    private boolean trueOrFalse()
    {
        return false;
    }
}

用javap把这段代码的.class反编译一下:

Constant pool:
   #1 = Class              #2             //  com/xrq/test6/TestMain
   #2 = Utf8               com/xrq/test6/TestMain
   #3 = Class              #4             //  java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               i
   #6 = Utf8               I
   #7 = Utf8               d
   #8 = Utf8               D
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Methodref          #3.#13         //  java/lang/Object."<init>":()V
  #13 = NameAndType        #9:#10         //  "<init>":()V
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/xrq/test6/TestMain;
  #18 = Utf8               print
  #19 = Utf8               trueOrFalse
  #20 = Utf8               ()Z
  #21 = Utf8               SourceFile
  #22 = Utf8               TestMain.java

看到Constant Pool也就是常量池中有22项内容,其中带”Utf8″的就是符号引用。比如#2,它的值是”com/xrq/test6/TestMain”,表示的是这个类的全限定名;又比如#5为i,#6为I,它们是一对的,表示变量时Integer(int)类型的,名字叫做i;#6为D、#7为d也是一样,表示一个Double(double)类型的变量,名字为d;#18、#19表示的都是方法的名字。 那其实总而言之,符号引用和我们上面讲的是一样的,是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。

直接引用: 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。

  1. 初始化

    初始化过程是执行一个类的构造器()方法的过程, 其实就是给static变量赋予用户指定的值以及执行静态代码块, 虚拟机会保证类在多线程环境下正确的被初始化并同步, 在同一个类加载器下, 一个类只会初始化一次.

以下几种场景, 类会被正常初始化

1、使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰的静态字段除外)、调用一个类的静态方法的时候

2、使用java.lang.reflect包中的方法对类进行反射调用的时候

3、初始化一个类,发现其父类还没有初始化过的时候

4、虚拟机启动的时候,虚拟机会先初始化用户指定的包含main()方法的那个类


1、子类引用父类静态字段,不会导致子类初始化。至于子类是否被加载、验证了,前者可以通过”-XX:+TraceClassLoading”来查看

public class SuperClass
{
    public static int value = 123;

    static
    {
        System.out.println("SuperClass init");
    }
}

public class SubClass extends SuperClass
{
    static
    {
        System.out.println("SubClass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        System.out.println(SubClass.value);
    }
}
运行结果为

SuperClass init

2、通过数组定义引用类,不会触发此类的初始化

public class SuperClass
{
    public static int value = 123;

    static
    {
        System.out.println("SuperClass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        SuperClass[] scs = new SuperClass[10];
    }
}

3、引用静态常量时,常量在编译阶段会存入类的常量池中,本质上并没有直接引用到定义常量的类

public class ConstClass
{
    public static final String HELLOWORLD =  "Hello World";

    static
    {
        System.out.println("ConstCLass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        System.out.println(ConstClass.HELLOWORLD);
    }
}
运行结果为
Hello World

在编译阶段通过常量传播优化,常量HELLOWORLD的值”Hello World”实际上已经存储到了NotInitialization类的常量池中,以后NotInitialization对常量ConstClass.HELLOWORLD的引用实际上都被转化为NotInitialization类对自身常量池的引用了。也就是说,实际上的NotInitialization的Class文件中并没有ConstClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。


    D:/src/main/java/
                  packageA/A.java
                  packageB/B.java
import packageB.B;

public class A{

    // do something
}

当需要在任意目录编译 A.java时, 需要知道所引用的B.java的位置, 假设运行javac的目录为D: , 因为当前目录是D:, 在当前目录下用包名无法找到B.java, 故需要手动指定额外的classpath, 则会在packageA和packageB生成各自的class文件.

    javac -classpath src/main/java src/main/java/packageA/A.java

jvm常用命令

问题

  1. spring是如何运行起来的, 并维持程序一直运行, 不结束