深入理解Java虚拟机

[学习笔记] 深入理解Java虚拟机

标签(空格分隔): java


第二部分.自动内存管理机制

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

深入理解Java虚拟机_第1张图片
java内存区域

2.1.方法区(永久代)

  • 存放:
    • 类常量
    • 字符串常量
    • 静态变量

2.2.堆(内存中最大的一块)

深入理解Java虚拟机_第2张图片
堆内存划分
  • 存放:
    • 实例
    • 数组元素
  • 划分:
    • 年轻代(young generation):新创建对象的存放区域.当年轻带被用完时,会触发Minor GC.
      • 伊甸区(eden)
      • 幸存区0(survivor0)
      • 幸存区1(survicor1)
        年轻代特点:
    • 老年代(old ggeneration):包含了长期存活的对象和经过多次MinorGC后依然存活下来的对象.通过老年代被占满的时候进行MajorGC.Major GC会花更多的实践.
    • 永久代(Perm):
  • Stop the World事件
    所有的来及回收事件都是stop the world事件,因为所有的应用线程都会停下来直到操作完成.
    因为年轻代里面的对象都是临时对象,执行Minor GC非常快,所以应用不会受到Stop the World影响.

2.3.虚拟机栈

  • 存放:
    • 局部变量.
      • 注意:局部变量表
  • “栈帧”:每个方法创建的时候都会创建一个栈帧,并压入帧栈.
    用于存放局部变量表、操作数、动态连接、方法出口等信息.
  • 操作数栈
    java没有寄存器,所有参数传递使用的操作数栈。
  • 栈上分配
    • 小对象(一般几十个byte)在没有逃逸的情况下,可以直接分配在栈上。
  • 栈、堆、方法区的交互:

注意
- 当线程栈深度超过 虚拟机栈所允许的最大深度的时候,就会抛StackOverflowError.
- 如果虚拟机栈扩展时,无法获取到足够的内存,就会抛OOM.

2.4.本地方法栈

  • 存放Native方法。
  • 在HotSpot中,本地方法栈已被归入虚拟机栈。

2.5.程序寄存器

  • 存储:
    • 如果线程执行的是一个java方法,那么存储的是正在执行的虚拟机的字节码指令地址.
    • 如果线程执行的时一个native方法,那么存储的时undefined.
  • 特点:
    • 存取速度非常快,程序不可控.
    • 这块区域没有规定没有任何OutOfMemory.

3.典型的垃圾回收算法

http://icyfenix.iteye.com/blog/715301

3.2 对象已死吗?

java堆中几乎存放了所有的对象,垃圾回收器在回收之前需要确定哪些对象还”活着“,哪些对象已经”死了“

3.2.1 引用计数法(Java没有使用)

优点:实现简单,效率高
缺点: 无法处理循环引用
原理:给对象添加一个引用,每当有一个地方引用这个对象的时候,计算器就+1,当引用失效的时候,引用就-1,当引用计算器=0的时候表示该对象不被引用。
    需要注意的是:引用计数器很难解决对象之间循环引用,所以主流java虚拟机没有使用引用计数器来管理内存。

3.2.2 可达性分析法

深入理解Java虚拟机_第3张图片
可达性分析法
- 强引用:GC永远不会回收引用对象。
- 软引用SoftReference:在内存溢出之前,会进行第二次回收,如果回收之后还没有足够的内存,才会抛出OOM。
- 弱引用WeakReference:引用对象只能生存到下一次GC之前。
- 虚引用(幽灵引用、幻影引用)PhantomReference:唯一目的就是GC在回收时收到一个系统通知。

3.2.3 finalize

finalize只会被执行一次。

一个对象真正被回收需要经历两次标记过程:
如果一个对象在可达性分析后没有与GC ROOT相连接的引用链,那就被被第一次标记并且进行一次筛选:
如果finalize没有被重写或调用过,虚拟机会让认为“没必要执行”。
否则,会执行finalize方法

finalize:
如果调用finalize方法,那么这个对象将会被放到一个队列里面[虚拟机建立并执行Finalizer线程执行调用],等待被回收。

finalize是对象逃脱死亡命运的最后机会。只要与引用链上的任何一个对象建立链接即可。

3.2.4方法区回收

  • 回收内容:
    • 废弃常量:没有地方引用
    • 无用的类:
      需要同时满足以下条件:
      1. java堆中没有这个类的实例;
      2. 加载该类的ClassLoader已经被回收;
      3. 该类的Class字节码没有任何地方被引用。没有任何地方通过反射调用该类的方法。

3.3 垃圾收集算法

3.3.1. Mark-Sweep(标记-清除算法)

- 最基础的算法
- 最容易实现的算法
- 算法:分为两个阶段:标记阶段和清除阶段。
    - 标记阶段:就是标记出所有要被回收的对象。
    - 清除阶段:回收被标记的要被回收的内存区域。
- 最大缺点:容易产生内存碎片。
深入理解Java虚拟机_第4张图片
标记清除算法

3.3.2. Copying(复制算法)

- 为了解决Mark-Sweep算法的缺点。
- 算法:将内存按容量划分成大小相等的两块,每次只使用其中的一块。
    当内存用完了,就将还存活的对象复制到另一块内存上,然后将已使用的内存空间一次性清除掉,这样一来就容易出现内存碎片。
- 优点:算法简单,运行高效而且还不容易出现内存碎片。
- 缺点:内存使用代价高,因为能够使用的内存只有原来的一半。
深入理解Java虚拟机_第5张图片
Copy算法

3.3.3. Mark-Compact(标记-整理算法)

- 为了解决Copy算法,充分利用内存空间。
- 算法:标记阶段和Mark-Sweep算法一样,但是完成标记之后,他不是直接清除被标记的内存区域,而是将存活对象往一端移动,然后清除端外的区域。
深入理解Java虚拟机_第6张图片
Mark-Compact算法

3.3.4.Generational Colletion (分代收集算法)

- 目前大多数jvm采用的算法
- 算法核心思想:根据对象的存活周期将内存划分为若干个区域。
    一般情况下将堆内存分为:新生代和老年代。
        - 新生代特点:每次内存垃圾回收时,有大量内存对象要被回收。
        - 老年代特点:每次垃圾回收时,只有少量对象需要被回收。
- 目前大多数垃圾收集器:
    - 对于新生代都采用Copying算法。
        因为新生代每次都有大量对象被回收,也就是说复制的次数较少,但是实际中并不是按1:1来划分新生代的内存空间。
        一般将新生代划分为一块较大的Eden区和两块较小的Survivor区(一般为8:1:1),每次使用Eden区和一块Survivor区。当进行回收时,一般都将Eden区和Survivor中还存活的对象复制到另一块Survivor中,然后清除Eden区和刚才使用过的Survivor区。
    - 对于老年代都采用Mark-Compact算法。
        因为老年代每次回收都只有少量对象回收。

3.4 hotspot算法的实现

3.4.1枚举根节点

3.4.2安全点

3.4.3安全区

3.5 垃圾收集器

参考资料:
https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/

3.5.1 serial收集器(新生代串行收集器)

深入理解Java虚拟机_第7张图片
串行收集器运行示意图
  • 特性:
    • 单线程,采用复制算法.
    • 简单高效
  • 基本原理:在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束.
  • 使用场景:Client模式下默认的新生代收集器

3.5.2 ParNew 收集器

深入理解Java虚拟机_第8张图片
ParNew收集器运行示意图

就是Serial收集器的多线程版本.

  • 特性:

    • 单CPU不如Serial收集器.
  • 基本原理:让垃圾线程和用户线程同时工作.

  • 使用场景:

    • 在Client模式下的首选.
    • 目前只能与CMS收集器配合工作.
  • XX:ParallelGCThreads:通过这个参数来设置GC线程数量.

  • 使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

3.5.3 parallel scavenge 收集器(新生代并行回收收集器)“吞吐量优先收集器”

  • 特性:
    • 吞吐量优先收集器
  • 和ParNew收集器的区别:关注点不同
    其他收集器的关注点是:在垃圾收集时尽量缩短用户线程的停顿时间.
    parallel scaveng的关注点是:达到一个可控的吞吐量.
    概念:吞吐量 = 代码运行时间/(代码运行时间+垃圾回收时间)
  • 基本原理:通过设置两个参数来精准控制吞吐量.
    • 最大垃圾收集停顿时间:-XX:MaxGCPauseMills
    • 直接设置吞吐量大小:-XX:GCTimeRadio

概念:
并行:指多条垃圾线程在并行工作,用户线程处于等待状态.
并发:垃圾收集线程和用户线程同时运行,垃圾线程在另一个CPU上运行.

3.5.4 serial old 收集器(老年代串行收集器)

深入理解Java虚拟机_第9张图片
老年代串行收集器
  • 特性:

    • 采用标记-整理算法.
  • 在Server使用场景:

    • 在jdk1.5之前的版本和Parallel Scavenge配合使用.
    • 作为CMS收集器的备选预案.

3.5.5 parallel old 收集器(老年代并行回收收集器)

深入理解Java虚拟机_第10张图片
Prallel Scavenge / Parllel Old 收集器运行示意图
  • 特性:
    • 使用多线程和标记-清理算法.

3.5.6 cms 收集器

Concurrent Mark Sweep


深入理解Java虚拟机_第11张图片
Concurrent Mark Sweep
  • 特性:

    • 采用标记-清理算法.
    • 以获取最短停顿时间为目标.适用于互联网网站、BS系统的服务器上。
  • 特点:

    • 初始标记[Stop-The-World]:Root可以直接关联到的对象。
    • 并发标记[和用户线程一起]:主要标记过程,标记全部对象。
    • 重新标记[Stop-The-World]:在正式清理前做修正。
    • 并发清理[和用户线程一起]:基于标记结果,清理对象。
  • 优点:并发收集、低停顿.

  • 缺点:

    1. 对CPU相当敏感。
    2. 无法处理浮动垃圾。

3.5.7 g1 收集器

  1. 面向服务器端."标记-整理"
  2. 分代收集:不需要配合其他GC收集器就能管理整个GC堆。
  3. 空间整合
  4. 可预测的停顿
  • 特点:
    • 初始标记:标记下GC Roots能直接关联到的对象,需要停顿线程,但是耗时很短。
    • 并发标记:需要从GC Roots中对对象进行可达性分析,找出存活的对象。这个阶段耗时较长,但可和用户线程并发执行。
    • 最终标记:修正在程序并发标记期间因用户程序继续运行而导致标记产生变动的那一部分记录。
    • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。
深入理解Java虚拟机_第12张图片
image_1bb13kfrh1l1l1puio61okhmqu9.png-27.9kB

理解gc日志

垃圾收集器参数总结

  • 对象优先在Eden区分配
  • 大对象直接进入老年代
  • 长期存活的对象进入老年代

5、Java堆内存开关

深入理解Java虚拟机_第13张图片
Java堆内存开关

http://p3.pstatp.com/large/f740004e7f539099df0
深入理解Java虚拟机_第14张图片
image_1b6ha87jm1qfv1fc5kvj1vnr13qv9.png-364kB

第三部分.虚拟机执行子系统

  1. 类文件结构
  2. 虚拟机类加载机制
  3. 虚拟机字节码执行引擎
  4. 类加载及执行子系统的案例与实战

7 虚拟机类加载机制

7.1 类加载的时机

深入理解Java虚拟机_第15张图片
类的生命周期
  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

加载、验证、准备、初始化、卸载 这五个步骤的顺序是确定的.
但是,解析 这一步可能在初始化之后完成,这是为了指出Java语言的运行时绑定.

什么情况下需要开始类加载的第一个阶段:加载?
1)new实例化对象的时候、读取或设置一个静态字段的时候、调用一个类的静态方法的时候
2)反射调用的时候.
3)初始化一个类的时候,如果发起其父类还没有被初始化的的时候,先触发父类的初始化方法
4)
5)

7.2 类加载的过程

类加载的过程:加载、验证、准备、解析、初始化

1)加载:
查找并加载类的二进制数据
2)链接:
验证:确保被加载的类符合JVM规范、没有安全性方面的问题.
准备:为静态变量分配内存,并将其初始化为默认值.
解析: 把虚拟机常量池中的符号引用转换为直接引用.
3)初始化
为类的静态变量赋予正确的初始化值.

7.2.1 加载

在加载阶段,虚拟机主要完成以下3件事:
1. 通过一个类的全名获取定义此类的二进制字节流;
2. 将这个字节流所代表的静态数据结构转换成方法区的运行时数据结构;
3. 在内存中生成一个代表该类的Class对象,作为方法区这个类的各种数据的访问入口.

7.2.2 验证

7.3 类加载器

7.3.1 类与类加载器

7.3.2 双亲委派模型

  • 类加载器类型:

    1. 启动类加载器(Bootstrap ClassLoader):使用C++语言实现,是虚拟机的一部分.
    2. 其他的类加载器:Java语言实现,独立于虚拟机外部,并且全部继承于抽象类java.lang.ClassLoader。
  • 常用的系统提供的类加载器:

    • 启动类加载器(Boostrap ClassLoader)
    • 扩展类加载器(Extension ClassLoader)
    • 应用程序类加载器(Application ClassLoader)
  • 双亲委派模型

    深入理解Java虚拟机_第16张图片
    类加载器的双亲委派模型

    • 双亲加载模型的工作过程:
      如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,自加载器才会尝试自己去加载.

    • 为什么要使用双亲加载模型?双亲加载模型的好处?
      保证Java核心库的安全性。
      (如果用户自己编写一个java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证)

    • 资料:
      http://www.cnblogs.com/sunniest/p/4574080.html
      http://blog.csdn.net/huachao1001/article/details/52297075

  • 双亲委派模型的实现

protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //1.检查请求的类是否已经被加载过了
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                    // 如果父类为null,调用父类的加载器
                        c = parent.loadClass(name, false);
                    } else {
                    //如果父类不为null,则调用bootstap的类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //如果父类加载器抛出ClassNotFoundException
                    //说明父类加载器无法完成加载请求
                }

                if (c == null) {
                    // 在父类加载器无法加载的时候
                    // 再调本身的findClass方法来进行类加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

7.3.3 破坏双亲委派模型


第四部分.程序编译与代码优化


第五部分.高效并发

12.java内存模型与线程安全

12.1. 主内存和工作内存

- 内存模型的主要目标:定义程序中各个变量的访问规则。
- 线程对变量的所有操作(读取、赋值等)都在工作内存中进行,不能再主内存中操作。
- 线程间变量值的传递均需要通过主内存来完成。
- 虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。
深入理解Java虚拟机_第17张图片
线程、主内存、工作内存三者的交互关系

12.2. 内存间的相互操作

  • 一个变量如何从主内存copy到工作内存
  • 如何从工作内存同步回主内存。
  • Java内存模型中定义了以下8种操作来完成主内存和工作内存之间交互的实现细节:
    • lock: 作用于主内存的变量,它把一个变量标示为一条线程独占的状态。
    • unlock:作用于主内存的变量,它把一个处于lock状态的变量释放出来,释放后的变量才可以被其他线程锁定。
    • read:作用于住内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的load动作使用。
    • load:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入到工作内存的变量副本中。
    • use:作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码的指令时会执行这个操作。
    • assign:作用于工作内存的变量,它把过一个执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量复制的字节码指令时执行这个操作。
    • store:作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的write操作。
    • wirte:作用于主内存的变量,它把store操作从工作内存中得到的变量值放到主内存的变量中。
    • 8中基本操作需要满足以下规则:


      深入理解Java虚拟机_第18张图片
      image_1b5ucg6rn1oha4a8mp9piv3v69.png-150.6kB

12.3.volatile型变量的特殊规则

1,当一个变量的定义为volatile变量之后,此变量对所有线程是可见的。
    - 基于volatile变量的运算在并发下是安全的???
    java里面的运算并非原子操作,所以volatile变量在并发条件下也是不安全的。
2. 禁止指令重排序优化。

12.4.先行发生原则(happen-before原则)

13.线程安全与锁优化


问题

  1. PSYoungGen/PSOldGen/PSPermGen区别

    PS是Paraller Scavenge的简称。
    PSYoungGen:新生代
    PSOldGen:老年代
    PSPermGen:永久代

    • SUN JVM GC 使用是分代收集算法,即将内存分为几个区域,将不同生命周期的对象放在不同区域里.
      新的对象会先生成在Young area,也就是PSYoungGen中
      在几次GC以后,如过没有收集到,就会逐渐升级到PSOldGen 及Tenured area(也就是PSPermGen)中。
    • 在GC收集的时候,频繁收集生命周期短的区域(Young area),因为这个区域内的对象生命周期比较短,GC 效率也会比较高。而比较少的收集生命周期比较长的区域(Old area or Tenured area),以及基本不收集的永久区(Perm area)。

参考

  1. Java 内存模型及GC原理
  2. java 内存垃圾回收模型

你可能感兴趣的:(深入理解Java虚拟机)