深入理解JVM-JAVA高级特性与最佳实践


对JAVA程序员来说JAVA虚拟机可以说既熟悉又神秘,很少有JAVA程序员能够抑制自己探究它的冲动。本文源自《深入理解Java虚拟机》并对该书核心内容精炼总结,持续更新...

运行时数据区域
1、程序计数器
    当前线程所执行的字节码的行号指示器
2、JAVA虚拟机栈
    线程私有,生命周期与线程相同,每个方法在执行的同时会创建一个栈帧用于存储局部变量表,操作数栈等。每个方法从调用直至执行完成,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
3、本地方法栈
    区别于JAVA虚拟机栈的是本地方法栈是为Native方法服务的。
4、JAVA堆
    用于存放对象实例,从内存回收的角度看,Java堆被分为新生代(Eden,From Survivor,To Survivor)和老年代。
5、方法区
    用于存储已被虚拟机加载的类信息,常量,静态变量等数据。该区域垃圾回收的目标主要是针对常量池的回收和对类型的卸载。


对象创建
1)内存分配
如果Java堆内存是绝对规整的,已分配的内存在一边,未分配的在另外一遍,因此内存分配只需要一个中间指针向空闲区滑动,这种分配方式叫“ 指针碰撞”。
如果Java堆内存是不规整的,虚拟机需要维护一个列表记录那个内存块是可用的,这种分配方式叫“ 空闲地址法”。
选择哪种分配方式是由Java堆是否规整决定的。
2)频繁的对象创建和对象指针更新的线程安全问题:
        1.JVM采用CAS+失败重试的方式保证更新操作的原子性。
        2.内存分配动作按照线程划分在不同的内存空间上进行。线程分配缓冲(TLAB)。

对象的内存布局
//...



OutOfMemoryError异常

1)Java堆溢出--要分清到底是内存泄露还是内存溢出。

2)虚拟机栈和本地方法栈溢出
        线程分配的栈容量越大,建立线程的数量就越小。
        虚拟栈内存和本地方法栈大小=机器剩余内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量)-程序计数器消耗(很小)
        因此解决栈溢出的方式:1.减少堆容量大小   2.减少线程栈容量大小

3)方法区和运行时常量池溢出
        方法区溢出主要是由于方法区中的常量池导致,重点关注String,以及str.intern()方法。

垃圾收集器和内存分配策略
JAVA内存区域中程序计数器、虚拟机栈、本地方法栈内存分配和回收具有确定性。

垃圾回收主要是针对Java堆和方法区

判断对象是否可用的算法:
1、引用计数算法
很难解决对象之间循环引用的问题,因此JAVA虚拟机并没有采用该方式判别可回收对象。

2、可达性分析算法
通过一系列称为“GC ROOT”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当对象没有与任何引用链相连接,那么证明该对象是不可用的--可回收。
GC ROOT 有:
    虚拟机栈中的引用对象
    方法区中的静态属性引用的对象
    方法区中常量引用的对象
    本地方法栈中JNI引用的对象

对象正式宣告死亡至少要经历两次标记过程,如果对象经过可达性分析后发现没有与GC Roots相连接,那么会进行第一次标记,并进行一次筛选判断是否有必要执行finalize()方法(主要是判断finalize是否被覆盖,或者已经执行过),如果有必要该对象会进入F-Queue队列。稍后会有一个Finalizer线程去执行它,并进行二次标记,如果对象执行finalize()时,重新和引用链建立连接,那么二次标记会把该对象移出即将回收的集合。

垃圾回收算法:
1、标记-清除算法
        标记处所有需要回收的对象,然后统一回收。
2、复制算法(新生代)
        HotSpot虚拟机默认Eden和Survivor的大小比例是 8:1
        每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor中还存活的对象一次性的复制到另一块Survivor中,然后清理掉刚才用过的Eden和Survivor空间。
        如果Survivor空间不够用,需要依赖其他内存(老年代)进行分配担保。
        该方式在对象存活率高的情况下不适用,因为会浪费大量的内存空间。因此老年代一般使用标记-整理算法。
3、标记-整理算法(老年代)
        依然和标记清除一样,但后续不直接对可回收对象进行清除,而是让所有存活对象都向一端移动。然后清理掉边界以外的内存。

4、分代收集算法


垃圾回收器:
---新生代---
Serial
单线程,必须STW,Client下默认的新生代收集器。

ParNew
是Serial收集器的多线程版本,是许多Server模式下默认的首选新生代收集器,可以和CMS收集器配合使用。单线程效率低于Serial收集器。

Parallel Scavenge
该收集器的主要目的是达到吞吐量(运行用户代码时间(/运行用户代码时间+垃圾回收时间) )的可控性,具有自适应调节策略。

---老年代---
Serial Old
是Serial收集器老年代版本,单线程收集器,使用标记-整理算法。也是Client模式下的虚拟机使用。

ParNew Old
使用多线程和标记-整理算法,是注重吞吐量和CPU资源敏感的场合,可以优先考虑使用Parallel Scavenge + ParNew Old收集器。

CMS
以获取最短的停顿时间为目标,基于标记-清除算法,整个过程分为四个步骤:
    初始标记-并发标记-重新标记-并发清除。其中初始标记和重新标记需要STW。耗时最长的并发标记和并发清除过程收集器线程可以和用户线程同时工作,因此具有较短的停顿时间。


G1
G1进行垃圾回收时,将整个JAVA堆划分成多个大小相等的Region,虽然还保留了新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,他们都是Region的集合。G1追踪各个Region里的垃圾堆积的价值大小,维护一个优先列表,
每次优先回收价值最大的Region,尽可能提高收集效率。



内存分配与回收策略:
1、对象优先在Eden分配
2、大对象直接进入老年代,避免Eden和两个Survivor区之间发生大量的内存复制。通过设置  
        XX:PretenureSizeThreshold=xxx  ,大于xxx的对象会直接在老年代分配。
3、长期存活的对象将进入老年代
        JVM为每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经历第一次Minor GC后仍然存活,并能被Survivor容纳的话,将被移动到Survivor中,并将对象年龄设为1,每经历过一次Minor GC年龄就会+1,默认15,就会被晋升到老年代。  老年代晋升阈值:-XX:MaxTeunringThreshold设置。


类加载机制:
类的生命周期:
    加载-> 验证->准备->解析->初始化->使用->卸载     
                 ( 连接阶段Linking)

1、加载完成的3件事情:
            >>通过类的全限定名来获取定义此类的二进制字节流。
            >>将字节流所代表的静态存储结构转化为方法区的运行数据结构。
            >>生成代表这个类的class对象,作为方法区中这个类的访问入口。
        注:加载完成后,虚拟机外部的二进制字节流就会存储在方法区中。

2、验证
    --目的:确保Class文件的字节流中包含和信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
    

3、准备
    正式为类变量分配内存(static修饰的变量)并设置类变量初始值(0/false/0.0f/null等而不是变量的实际值)。
    特殊情况:public static final int value = 123; 准备阶段会被赋值为123.而不是0.

4、解析
      将常量池内的符号引用替换为直接引用的过程  


5、初始化时机:
            >>遇到new、getstatic、putstatic 或 invokestatic 字节码指令,如果类没有进行初始化,则进行。
            >> 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行初始化,则进行初始化。
            >>初始化一个类,如果其父类没有初始化,则先触发父类的初始化。
            >>JVM启动,先初始化main方法所在的类。
        注:类不会被初始化的3种场景p211.



JAVA内存模型和线程
Java线程 - 工作内存 |
Java线程 - 工作内存 | --save/load操作-> 主内存
Java线程 - 工作内存 | 

    Java内存模型的主要目标是定义程序中各个变量(实例变量,静态字段等,不包括局部变量)的访问规则。
    Java内存模型规定了所有的变量都存储在主内存中,每个线程有自己的工作内存,其中保存了线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接操作主内存中的变量。不同的线程之间的操作也无法直接访问对方的工作内存中的变量,线程间的变量传递都是通过主内存来完成的。

内存间的交互操作
Java内存模型中定义了8中操作来实现工作内存和主内存数据同步。这些操作都是原子的,不可再分。
lock  unlock  read  load  use assign store write

volatile关键字
1、可见性-volatile变量在各个线程中都是一致的,这事因为每次使用变量之前都会做刷新操作(主内存-->工作内存)
2、禁止指令重排序优化

可见性的实现还有:final   synchronized

先行发生原则
该原则是判断数据是否存在竞争,线程是否安全的主要依据。
先行发生原则(happen-before)是指Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先发生于操作B,也就说说操作A产生的影响(修改内存中的共享变量,发送消息等。)会被操作B观察到。
>>Java内存模型下天然的先行发生关系:
    程序次序原则:一个线程内,按照代码的顺序执行。
    管程锁定:unlock 先行发生于后面对同一个锁的lock操作。
    volatile:volatile变量的写操作先行发生于后面对这个变量的读操作。
    对象终结:一个对象的初始化完成先行发生于finalize()方法开始。


衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。 


线程安全与锁优化
线程安全的实现方法:
1、同步和互斥
       >>使用synchronized关键字可以实现同步和互斥,但由于阻塞和唤醒线程需要操作系统帮助,那就需要从用户态转换到内核态,从而消耗较长的时间,所有synchronized是java语言中的一个重量级操作,除了确实必要的场景,一般不使用。
       >>还可以使用重入锁,ReentrantLock来实现同步,相比synchronized,ReentrantLock增加了一些高级功能,如:等待可中断,可实现公平锁,以及锁可以绑定多个条件。
            1)等待可中断:是指当持有锁的线程长期不释放锁,正在等待的线程可以选择放弃,改为处理其他事情。
            2)公平锁:多个线程等待同一个锁,必须按照申请锁的时间顺序来一次获得锁。
            3)锁绑定多个条件:只需要多次调用newCondition()即可。
    JDK1.5 多线程下synchronized的吞吐量会严重下降,而ReentrantLock则能基本维持在一个比较稳定的水平上。
    JDK1.6 synchronized和ReentrantLock基本持平,性能因素不再是选择ReentrantLock的主要因素。


2、非阻塞同步
        基于冲突检测的乐观并发策略--先进行操作,如果没有其他线程争用共享数据,那么操作成功,否则采用补偿措施(不断重试知道成功)、这种乐观并发策略成为非阻塞式同步。
        该方式基于硬件指令集的发展,需要操作和冲突检测具备原子性--即通过一条处理器指令就能完成,这类指令常用的有:1)测试并设置  2)获取并增加  3)交换  4)比较并交换(CAS,JDK1.5之后)5)加载链接/条件存储
        
        CAS的逻辑漏洞--ABA问题,但该问题一般不会影响并发的正确性,可以通过同步互斥的方式解决。
        
3、线程本地存储(Thread Local Storage)
        如果一个变量需要被某个线程独享,那么可以使用java.lang.ThreadLocal类来实现线程本地存储功能。

锁优化:
1)自旋锁与自适应自旋
自旋锁:同步和互斥的方式对性能最大的影响是阻塞的实现,挂起和恢复线程都需要转入内核态完成,但是很多共享数据的锁定只会持续很短的时间,为了这段时间挂起和恢复现场不值得,因此我们让请求锁的线程稍等一会,执行一个忙循环(自旋),这项技术,就是自旋锁。这种方式虽然避免线程切换的开销,但是会占用处理器时间,因此自旋等待的时间必须要有一定限度,自旋次数(默认10次,-XX:PreBlockSpin设置)超过限定,就使用传统的方式挂起。
自适应自旋锁:JDK1.6引入,自旋时间不再固定,而是由前一次在同一锁上的自旋时间及锁的拥有者的状态来决定。同一个锁对象,如果刚刚通过自旋获得过锁,那么他将允许更长的自旋等待时间,相反,如果某个锁,自旋很少成果获得,那么可能或省略掉自旋过程,以避免处理器资源浪费。

2)锁消除
在虚拟机即时编译之后对于不可能存在共享数据竞争的锁会进行消除。即代码会忽略掉所有同步直接执行。

3)锁粗化
虚拟机检测到有一串操作是对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列外部,这要只需要加锁一次就可以了。

4)轻量级锁 JDK1.6+
为了在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

5)偏向锁  JDK1.6+ 
JDK1.6的默认设置,和轻量级锁的目的一致。




























  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值