JVM虚拟机底层原理


介绍


JVM概念:Java Virtual Machine:java程序的运行环境(java二进制字节码的运行环境)。

区别:

JVM + 基础类库 = JRE

JVM + 基础类库 + 编译归工具 = JDK

类加载器:ClassLoader

JVM内存结构:方法区(Method Area)、堆(Heap)、虚拟机栈(JVM Stacks)、程序计数器(PC Register)、本地方法栈(Nataive Method Stacks)。

执行引擎:解释器(Interpreter)、即时编译器(JIT Compiler)、垃圾回收(GC)。


JVM内存结构

JVM内存结构之程序计数器


指令执行流程:jvm指令交给解释器,解释器再翻译成机器码,机器码再交给CPU运行。

程序计数器(寄存器)(Program Counter Register)作用:记住下一条jvm指令的执行地址

程序计数器特点:每个线程都有自己的程序计数器,是线程私有的。不会存在内存溢出。


JVM内存结构之虚拟机栈


Java虚拟机栈(Java Virtual Machine Stacks):每个线程运行时需要的内存空间,称为虚拟机栈。

栈帧:每个方法调用运行时候需要的内存。一个栈内可能有多个栈帧。比如,方法一调用了方法二,方法二调用了方法三。

活动栈帧:正在执行的那个方法,即栈顶部的栈帧。每个线程只能有一个活动栈帧。

垃圾回收是否涉及栈内存? 不需要,弹出栈。

栈内存分配越大越好吗?递归时候大内存较好,但是栈内存大了会影响线程数量。

方法内的局部变量是否线程安全? 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。下面代码,只有m1()方法是线程安全的。

public static void m1() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb.toString());
}

public static void m2(StringBuilder sb) {
    sb.append(1);
    sb.append(2);
    sb.append(3);
    System.out.println(sb.toString());
}

public static StringBuilder m3() {
    StringBuilder sb = new StringBuilder();
    sb.append(1);
    sb.append(2);
    sb.append(3);
    return sb;
}

栈内存溢出(java.lang.StackOverflowError):栈帧过多导致栈内存溢出(比如,递归);栈帧过大导致栈内存溢出(该情况较少)。


JVM内存结构之本地方法栈


本地方法:不是java代码编写的方法。这些本地方法使用的内存就是本地方法栈。

protected native Object clone() throws CloneNotSupportedException;

JVM内存结构之堆


堆(Heap):通过new关键字,创建对象都会使用堆内存。

堆特点:它是线程共享的,堆中对象都需要考虑线程安全的问题;有垃圾回收机制。

内存溢出(java.lang.OutOfMemoryError)


JVM内存结构之方法区


方法区:所有Java虚拟机线程共享的区域。它存储了和类结构相关的信息,如运行时常量池,成员变量,方法,构造方法。

方法区在虚拟机启动时候就被创建了。方法区也会内存溢出。

常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

运行时常量池:常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。

常量池中的信息,都会被加载到运行时常量池中。


StringTable


JVM1.6中StringTable存放在永久代方法区中;1.8中将StringTable存放在堆中了。

StringTable是hashtable结构,不能扩容。

String s1 = "a"; // 懒惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() -> new String("ab")
String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab

System.out.println(s3 == s4);//false,s3在串池中,s4在堆中
System.out.println(s3 == s5);//true

intern():将字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回。

String x = "ab";
String s = new String("a") + new String("b");// 堆
String s2 = s.intern();

System.out.println( s2 == x);//true
System.out.println( s == x );//false

面试题

String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2;   // new String("ab")
String s5 = "ab";
String s6 = s4.intern();

// 问
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true

String x2 = new String("c") + new String("d"); // new String("cd")
String x1 = "cd";
x2.intern();//x2入池失败
System.out.println(x1 == x2);//false

// 调换了位置
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();//x2入池成功
String x1 = "cd";
System.out.println(x1 == x2);//true

StringTable 特性:

常量池中的字符串仅是符号,第一次用到时才变为对象 
利用串池的机制,来避免重复创建字符串对象 
字符串变量拼接的原理是 StringBuilder (1.8) 
字符串常量拼接的原理是编译期优化 
可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池 
1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回 
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

StringTable会GC垃圾回收。


垃圾回收

判断对象是否可以回收之引用计数法


被引用几次就计数几,当计数零时候就没有引用的了,可以回收。弊端:两个对象相互引用,就无法回收。


判断对象是否可以回收之可达性分析算法


Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。

根对象(GC Root):肯定不能当成垃圾被回收的对象称为根对象。一个对象没有被根对象直接或者间接引用,就可以被回收。

扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收该对象。


五种引用


强引用:只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收。

软引用(SoftReference):仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象;可以配合引用队列来释放软引用自身。

弱引用(WeakReference):仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。可以配合引用队列来释放弱引用自身。

虚引用(PhantomReference):必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由Reference Handler 线程调用虚引用相关方法释放直接内存

终结器引用(FinalReference):无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次GC时才能回收被引用对象。


强引用和软引用代码:

private static final int _4MB = 4 * 1024 * 1024;

List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    list.add(new byte[_4MB]);
}
System.in.read();

// 软引用:list --> SoftReference --> byte[]
List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
    SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
    System.out.println(ref.get());
    list.add(ref);
    System.out.println(list.size());
}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
    System.out.println(ref.get());
}

软引用配合引用队列使用,自动清除无用的软引用:

private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
    List<SoftReference<byte[]>> list = new ArrayList<>();
    // 引用队列
    ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
    for (int i = 0; i < 5; i++) {
        // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
        SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
        System.out.println(ref.get());
        list.add(ref);
        System.out.println(list.size());
    }

    // 从队列中获取无用的 软引用对象,并移除
    Reference<? extends byte[]> poll = queue.poll();
    while( poll != null) {
        list.remove(poll);
        poll = queue.poll();
    }
    System.out.println("===========================");
    for (SoftReference<byte[]> reference : list) {
        System.out.println(reference.get());
    }
}

弱引用演示代码:

private static final int _4MB = 4 * 1024 * 1024;
public static void main(String[] args) {
    //  list --> WeakReference --> byte[]
    List<WeakReference<byte[]>> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
        list.add(ref);
        for (WeakReference<byte[]> w : list) {
            System.out.print(w.get()+" ");
        }
        System.out.println();
    }
    System.out.println("循环结束:" + list.size());
}

垃圾回收算法之标记清除


Mark Sweep:先标记垃圾,再清除垃圾。优点:速度快。缺点:空间不连续,产生内存碎片。


垃圾回收算法之标记整理


Mark Compact:优点:不会产生内存碎片,缺点:速度慢。


垃圾回收算法之复制


Copy:内存空间划分为FROMTO两个区域。在FROM垃圾回收后,复制到TO区域,不会有内存碎片,然后TO变成了FROM。需要占用双倍内存空间。


分代垃圾回收


堆内存分为:老年代(长时间使用的内存区域)和新生代(分为:伊甸园eden、幸存区From、幸存区To)。

新生代new generation垃圾回收:Minor GC

老年代tenured generation垃圾回收:Full GC

对象首先分配在伊甸园eden区域。新生代空间不足时,触发 minor gc,使用复制垃圾回收算法,将伊甸园from存活的对象使用 copy 复制到 to 中,存活的对象年龄加1并且交换 from to

minor gc 会引发 stop the world,暂停其它用户的线程(因为对象地址会改变),等垃圾回收结束,用户线程才恢复运行。

当新生代中的对象年龄超过阈值时,会晋升至老年代,最大寿命是15(4bit)。

当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gcstop the world的时间更长。老年代垃圾回收的算法是标记清除或者标记整理。

首次存储一个新对象:如果新生代空间不足,老年代充足,会直接存储在老年代中;如果新生代空间不足,老年代空间也不足,会OOM。

子线程的OOM不会导致主线程的结束。

相关VM参数:

堆初始大小 -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

垃圾回收器


串行:单线程,堆内存较小,适合个人电脑。XX:+UseSerialGC = Serial(新生代-复制算法) + SerialOld(老年代-标记整理算法)

吞吐量优先:多线程,堆内存较大,多核cpu。让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高。

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n

响应时间优先:多线程,堆内存较大,多核cpu。尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5。

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

Garbage First




文章作者:
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 !
  目录