深入理解Java虚拟机(周志明版)总结—WSYW126

 2019-12-22 10:37  阅读(1004)
文章分类:JVM

1.Java的内存区域与内存溢出异常(常见内存溢出错误解决办法):

a) Java Heap 溢出:java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。

i. 如果是内存泄漏找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收.

ii. 如果不存在泄漏即内存溢出,应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。

b) 虚拟机栈和本地方法栈溢出

i. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

ii. 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。这里需要注意当栈的大小越大可分配的线程数就越少。

c) 运行时常量池溢出

i. 如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。

d) 方法区溢出

i. 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

2.垃圾回收器和内存分配策略

a) 对象已死吗

i. 引用计数算法(很难解决对象之间的循环引用的问题)

ii. 可达性分析算法。(有以下四种对象可以作为GCRoot的对象)

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  2. 方法区中类静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI引用的对象。

b) 对象的引用

i. 强引用:强引用只要存在,对象就不会被垃圾回收器回收。

ii. 软引用:还有用但非必须的对象。(与缓存有关)(软引用对象会存在内存中,如果内存空间足够,则不回收,否则回收。)

iii. 弱引用:非必须的对象。(垃圾回收时不论空间是否足够都回收弱引用对象)

iv. 虚引用:对对象的生存时间没有影响,也无法通过虚引用获得一个对象实例,虚引用的目的是能够在对象被回收的时候收到一条系统通知。

c) 对象的逃生:对象在进行可达性分析后没有与GCRoot相关的引用。它将会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。

i. 对象没有覆盖finalize()方法。

ii. Finalize()方法已经被虚拟机执行过。

对于上面的两种情况会视为“没有必要执行”。如果对象要逃生,则在finalize()方法中将自己与GCRoot产生关联即可。

d) 方法区回收(永久代的回收)。

i. 主要分为两部分内容:废弃的常量和无用的类。

ii. 无用的类

  1. 该类的所有的实例都已经回收。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对象的java.lang.Class对象没有在任何地方被引用,即无法通过反射机制访问该类的方法。

e) 垃圾回收算法

i. 标记–清除算法:存在两个不足之处

  1. 效率问题:标记和清除两个过程效率都不高。
  2. 空间问题:清除后会产生大量的不连续的内存碎片。

ii. 复制算法:将内存按容量分为相等的两块,每次使用一块,当A块使用完时,将存活的对象复制到B块,然后清除A块。此时不用考虑内存碎片,只需移动堆顶指针,按顺序分配内存,运行高速。

  1. 代价:将可使用的内存缩小了一半。
  2. 改进:一块Eden和两块Survivor。一般该算法回收新生代。

iii. 标记–整理算法:这个算法是根据老年代的特点提出。让所有的存活对象都向一端移动,然后直接清除掉端边界以外的内存。

iv. 分代收集算法

f) 安全点:以程序“是否具有让程序长时间执行的特征”为标准。长时间执行的特征就是指令序列复用,例如:方法调用、循环跳转、异常跳转。

g) 安全区域:在一段代码中,引用关系不会发生变化。

h) 垃圾回收器

i. 新生代:Serial,ParNew,Parallel Scavenge,G1

ii. 老年代:CMS,Serial Old(CMS),Parallel Old,G1

iii. Serial:单线程。关注点在缩小停顿时间。

iv. ParNew:Serial收集器的多线程版本。多线程时效果很好。(可以与CMS配合)关注点在缩小停顿时间。

v. Parallel Scavenge:使用复制算法,并行多线程收集器。关注点在吞吐量。

  1. -XX:MaxGCPauseMillis:最大垃圾回收停顿时间。
  2. -XX:GCTimeRatio:吞吐量大小。

vi. Serial Old:Serial老年代版本,单线程,使用“标记–整理”算法。

vii. Parallel Old:Parallel老年代版本,多线程,使用“标记–整理”算法。

viii. CMS:一种以获取最短回收停顿时间为目标的收集器。基于“标记–清除”算法。默认回收线程数:(CPU数+3)/4

i) 内存分配和回收策略

i. 对象优先在Eden分配。

ii. 大对象直接进入老年代。

iii. 长期存活对象将进入老年代。

iv. 动态对象年龄判定

如果在Survivor空间(指一块Survivor块的大小)中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象可直接进入老年代。无须等到MaxTenuringThreshold中要求的年龄。

v. 空间分配担保

j) 所有垃圾回收器之间可以搭配使用的关系

2019120001153\_1.png

3.虚拟机类加载机制:

a) 生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。

b) 在加载阶段,虚拟机需要完成三件事:

i. 通过一个类的全限定名来获取定义此类的二进制字节流。

ii. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。

iii. 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口。

c) 验证阶段大致会完成下面4个阶段的校验动作:

i. 文件格式验证。

ii. 元数据验证。

iii. 字节码验证。

iv. 符号引用验证。

d) 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,类变量所使用的内存在方法区进行分配。(这里的变量指类变量,被static修饰过的,不包括实例变量。准备阶段过后类变量的值是数据类型的零值,如public static Int value = 123,过后是0,不是123,在初始化后才是123)

基本数据类型的零值

数据类型 零值 数据类型 零值
数据类型 零值 数据类型 零值
int 0 boolean false
long 0L float 0.0f
short (short)0 double 0.0d
char ‘\u0000’ reference null
byte (byte)0    

e) 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

i. 类或接口的解析

ii. 字段解析

iii. 接口方法解析

f) 初始化阶段根据程序员通过程序制定的主观计划去初始化类变量和其他资源。(初始化阶段是执行类构造器()方法的过程)

4.类加载器:实现加载动作中b.i的代码模块。

a) 启动类加载器(bootstrap ClassLoader)

b) 扩展类加载器(Extension ClassLoader)

c) 应用程序类加载器(Application ClassLoader)

d) 自定义类加载器(User ClassLoader)

e) 类加载器之间的关系叫做双亲委派模型:如果一个类加载器收到类加载的请求,他不会自己去加载类,而是把这个请求委派给父类加载器去完成,每一层次的加载器都是如此,因此所有的加载请求最终应该传到顶层的启动类加载器,只有父加载器反馈无法加载时,子加载器才会自己尝试加载。

f) 双亲委派模型使java类随着它的类加载器一起具备了一种优先级的层次关系。

g) 是lazy loader,延迟加载。当使用时在加载。

5.Java虚拟机的内存模型

a) 内存模型:

i. 线程、主内存、工作内存

ii. 八个操作:lock、read、load、user、assign、store、write、unlock

b) Volatile

i. 具有两种特性:

  1. 保证此变量对所有线程的可见性。(某线程修改这个变量的值后,新值对于其他线程来说是可以立即可见的)
  2. 禁止指令重排序优化。

ii. 由于volatile只保证可见性,不符合以下情况的运算中我们仍要加synchronized来保证原子性。

  1. 运算结果并不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值
  2. 变量不需要与其他的状态变量共同参与不变约束

iii. Volatile变量读操作的性能和普通操作几乎没差别,但是写操作可能会慢一点,因为它需要在本地代码中插入许多的内存屏障执行来保证不发生乱序执行。

iv. volatile的总开销要比锁低。

v. 可见性实现:volatile、synchronized和final都能实现可见性。

c) 先行发生原则

i. 程序次序规则

ii. 管程锁定规则

iii. Volatile变量规则

iv. 线程启动规则

v. 线程终止规则

vi. 对象终结规则

vii. 传递性

d) 线程状态转换关系

2019120001153\_2.png

6.Java线程安全和锁优化

a) Java语言中的线程安全

i. 不可变(immutable)

在java中如果共享数据是一个基本类型数据,那么定义时使用final关键字修饰就可以保证它是不变的。String、枚举类型、java.lang.Number的子类(Long和Double等数值包装,BigInteger和BigDecimal等大数据)都是不可变的。但是number的子类型中原子类AtomicInteger和AtomicLong则并非是不可变的。

ii. 绝对线程安全:不管运行时环境如何,调用者都不需要任何额外的同步措施。

iii. 相对线程安全:vector、hashtable、collections的synchronizedCollection()方法包装集合都是相对线程安全的。

iv. 线程兼容:java API中大部分的类都是属于线程兼容的。

v. 线程独对立:无论是否采用同步手段,都无法保证在多环境下并发使用代码。

b) 线程安全的实现方法

i. 互斥同步

实现方式:临界区(critical section)、互斥量(Mutex)、信号量(Semaphore)

  1. Java中最基本的互斥手段就是:synchronized关键字,经过编译后,会在同步代码前后分别形成monitorenter和monitorexit两个字节码,这两字节码都需要一个reference类型的参数指明要锁定和解锁的对象。如果synchronized指明对象参数,那就是这个对象的reference;若没指明,那就根据修饰的是实例方法还是类方法,去取相应的对象实例或者Class对象来作为锁对象。(就是synchronized后参数)(synchronized是重量级操作)
  2. Java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。新加特性:

a) 等待可中断:长期持有锁线程不释放时,等待进程可以选择放弃。

b) 可实现公平锁:(synchronized和lock默认都是非公平的,lock可以通过构造函数中的布尔值要求使用公平锁)

c) 锁可以绑定多个条件:通过多次调用newCondition()方法即可。

  1. 二者区别:lock表现为API层面的互斥锁(lock()和unlock()方法需要配合try/finally语句块来完成)。Synchronized变现为原生语法层面的互斥锁。

ii.&bsp;非阻塞同步:基于冲突检测的乐观并发策略。(atomicInteger等)

iii. 无同步方案

  1. 可重入代码(Reentrant Code)
  2. 线程本地存储(Thread Local Storage)

c) 锁优化

i. 自旋锁和自适应自旋(adaptive Spinning)

ii. 锁消除

iii. 锁粗化

iv. 轻量级锁

以上排版,看着不舒服,请需要的同学,下载PDF版的文档,点击下载

参考资料
深入理解Java虚拟机(周志明版)
备注
转载请注明出处:http://blog.csdn.net/wsyw126/article/details/51395196
作者:WSYW126

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解Java虚拟机(周志明版)总结—WSYW126

相关推荐