码迷,mamicode.com
首页 > 其他好文 > 详细

深入理解JVM

时间:2017-04-30 18:37:33      阅读:244      评论:0      收藏:0      [点我收藏+]

标签:存在   虚拟   版本号   热点   同步   句柄   加锁   内存管理   before   

一、内存管理

1、运行时的内存区域

线程私有:虚拟机栈、本地方法栈、程序计数器

线程共享:堆、方法区

2、各个内存区域可能抛出的异常

  • 1、当单线程时,栈的深度太大,会发生StackOverflowError,比如无穷的递归调用。

    2、当多线程时,若不停地创建线程,则会导致OutOfMemoryError,因为除去堆和方法区之外,剩下的栈总空间是有限的,不停创建线程则会不停申请栈空间,最终会导致内存溢出。

  • 当不停地创建(new)对象时,会导致OutOfMemoryError

  • 方法区

    运行时产生大量的类,去填满方法区,比如用CGLib去无穷生成类。

  • 直接内存

    使用Unsafe分配本机内存时,可能导致OutOfMemoryError。

3、各个内存区域容量设置的参数

  • -Xss2M:设置栈的容量为2M

  • -Xms10M:设置堆的初始容量为10M

  • -Xmx10M:设置对的最大容量为10M

  • -XX:PermSize=10M:设置方法区的初始容量为10M

  • -XX:MaxPermSize=10M:设置方法区的最大容量为10M

  • -XX:MaxDirectMemorySize=10M:设置直接内存的最大容量为10M

4、对象的创建

  • 如何在堆中分配内存

    根据内存是否规整,即GC收集器是否带有压缩整理功能,分为指针碰撞和空闲列表两种。

  • 如何处理内存分配冲突

    1、CAS+失败重试;2、TLAB,即本地线程分配缓冲。

  • 对象在内存中的布局

    对象头(哈希码、GC分代年龄、所状态标志等)、实例数据、对象填充

  • 如何访问对象

    1、通过句柄(栈上的指针指向句柄,句柄中分别有指向对象的指针,和指向类信息的指针)

    2、直接指针(栈上的指针直接指向堆中的对象,对象中头部有一个类型指针,指向类型信息)

5、对象存活判定

即如何判断一个对象所占用的内存是否该回收?

有两种方法:1、引用计数法;2、可达性分析法。

  • 引用计数法

    该方法容易出现循环引用的问题,JVM并未采用。

  • 可达性分析法

    判断是否能从GCRoots中找到一条到达该对象的路径。

    GCRoots包括:栈中变量引用的对象、方法区中静态属性(static)引用的对象、方法区中常量(final)引用的对象。

技术分享

6、垃圾回收

  • 引用的种类

    1、强引用:通常new出来的对象的引用都是强引用。

    2、软引用(SoftReference):如果某次回收完之后,还是可能发生内存溢出,则进行第二次回收,在第二次回收时会回收软引用,若这次回收后仍是内存不够,这时候才发生内存溢出。

    3、弱引用(WeakReference):其指向的对象只能生存到下一次垃圾收集之前,无论当前内存是否足够,都会回收弱引用指向的对象。

    4、虚引用(PhantomReference):设置虚引用的目的可能是为了,在该对象被回收前能够得到一个系统通知。

  • finalize方法

    若某类覆盖了该方法,则其对象再被回收前会调用此方法(仅限于第一次回收该对象时)。

  • 方法区的回收

    废弃的常量:当前系统中没有一个对象引用此常量。

    无用的类:堆中不存在该类的任何实例,该类的类加载器已被回收,该类的Class对象没有在任何位置被引用。

技术分享

  • 垃圾收集算法

    1、JVM整体上是采用“分代收集算法”。

    根据对象的存活周期的不同将Java堆分为新生代和老年代。

    新生代又分为Eden区,Survivor From区,Survivor To区,其大小比例默认为8:1:1。

    Java的方法区被定义为永久代

    2、对于新生代,一般采用“复制算法”。因为新生代的存活时间相对较短,复制的时候不会复制太多对象,所以整体效率不至于太低。

    当从Eden和Survivor From区向Survivor To区复制时,若Suvivor To区的空间不够,则需要依赖老年代进行“分配担保”。

    3、对于老年代,一般采用“标记-清除”(容易产生内存碎片)或“标记-整理”算法。因为老年代的对象存活率较高,若仍是采用复制操作,则需要复制的对象太多,效率会很低。

    4、Stop The World

    在判断对象是否应该被回收时,是通过GCRoots来判断的。

    当枚举GCRoots时,不可以出现在分析过程中,对象的引用关系还在不断发生变化,所以这时候必须停顿所有线程,这种现象称为“Stop The World”。

    5、安全点和安全区域

    安全区域是指:在这段代码片段内,引用关系不会发生变化。

    6、内存分配和回收策略

    对象优先在Eden区分配,若内存不够则进行一次Minor GC,将Eden区的活跃对象复制到Survivor To区。

    若Survivor To区大小足够,则将其中的存活对象的GC年龄加1,并判断是否应该晋升到老年代区。

    若Survivor To区大小不够,则进行分配担保,将对象复制到老年代。

    若此时老年代内存大小不够,则进行一次Full GC。

  • 垃圾收集器

  • Serial:新生代收集器,单线程收集器,采用复制算法。

  • ParNew:新生代收集器,多线程收集器,采用复制算法。

  • Parallel Scavenge:新生代收集器,多线程收集器,侧重于提高程序运行的吞吐量。

  • Serial Old:老年代收集器,单线程收集器,采用标记-整理算法。

  • Parallel Old:老年代收集器,多线程收集器,采用标记-整理算法。

    由于多线程的老年代收集器可以充分利用服务器多CPU的处理能力,所以常用Parallel Scavenge/Parallel Old组合,亦提高了吞吐量。

  • CMS(Concurrent Mark Sweep)

    老年代收集器

    初始标记-->并发标记-->重新标记-->并发清除

  • G1收集器

    初始标记-->并发标记-->最终标记-->筛选回收

二、执行子系统

(一)、类文件结构

1、平台无关性和语言无关性

平台无关性:通过Java虚拟机,Java代码可以运行在不同的操作系统上。

语言无关性:不同的语言通过编译成字节码,均可以运行在JVM上。

2、Class文件结构

(1)、 两种数据类型:无符号数,表。

(2)、魔数:“CAFEBABE”

(3)、次版本号,主版本号

(4)、常量池:常量个数,常量项(类型tag+内容)。包括字面量和符号引用。

(5)、访问标志:是否 public,final,super,interface,abstract,synthetic,annotation,enum

(6)、类索引,父类索引,接口索引

(7)、字段表

(8)、方法表

(9)、属性表

3、字节码指令

(1)、字节码中的数据类型:byte、short、int、long、float、double、char、reference

(2)、加载和存储指令:将数据加载到操作数栈,将数据存储到局部变量表

(3)、运算指令:加减乘除、取余、取反、位移、位运算(与、或、异或)、局部变量自增、比较

(4)、类型转换

(5)、对象创建和访问

(6)、操作数栈相关指令

(7)、控制转移指令:条件分支等

(8)、方法调用和返回:

invokevirtual(实例方法)

invokeinterface(接口方法)

invokespecial(构造方法,私有方法,父类方法)

invokestatic(静态方法)

(9)、同步指令:monitorenter、monitorexit。通过管程实现,用于支持synchronized关键字。

(二)、类加载机制

1、类在什么时候会加载

(1)、new(使用new实例化对象的时候),getstatic(读取一个类的静态字段,不包括final的),putstatic(设置一个类的静态字段,不包括final的),invokestatic(调用一个类的静态方法时)。

(2)、通过java.lang.reflect包的方法,对类进行反射调用的时候,若类未初始化,则会对其进行初始化。

(3)、当初始化一个类时,若其父类还未初始化,则先初始化其父类。

(4)、虚拟机启动时,会先初始化包含main方法的那个类,即主类。

(5)、被动引用不会导致类加载,比如:通过子类引用父类的静态字段,不会导致子类初始化;定义某类的数组,则该类不会初始化;引用某类的静态常量,则该类不会初始化。

2、类加载的过程

(1)、加载

通过类的全限定类名获取二进制流;将字节流转换为方法区内的运行时数据结构;在内存中生成一个代表该类的Class对象,作为方法区里这个类的各种访问数据的入口。

(2)、验证

校验Class文件中的信息是否复合JVM的要求。

文件格式验证(基于二进制字节流,校验主次版本号是否支持等);

元数据验证(是否继承的final类,是否实现了接口的所有方法等);

字节码验证(通过数据流和控制流分析,验证程序语义,比如只能父类引用指向子类对象,子类引用不能指向父类对象);

符号引用验证(当JVM将符号引用转换为直接引用时,会检查是否能根据名称找到相应的类,方法,字段等);

验证阶段其实不是必须的,如果该字节码被反复验证过,其实可以关闭验证。

(3)、准备

为静态变量设置初始值(int为0,reference为null等);

为常量设置初始值。

(4)、解析

将常量池里的符号引用解析为直接引用(即指向内存中某个区域的指针)。

解析的符号引用有:类或接口、字段、方法、接口方法等。

(5)、初始化

若父类没有加载,则先加载父类;

然后为静态变量设置初始值,执行静态代码块等;

3、类加载器

(1)、每个类,都要由“加载它的类加载器”和“这个类本身”一块确定该类在虚拟机里的唯一性。

(2)、类加载器种类

启动类加载器:加载<JAVA_HOME>/lib目录中的类;

扩展类加载器:加载<JAVA_HOME>/lib/ext目录中的类;

应用程序类加载器:加载CLASSPATH中的类,如果应用程序没有自定义过自己的类加载器,这个便是程序中默认的类加载器。

(3)、双亲委派模型

技术分享

当一个类加载器加载类的时候,首先不会亲自加载这个类,而是会把这个请求委派给父加载器去加载,如此递归下去,所有的请求最终会传到顶层的引启动类加载器。只有当父加载器无法加载该类时,才会让子类去尝试加载。

这样做可以保证,同一个类在虚拟机中不会被不同的类加载器加载很多次。

(三)、字节码执行引擎

1、运行时栈帧结构

技术分享

程序执行时,内存中的栈,里面是一个一个的栈帧。每一个方法的调用及其执行,都对应着一个栈帧。

2、方法调用

(1)、方法调用指令

invokestatic:调用静态方法

invokespecial:调用实例构造器、私有方法、父类方法

invokesvirtual:调用所有的虚方法(非static非final方法)

invokeinterface:调用接口方法

(2)、分派

静态分派:对应于方法参数上的重载。编译器在重载时,是通过参数的静态类型,而不是实际类型作为判断依据的。

动态分派:对应于多态。当invokespecial指令执行时,第一步就是在运行期确定接受者的实际类型。

三、代码优化

  • 编译期优化

    (1)、编译过程

    词法分析,语法分析-->

    填充符号表-->

    处理注解-->

    语义分析(标注检查、数据及控制流分析、解语法糖)-->

    字节码生成。

    (2)、Java的语法糖

    泛型与类型擦除

    自动装箱、拆箱

    foreach循环

    变长参数

    注意:1、JVM在字节码里,用Signature属性存储了方法在字节码层面的方法签名,通过这项元数据,可以通过反射获取类的泛型信息。2、包装类的“==”运算,在不遇到算数运算时不会自动拆箱,这时候比较的是引用是否相等。

  • 运行期优化

    (1)JIT编译器(Just In Time)

    Java程序最初是通过解释执行的,当虚拟机发现“某个方法或某段代码块

    ”运行特别频繁时,会把这些代码认定为“热点代码(Hot Spot Code)”。为了提高热点代码的执行效率,在运行时,会把这些代码编译为与本地平台相关的机器码,并进行各种优化。完成这个任务的编译器叫做即时编译器。

    (2)、解释器与编译器并存

技术分享

  • 1、编译对象

    被多次调用的方法:将整个方法作为编译对象。

    被多次执行的循环体:依然将整个方法作为编译对象,进行栈上替换(On Stack Replacement),即替换栈帧。

    2、热点探测的方式

    基于采样:周期性的检查栈顶,若某方法经常出现在栈顶,则其是热点方法。

    基于计数器:统计方法执行次数,到了一定的阈值,则其是热点方法。

    3、两种计数器

    JVM的热点探测是基于计数器的,有两种计数器:方法调用计数器和回边计数器。

    方法调用计数器:即统计方法执行次数。

    回边计数器:循环体中代码执行的次数。“回边”,即在字节码中遇到控制流向后跳转的指令。

    4、编译优化技术

    公共子表达式消除、数组边界检查消除、方法内联、逃逸分析等。

四、并发

1、JVM内存模型和线程

(1)、JVM内存模型

技术分享

技术分享

1、JVM内存模型主要是定义程序中变量的访问规则,即在虚拟机中将变量存储到内存,和从内存中取出变量这样的细节。这些变量指的是实例字段、静态字段等,不包括局部变量(因为局部变量是线程私有的,不被共享,不存在竞争问题)。

2、工作内存中保存了该线程使用到的变量的在主存中的拷贝。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行,不能直接读写主存中的变量。

3、Java内存模型中的工作内存只是个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其它的硬件和编译器优化。

4、内存间的交互操作

lock、unlock

read、load、use、assign、store、write

5、volatile变量

6、原子性、可见性、有序性

7、happens-before原则

(2)、线程

1、线程的实现方式

三种方式:1:1、1:N、N:M。

Java是通过将线程映射到操作系统的线程上去实现的。

2、线程的调度方式

两种方式:协同式线程调度、抢占式线程调度。

Java中使用的是抢占式线程调度。

3、线程的状态转换

1、线程安全和锁优化

  • 线程安全

    (1)、共享数据的种类:

    1、不可变:不可变对象是值“对象中带有状态的变量都声明为final”。

    2、绝对线程安全

    3、相对线程安全:Vector、Hashtable等

    4、线程兼容:ArrayList、HashMap

    5、线程对立

    (2)、线程安全的实现方法

    1、互斥同步(Mutual Exclution & Synchronization)

    同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只被一个(一些)线程使用。

    互斥是实现同步的一种手段,临界区、互斥量、信号量都是互斥的实现方式。

    互斥是因,同步是果;互斥是方法,同步是目的。

    互斥同步又称为“阻塞同步”,属于一种悲观的并发策略。

    2、非阻塞同步

    非阻塞同步是一种基于“冲突检测”的乐观并发策略;

    即先进行操作,如果“没有其它线程争用共享数据”,那操作就成功了。如果共享数据有争用,产生了冲突,那就再采取其它的补偿措施(比如不断重试,直到成功为止)。

    因为这种策略不需要将线程挂起,所以成为非阻塞同步。

    “操作和冲突检测”这两个步骤,需要具备原子性,这可以通过硬件的CAS来实现。

    CAS指令需要三个操作数:内存位置,旧的预期值,新值。当且仅当“内存位置的值”符合“旧的预期值”时,处理器用“新值”更新“内存位置的值”,否则就不更新。最终无论是否更新,均会返回“旧值”。

    3、不须同步

    可重入代码:不依赖堆上的数据和共用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法。如果一个方法,它的返回结果可预测,相同的输入,均能返回相同的输出,它便是可重入的。

    线程本地存储:即ThreadLocal,以当前线程哈希码为键,某变量位置的一个键值对。

  • 锁优化

    1、自旋锁

    因为当线程阻塞,或从阻塞中恢复时,挂起线程和恢复线程的操作都需要转入到内核态去完成,这给系统的性能带来了很大压力。

    如果某个锁被占用的时间很短,这时候可以让“后面那个请求锁的线程”稍微等一下,不放弃CPU的执行时间,执行一个忙循环,直到获得锁。

    2、锁消除

    将一些代码上进行了同步,但实际不会存在共享数据竞争的锁进行消除。

    3、锁粗化

    如果对一个对象反复加锁和解锁,甚至这个对象在循环体内,则可以把锁加到循环体外,这样可以消除反复加解锁所带来的损耗。

    4、轻量级锁

    5、偏向锁

深入理解JVM

标签:存在   虚拟   版本号   热点   同步   句柄   加锁   内存管理   before   

原文地址:http://www.cnblogs.com/yuanhaha/p/6789761.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!