JVM虚拟机基础

1. 什么是JVM

  JVM(Java Virtual Machine)是用来保证java的跨平台性的,将.class字节码文件转换成操作系统能够直接识别的指令,它的本质是一个进程。

2. Java对象编译过程

 

 主要分为两个部分:

源文件编译成字节码对象

字节码由java虚拟机解释执行

3. 类加载器

  类的加载是将类的.class文件中的二进制文件读取进内存中,将其放进运行时数据区的方法区内,然后在堆区创建一个java.lang.class对象,用来封装类在方法区内的数据结构。

  注意:jvm主要是在程序第一次主动使用类的时候,才去加载该类,jvm并不是一开始就去嫁娶程序中所有的类,而是到不得不用的时候才加载它,并且只加载一次。

A. 类加载器

  1. BootStrap ClassLoader(根类/启动类加载器):负责加载$JAVA_HOME中jre/lib/rt.jar相关的jar包,底层使用C++实现。
  2. Extension ClassLoader(拓展类):负责加载$JAVA_HOME中jre/lib/ext/*.jar,JDK1.9的时候更名为Platform ClassLoader。
  3. AppClassLoader(应用程序加载器):负责加载classpath中指定的jar包及目录下的class。JDK1.9的时候更名为System ClassLoader。
  4. CustomerClassLoader(用户自定义加载器):Tomcat和JBoss都对根据J2EE规范自行实现。其实就是继承ClassLoader重写findClass(),loadClass()

B. 类加载器的顺序

类加载器加载顺序:BootStrap ClassLoader-->Extension ClassLoader -->App ClassLoader-->User ClassLoader

类加载时检查的顺序:User ClassLoader-->App ClassLoader-->Extension ClassLoader-->BootStrap ClassLoader

类加载器的“检查机制”://双亲委派机制,缓存机制,全盘加载

4. JVM的内存模型

A. 概念解读

  1. 方法区:方法区是被线程共享的区域,存储以被虚拟机加载的类信息,常量,静态变量等。
  2. 堆:堆也是被所有线程共享的区域,对象的实例都在堆中分配。
    1. 指针碰撞法:将对内存中的内存进行划分,已分配的内存和分配的内存在不同的侧,通过指针作为分界点,在需要分配对象的时候,指针只需要向空闲的一侧移动与对象大小相等的距离。
    2. 空闲列表法:在内存中维护一个表,存储内存块的信息,在需要分配内存的时候,去列表中寻找一个空闲的内存块分给给对象,并更新列表上的记录。
  3. 虚拟机栈:虚拟机栈是线程独享的,存储每个线程中方法运行所需的数据,指令返回通信地址等信息。
  4. 程序计数器:程序计数器是线程独享的,执行当前线程正在执行字节码的地址。
    1. 程序运行在cpu中,cpu是基于时间片的调度策略,所以需要记住线程的运行指令地址。假设由于线程的时间片用完被挂起,当线程又获得时间片需要继续运行时,就需要从程序计数器获取该线程已经执行到的指令,并继续执行下去。
  5. 本地方法栈:执行虚拟机中Native本地方法。
  6. 直接内存:向操作系统借的内存。

 B. 画出下面代码的内存模型图

        public class A {
            public void print() {
                System.out.println("h");
                System.out.println("e");
                System.out.println("l");
                System.out.println("l");
                System.out.println("o");
            }

            public static void main(String[] args) {
                A a = new A();
                a.print();

                //创建两个线程对象, 调用A#print();
                //线程是CPU运行的基本单位, 创建销毁由操作系统执行.
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        a.print();
                    }
                }).start();

                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        a.print();
                    }
                }).start();
            }
        }
View Code

C. 线程安全问题

    当多个线程访问一个对象时,都可以得到正确的结果,那么这个线程是安全的。

1.  线程安全是什么,分为什么程度?

  • 不可变对象:不可变的对象一定是线程安全的,不可变对象有String,Number的部分子类,Long,Double,BigInteger,BigDecimal。
  • 绝对线程安全:不管运行时环境如何,调用者都不需要任何额外的同步措施。
  • 相对线程安全:就是我们通常意义上所讲的线程安全,需要保证这个对象单独操作是线程安全的。Vector,HashTable,Collections的synchronizedCollection方法包装的集合。
  • 线程兼容:是指对象本身并不是安全的,但是用过调用端正确的使用同步手段保证对象在并发的环境中可以安全的使用。
  • 线程对立:是指无论调用端是否采用了同步措施,都无法在多线程中并发使用的代码。

2. 在JVM内存模型中哪块区域会出现的线程安全问题?

  线程安全问题需要产生在:多线程,并发,操作同一数据。所以说堆是被多个线程共享的空间,其他都不被多个线程共享,所以只有堆会出现线程安全问题。

D. 内存溢出问题

   在Java中,内存泄漏就是内存中存在一些被分配的对象。A:这些对象是可达的,即GC Roots到对象之间有引用链相连,那么GC是无法回收的。B:这些对象是无用的,即程序以后不会再用到这些对象。

  在JVM虚拟机中出了程序计数器理论上不会发生内存溢出,其他几个运行时区都可能发生OOM(Out Of  MemoryError)

5. JVM垃圾回收算法

A. 垃圾回收算法理论---复制算法

  复制算法将可用的内存容量分为两个部分,每次只使用其中的一块,当这一块内存用完,就会将还存活的对象放到另外一块区域上,然后再把自己已使用的内存空间一次性清理掉,这样每次清理就对整个整个内存空间的半区进行回收。内存分配的时候也不需要考虑内存碎片的问题。

  但是复制算法也有很多缺点:

  • 需要提前预留一般的内存区域用来存活的对象,这样导致整个可用的区域就减小了一半,总体的GC就更加频繁了。
  • 如果存活对象多,复制成本上升。
  • 如果老年代的对象多,那么是无法使用这个算法的。

B. 垃圾回收算法理论---标记清除

  先将待回收的对象标记,之后在统一清除

  缺点:标记和清除的效率不高,标记清除后产生大量不连续的内存碎片,这就会导致后续分配较大对象的时候,找不到连续的地址空间。

C. 垃圾回收算法理论---标记整理

  标记整理算法和标记清除算法很类似,只是在清除并不是直接清除,而是让存活的对象向一端移动,然后直接清理掉边界意外的内存。

D. JDK1.7的堆内存垃圾回收算法

  1. 新创建的对象首先会分配给Eden(如果创建的对象比较大会直接进入年老代),这个时候两个suivivor1是空的,随着对象的创建Eden被填充满,之后会触发Minor GC,这一阶段采用垃圾回收算法是复制算法,将存活的对象复制到survivor,无用的对象直接回收。接下来新来的对象还是会先分配Eden,随着Minor GC的进行每次对象的年龄都会加1,达到15时对象就进入老年代。
  2. 随着年轻代的清理,达到年龄还存活对象会存到年老区,到年老代的对象越来越多会触发Major GC,这一阶段的垃圾回收算法采用的是标记清除或者标记整理法。每次进行Minor GC的时候都会检查年老代剩余空间是否能够存下晋升到年老代的对象,如果空间不足就会触发FULL  GC。
  3. 持久代也就是方法区:存储着方法的元数据信息。class文件会被加载到持久代,在程序运行过程中GC不会对持久代进行回收。

E. JDK1.8的堆内存垃圾回收算法     

  G1垃圾回收器是出现在JDK1.8,在1.9作为默认的垃圾回收器。                        

 G1回收器首先将堆内存空间划分成一个个相等region块,每一个块的地址都是连续的,还是采用分代收集算法。每个块也会充当Eden,Survivor,Old三种角色。但是他们不是固定的。

6. JVM垃圾回收器

A. Serial收集器

  单线程收集器:单线程不仅仅说明它只会使用一个CPU或者一个收集线程去完成垃圾收集的工作,更重要的是在收集的时候,必须停止其他的工作线程,直到垃圾收集完毕。对于单核Cpu来说这种方法简单高效。

 B. ParNew收集器(算法是Stop World)

  除了采用多线程收集以外和Serial收集器是一样的。

C. Parallel Scavenge收集器

  Parallel ScaVenge收集器是一个新生代的收集器,并且使用复制算法,而且是一个并行的线程收集器。

  1. 其他收集器是尽量缩短垃圾收集时候用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。
  2. 吞吐量等=运行用户代码时间/(运行用户代码时间+垃圾回收时间)

 

D. Serial Old收集器

  Serial Old是Serial的老年代版本,使用标记整理算法

 E. Parallel Old收集器

  Parallel Old是Parallel Scavenge的多线程版本,使用的是标记整理算法

 F. CMS收集器

  基于标记-清除算法,一种以获取最短回收时间为目标的收集器。适用于注重服务的响应速度的,希望系统停顿的时间最短。

  1. 初始标记:标记GC Root能直接到的对象,速度很快但是仍然存在Stop The World 的问题。
  2. 并发标记:找出存活对象,且用户线程可以并发执行。
  3. 重新标记:为了修正并发标记期间因用户程序继续执行而导致标记产生变动的那一部分对象的标记记录,仍然存在Stop of World的问题。
  4. 并发清除,对标记的对象进行回收。是与用户线程一起并发执行的。

G. G1收集器

https://www.cnblogs.com/chenpt/p/9803298.html

 

原文地址:https://www.cnblogs.com/qidi/p/11725676.html