JVM 简单笔记

JVM简单笔记

1. JVM的体系结构

JVM 简单笔记

其中,红字标出的方法区和堆是会产生垃圾的地方,而方法区是一个特殊的堆。并且,所谓 JVM 调优大部分情况下都是对堆进行调优。

T:栈区存放的是对象的引用(地址),对象的实例存放在堆区。

2. 类加载器

JVM 简单笔记

其中,类加载器默认有三种,级别从高到低为:

  1. 启动类加载器(BootstrapClassloader):加载核心类库,如 rt.jar 等;
  2. 扩展类加载器(ExtClassLoader):加载扩展类库,位于 \jdk1.8.0_144\jre\lib\ext
  3. 应用程序类加载器(AppClassLoader):加载用户自定义的类;

并且,在这三个类加载器加载类的时候,遵从关键的双亲委派机制

简单来说,类加载器在加载类的时候先将加载请求向上级委派,如果上级加载器可以加载,则加载成功;当*别的加载器,即启动类加载器都未能加载时,再将请求向下级委派加载,直到应用程序类加载器,若仍未加载成功,则报 ClassNotFound 异常。

JVM 双亲委派机制详解

类加载器的双亲委派机制是为了保护系统的安全,防止善意代码被恶意代码覆盖,属于沙箱安全机制的一部分。

3. 本地方法

当我们需要开启一个线程的时候,会调用 Thread 的 start 方法

public class NativeDemo {
    public static void main(String[] args){
        new Thread(()->{
            System.out.println("Thread Output");
        },"My Thread").start();
    }
}

此时我们可以查看 start 方法的源码

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

可以看到,start 方法中调用了 start0 方法,这个方法就在它下面,且只有一句话,像一个接口一样。而我们知道,普通类中是不能出现抽象方法和接口的,这个方法能存在正是因为它的 native 关键字

native 关键字:添加了 native 关键字的方法,说明超出了 Java 的作用范围,需要调用底层的 C 语言库实现。

即使用 native 修饰的方法,属于本地方法,会在本地方法栈(Native Method Stack)中进行登记,执行时会通过本地方法接口(Java Native Interface,JNI)调用本地方法库。

4. 方法区

JVM 简单笔记

方法区(Method Area)是被所有线程共享的区域,所有字段和方法字节码,以及一些特殊方法如构造方法、接口定义都存放在此处。简单来说,所有定义的方法和信息都保存在该区域,该区域属于共享空间。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

静态变量(static)、常量(final)、类信息(构造方法、接口定义)、运行时的常量池都存在方法区中,而实例变量存在堆内存中。

需要注意,JDK 版本号为 1.6 时常量池在方法区,1.7 时在堆区,1.8 时在元空间。

jvm 堆、栈 、方法区概念和联系

5. 栈、堆

简单说一下栈和堆两者的特点和区别:

  1. 栈内存中存放基本数据类型、局部变量,用完就消失,且方法(函数)执行时会以栈帧的形式压入栈中;
  2. 堆内存中存放 new 创建的实例化对象及数组,用完之后依靠垃圾回收机制不定期自动清除;

JAVA中的栈和堆

栈和堆存储的数据不同,它们出现的问题也不同,举两个例子:

public class StackDemo {
    public static void main(String[] args){
        a();
    }

    public static void a(){
       b();
    }

    public static void b(){
        a();
    }
}

这段代码中,方法 a 调用方法 b,方法 b 又调用方法 a,形成了不会停止的递归调用,而调用一个方法时,会在栈中压入栈帧,因此这段代码的运行结果就是:Exception in thread "main" java.lang.*Error,栈溢出错误。

public class HeapDemo {
    public static void main(String[] args){
        String str = "Qiyuanc";
        while(true){
            // 指数倍增
            str += (str + "22222");
        }
    }
}

这段代码会一直创建新的对象(字符串相加创建的是新对象),而堆中存放的就是实例化对象。在这个死循环中一直创建新对象,最终的结果就是:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space,内存溢出错误,即OOM。

6. 新生代、老年代、永久代

JVM中的堆,一般分为三部分:新生代、老年代、永久代:

JVM 简单笔记

6.1 新生代

新生代:主要用于存放新生的对象,一般占据堆的1/3空间。由于经常创建对象,所以新生代也会经常触发MinorGC 进行垃圾回收。

新生代又分为 Eden 区、ServivorFrom 区(S0 区)、ServivorTo 区(S1 区)三个区:

  1. Eden 区:本区是 Java 新对象的出生地(如果新对象占用内存很大,则会直接进入老年代)。当 Eden 区内存不够时就会触发 MinorGC,对新生代区进行一次垃圾回收;
  2. ServivorFrom 区:保留上一次 GC 的幸存者,也是下一次 GC 的被扫描者,会与 ServivorTo 区互换;
  3. ServivorTo 区:与 ServivorFrom 区相同。

其中 S0 区与 S1 区涉及到 Minor GC 的算法,复制算法:

  1. 先将 Eden 区和 S0 区存活的对象复制到 S1 区(如果有对象的年龄达到了老年的标准,一般是15,则复制到老年代区);
  2. 将这些对象的年龄 +1;
  3. 清空 Eden 区和 S0 区的对象,交换 S0 区和 S1 区,即下一次扫描的是 S1 区,复制到 S0 区。

复制算法不会产生内存碎片。

6.2 老年代

老年代用于存放已经存活了很久的对象,当老年代区也满了的时候,会触发 Full GC(Major GC)。不过因为老年代的对象比较稳定,所以 Full GC 不会经常触发。

Major GC的触发机制:在进行 Major GC 前一般都先进行了一次 Minor GC,使新生代的对象进入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 Major GC 进行垃圾回收腾出空间。

Major GC 采用标记—清除算法:

  1. 先扫描一次老年代,标记所有存活的对象;
  2. 然后回收没有标记的对象。

Major GC 的耗时较长,因为要扫描一次再回收,同时可以注意到,标记—清除算法清理出的空间不一定是连续的,即可能产生内存碎片。

当老年代空间也满了的时候,就会抛出 OutOfMemory 异常。

垃圾回收之MinorGC,MajorGC和FullGC的区别

6.3 永久代

永久代指内存的永久保存区域,主要存放类(Class)和元数据(Meta)的信息。看着有点熟悉,其实就是上面提到的方法区。Class 在被加载的时候被放入永久区,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区进行清理。这也导致永久代区域会随着加载的 Class 的增多而溢出,最终抛出 OOM 异常。

而在 Java1.8 中,永久代被移除,由元空间所取代。

元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。

6.4 调优

说了那么多,最后的目的还是为了 JVM 调优,如通过

// -Xms8m -Xmx8m -XX:+PrintGCDetails

可以将虚拟机最大最小内存都设置为 8M,同时输出垃圾回收的细节。

对之前的 HeapDemo 应用这个参数,可以看到具体的垃圾回收细节为

[GC (Allocation Failure) [PSYoungGen: 1535K->488K(2048K)] 1535K->640K(7680K), 0.0010702 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 2002K->496K(2048K)] 2154K->1036K(7680K), 0.0008752 secs] [Times: user=0.09 sys=0.03, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1687K->384K(2048K)] 2228K->1884K(7680K), 0.0010100 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1572K->368K(2048K)] 3072K->2252K(7680K), 0.0008888 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1581K->0K(2048K)] [ParOldGen: 4956K->2201K(5632K)] 6537K->2201K(7680K), [Metaspace: 3463K->3463K(1056768K)], 0.0046226 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1536K->0K(2048K)] [ParOldGen: 5273K->4505K(5632K)] 6809K->4505K(7680K), [Metaspace: 3463K->3463K(1056768K)], 0.0037556 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 30K->32K(2048K)] 4535K->4537K(7680K), 0.0004138 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 32K->32K(2048K)] 4537K->4537K(7680K), 0.0004336 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(2048K)] [ParOldGen: 4505K->4501K(5632K)] 4537K->4501K(7680K), [Metaspace: 3463K->3463K(1056768K)], 0.0051679 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4501K->4501K(7680K), 0.0006739 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4501K->4483K(5632K)] 4501K->4483K(7680K), [Metaspace: 3463K->3463K(1056768K)], 0.0066683 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2048K, used 91K [0x00000000ffd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 1536K, 5% used [0x00000000ffd80000,0x00000000ffd96fd0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 5632K, used 4483K [0x00000000ff800000, 0x00000000ffd80000, 0x00000000ffd80000)
  object space 5632K, 79% used [0x00000000ff800000,0x00000000ffc60de8,0x00000000ffd80000)
 Metaspace       used 3495K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 384K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

可以看到其中的 GC 过程,和新生代 PSYoungGen、老年代 ParOldGen、元空间 Metaspace 的使用情况,差不多就这样。

7. 总结

简单了解一下 JVM,没什么深入的内容,不过就先这样吧。

希望是无限的,而失望是有限的,没有人或物能承载无限的希望,最终都会变为有限的失望。

上一篇:OutOfMemoryError异常总结


下一篇:gc root总结