速通Java虚拟机

1 内存管理

1.1 运行时数据区域

1.【强制】运行时数据区。

运行时数据区包括:堆、方法区、虚拟机栈、本地方法栈、程序计数器。

1) 堆。

    所有线程共享,在虚拟机启动时创建,唯一的用途是存放对象实例,Java中几乎所有的对象实例都在这里分配内存。

    虚拟机设计者一般至少会把Java堆划分为新生代和老年代两个区域,空间大小 Young : Old = 1 : 2。

    新生代又可以划分为一块较大的Eden空间和两块较小的Survivor空间(From Survivor和To Survivor),空间大小 Eden : From : To = 8 : 1 : 1。

2) 方法区。

    所有线程共享,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    方法区有两种实现:元空间和永久代,区别在于:永久代使用JVM内存,元空间直接使用本地内存。

3) 虚拟机栈。

    线程私有,每个方法被执行的时候,Java虚拟机会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息,每一个方法被调用直至执行完毕的过程,对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

4) 本地方法栈。

    线程私有,与虚拟机栈的区别是,虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,本地方法栈为虚拟机执行本地(Native)方法服务。

5) 程序计数器。

    线程私有,记录当前线程执行的字节码的行号,字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。

2.【推荐】ThreadLocal。

ThreadLocal是给每个线程在Java堆中分配的一小块内存,其中的对象只能被这个线程获取,不允许其它线程访问,即ThreadLocal具有线程隔离的效果。

1.2 HotSpot虚拟机对象

1.【强制】对象的内存布局。

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为:

1) 对象头,包括Mark Word和类型指针。

    Mark Word:对象自身的运行时数据,包括hashCode,GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。

    类型指针:对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。

    *数组长度:如果对象是一个Java数组,在对象头中还必须有一块用于记录数组长度的数据。

2) 实例数据。

    对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字段内容。

3) 对齐补充。

    仅仅起着占位符的作用,保证对象的起始地址必须是8字节的整数倍。

2.【强制】对象的创建过程。

HotSpot虚拟机创建对象的过程包括 类加载、内存分配、初始化、设置对象的头部信息、执行构造方法 5个步骤。

1) 类加载。

    当Java虚拟机遇到一条字节码new指令时,首先会去检查待创建对象的类是否已被加载、解析和初始化过。如果没有,先执行相应的类加载过程。

2) 内存分配。

    虚拟机为新生对象分配内存。分配内存有“指针碰撞”和“空闲列表”两种方式,具体用哪种方式由Java堆内存是否规整决定,Java堆内存是否规整又由使用的垃圾收集器是否带有空间压缩整理的能力决定。

3) 初始化。

    虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。

4) 设置对象的头部信息。

5) 执行构造方法。

    按照程序员的意愿对对象进行初始化。

3.【推荐】如何定位内存中的对象?

使用直接指针或者句柄访问。

句柄访问:在Java堆中划出一块内存作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含对象实例和类型数据各自的具体地址信息。

1.3 内存溢出和内存泄漏

1.【强制】内存溢出。

内存不够用。

2.【强制】内存泄漏。

内存无法释放。

1.4 垃圾收集

1.4.1 垃圾收集理论

1.【强制】分代收集理论。

将Java堆内存划分为新生代和老年代的主要依据是分代收集理论。

1) 弱分代假说:绝大多数对象都是朝生夕死的。

2) 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

3) 跨代引用假说:跨代引用相对于同代引用来说仅占极少数。

4) 分代收集理论。

    核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。

    一般情况下将堆区划分为新生代和老年代,新生代的特点是每次垃圾收集要回收掉大部分的对象,老年代的特点是每次垃圾收集只有少量对象需要被回收,新生代和老年代适合不同的垃圾收集算法。

    新生代:标记-复制算法。

    老年代:标记-清除算法 或者 标记-整理算法。

2.【强制】如何判断对象是否可以回收?

可达性分析。

3.【推荐】4种引用类型。

强软弱虚。

1.4.2 垃圾收集算法

1.【强制】可达性分析算法。

通过可达性分析算法判定对象是否存活。这个算法的基本思路是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。

在Java技术体系里面,固定可作为GC Roots的对象包括:

    在虚拟机栈中引用的对象,如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;

    在方法区中类静态属性引用的对象,如Java类的引用类型静态变量;

    在方法区中常量引用的对象,如字符串常量池中的引用;

    在本地方法栈中Native方法引用的对象;

    Java虚拟机内部的引用,如基本数据类型对于的Class对象、一些常驻的异常对象、还有系统类加载器;

    所有被同步锁持有的对象。

2.【推荐】三色标记。

在垃圾收集算法中,标记是必不可少的一步。根据可达性分析,从GC Roots开始进行遍历访问,把遍历对象图过程中遇到的对象按是否访问过标记成三种颜色。

1) 白色:尚未访问过。

2) 黑色:已访问过,并且本对象引用的其他对象也已经全部访问过了。

3) 灰色:已访问过,但是本对象引用的其他对象尚未全部访问完。

3.【强制】垃圾收集算法。

1) 标记-复制算法。

    给对象分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。罕见情况下,另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象便通过分配担保机制直接进入老年代。

2) 标记-清除算法。

    首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

    缺点:空间碎片化严重。

3) 标记-整理算法。

    在标记完成后,让所有存活的对象都向内存空间一端移动,然后直接清理掉边界(指针)之外的内存。

    缺点:效率不高。

1.4.3 垃圾收集器

1.【推荐】垃圾收集器。

一般是一个新生代收集器搭配一个老年代收集器使用。常用搭配:

    Serial - Serial Old:新生代采取标记-复制算法,老年代采取标记-整理算法。

    Parallel Scavenge - Parallel Old:新生代采取标记-复制算法,老年代采取标记-整理算法,特点是吞吐量优先。

    ParNew - CMS:新生代采取标记-复制算法,老年代采取标记-清除算法,响应时间优先。

    G1(单独使用):面向局部收集、基于Region,采取标记-复制算法。

2.【了解】CMS垃圾收集过程。

1) 初始标记。

    (暂停用户线程)仅仅标记一下GC Roots能直接关联到的对象,速度很快。

2) 并发标记。

    从GC Roots的直接关联对象开始遍历整个对象图,找出要回收的对象,这个过程耗时较长,但可以与用户线程并发运行。

3) 重新标记。

    (暂停用户线程)修正并发标记期间,因用户程序继续运作而导致标记变动的那一部分对象的标记记录。

4) 并发清除。

    清理删除掉标记阶段判断的已经死亡的对象,这个阶段也是可以与用户线程并发的。

3.【了解】G1垃圾收集过程。

G1不再坚持固定大小以及固定数量的分代区域,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

G1收集器会去跟踪各个Region里面的垃圾堆积的价值大小,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region。

G1收集器的运作过程大致可划分为:

1) 初始标记。

    (暂停用户线程)仅仅标记一下GC Roots能直接关联到的对象,耗时很短。

2) 并发标记。

    从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。

    在此阶段,如果内存回收的速度赶不上内存分配的速度,G1收集器会被迫冻结用户线程的执行,导致Full GC。

3) 最终标记。

    (暂停用户线程)处理在并发阶段发生引用变动的对象。

4) 筛选回收。

    (暂停用户线程)更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧region的全部空间。

2 类加载

1.【强制】类加载过程。

Java虚拟机中类加载的全过程包括 加载、验证、准备、解析、初始化 5个阶段。

1) 加载。

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

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

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

2) 验证。

    确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

3) 准备。

    正式为类中定义的变量分配内存并设置类变量初始值。

4) 解析。

    Java虚拟机将常量池内的符号引用替换为直接引用。

5) 初始化。

    根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。

2.【强制】类加载器。

Java虚拟机中的类加载器包括 启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器。

1) 启动类加载器(Bootstrap ClassLoader)。

    负责将存放在<JAVA_HOME>\lib\目录,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。

2) 扩展类加载器(Extension ClassLoader)。

    负责加载<JAVA_HOME>\lib\ext\目录中所有的类库。

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

    负责加载用户类路径(ClassPath)上所有的类库。

4) 自定义类加载器。

    实现自定义类加载器分两步,一是继承java.lang.ClassLoader,二是重写父类的findClass方法。

3.【强制】类初始化的顺序。
先静后动,在此前提下先父后子。

4.【强制】双亲委派模型。

如果一个类加载器收到了类加载的请求,它不会马上去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层级的类加载器都是如此,因此所有的加载请求最终都被传送到顶层的启动类加载器中,只有父加载器无法完成加载请求时,子加载器才会尝试去加载。

5.【推荐】Tomcat、Jetty、WebLogic等服务器为什么要重写类加载器?

一个功能健全的Web服务器,都要解决如下问题:

    部署在同一个服务器上的两个Web应用程序所使用的Java类库既要实现相互隔离,又要实现互相共享;

    服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响,服务器所使用的类库应该与应用程序的类库互相独立;

    支持JSP应用的Web服务器,十有八九都需要支持HotSwap功能(热替换)。

各种Web服务器的做法是:

    提供好几个有着不同含义的ClassPath路径供用户存放第三方类库,并且自定义了多个类加载器;

    这样的做法没有打破双亲委派模型,自定义类加载器都是Application ClassLoader的子孙加载器。

3 编译与优化

1.【强制】常用的调优参数。

-Xms:初始堆大小。

-Xmx:最大堆大小。

-XX:NewRatio=n:设置老年代和新生代的比值。

-XX:SurvivorRatio:新生代中eden区和一个survivor区的比值。

-Xss:每个线程栈的大小。

-XX:PermSize=n:永久代初始值。

-XX:MaxPermSize=n:永久代大小。

-XX:MaxTenuringThreshold:新生代对象的最大年龄。

-XX:+UseG1GC:设置G1收集器。

-XX:+UseXXXGC:设置XXX收集器。

4 并发

4.1 Java内存模型

1.【推荐】缓存一致性问题。

在多路处理器系统中,所有的处理器共享同一主内存,而每个处理器又都有自己的高速缓存。在每一时刻,每个处理器的高速缓存中的同一数据应该是一致的。

为解决缓存一致性问题,需要各个处理器访问缓存时都遵循一些协议,在读写时根据协议来进行操作。

2.【强制】主内存与工作内存之间的8种交互操作。

1) lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

2) unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

3) read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

4) load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

5) use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

6) assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

7) store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

8) write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

4.2 锁优化

1.【推荐】锁消除。

锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。

锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。

2.【推荐】锁粗化。

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等 待锁的线程也能尽可能快地拿到锁。

大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,这样只需要加锁一次就可以了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值