JVM虚拟机

JVM有哪些分区?程序计数器,java虚拟机栈,本地方法栈,堆,方法区。

java栈中存放的是一个个栈帧,每一个栈帧对应每一个调用的方法,栈帧包括局部变量表,操作数栈方法的返回地址,当前方法所属类的运行常量池的引用,附加信息。方法区有运行常量池。.程序计数器是唯一一个在Java 虚拟机规范中没有规定任何oom 情况的区域。

2.在java 虚拟机规范中,对于java 虚拟机栈,规定了2 中异常,1)若线程请求的栈深度> 虚拟机所允许的深度,则抛出Stack Overflowerror 异常2)若虚拟机可以动态扩展,若扩展时无法申请到足够的内存空间,则抛出oom 异常。

[if !supportLists]3. [endif]java 虚拟机栈为执行java 方法,本地方法栈为虚拟机使用native 方法服务,本地方法栈也会抛出Stack Overflowerror 和oom。

[if !supportLists]4. [endif]Java 堆可以处于物理上连续的内存空间,只要逻辑上是连续的即可。可固定,可扩展。

若堆中没有内存完成实例分配,并且堆也无法再扩展,则会抛出oom。

5.直接内存不是运行时数据区的一部分。堆上的OOM 测试

垃圾回收

对象是否存活

1)引用计数法缺点:很难解决对象之间循环引用的问题。

2)可达性分析法基本思想:通过一系列的称为“GC roots”的对象作为起始点,从这些节点,开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC root 没有任何引用链相连(用图论的话来说,就是从GC roots 到这个对象不可达),则证明此对象是不可用的。

可作为GC roots 的对象

1)java 虚拟机栈(栈帧中的本地变量表)中引用的对象

2)方法区中类的静态属性引用的对象

3)方法区中常量引用的对象

4)本地方法栈中JNI 引用的对象

引用强度强引用>软引用>弱引用>虚引用

任何一个对象的finalize()方法都只会被系统调用一次。

若对象在进行可达性分析后发现没有与GC roots 相连接的引用链,那么他将会被第一次标记并进行一次筛选,筛选的条件是该对象是否有必要执行finalize()方法,当对象没有重写

finalize()方法或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没必要执行。

若该对象被判定为有必要执行finalize 方法,则这个对象会被放在一个F-Queue 队列,finalize 方法是对象逃脱死亡命运的最后一次机会,稍后GC 将对F-queue 中的对象进行第二次小规模的标记,若对象要在finalize 中成功拯救自己—只要重新与引用链上的任何一个对象建立关联即可,那么在第二次标记时他们将会被移出“即将回收”集合。


Finalize 方法不是c 或c++的析构函数。


停止-复制算法:它将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,则就将还存活的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。商业虚拟机:将内存分为一块较大的eden 空间和两块较小的survivor 空间,默认比例是8:1:1,即每次新生代中可用内存空间为整个新生代容量的90%,每次使用eden 和其中一个survivour。当回收时,将eden 和survivor 中还存活的对象一次性复制到另外一块survivor 上,最后清理掉eden 和刚才用过的survivor,若另外一块survivor 空间没有足够内存空间存放上次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。


标记-清除算法:缺点1)产生大量不连续的内存碎片2)标记和清除效率都不高

标记-清理算法:标记过程和“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清除,而是让all 存活对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集:新生代 停止-复制算法 老年代 标记-清理或标记-清除垃圾收集器,前 3 个是新生代,后 3 个是老年代

[if !supportLists]1) [endif]serial 收集器:单线程(单线程的意义不仅仅说明它会使用一个cpu or 一条垃圾收集

线程去完成垃圾收集工作,更重要的是在它进行垃圾收集的时候,必须暂停其他all 工作线程,直到他收集结束)。对于运行在client 模式下的虚拟机来说是个很好的选择。停止-复制

[if !supportLists]2) [endif]parNew 搜集器:serial 收集器的单线程版本,是许多运行在server 模式下的虚拟机首选的新生代收集器。停止-复制

[if !supportLists]3) [endif]parallel scaverge:目标达到一个可控制的吞吐量,适合在后台运算,没有太多的交互。停止-复制。

[if !supportLists]4) [endif]serial old:serial 的老年代版本,单线程,标记-清理

[if !supportLists]5) [endif]parallel    old:parallel    scaverge    老年代的版本,多线程 标记-清理6)cms 收集器:一种以获取最短回收停顿时间为目标的收集器 “标记-清除”,有 4 个过程初始标记(查找直接与 gc roots 链接的对象) 并发标记(tracing 过程) 重新标记(因为并发标记时有用户线程在执行,标记结果可能有变化) 并发清除 其中初始标记和重新标记阶段,要“stop the world”(停止工作线程)。优点:并发收集,低停顿 缺点:1)不能处理浮动垃圾 2)对 cpu 资源敏感 3)产生大量内存碎片 {每一天具体的详解请看《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》}


对象的分配:1)大多数情况下,对象在新生代eden 区中分配,当Eden 区中没有足够的

内存空间进行分配时,虚拟机将发起一次minor GC {minor gc:发生在新生代的垃圾收集动作,非常频繁,一般回收速度也比较快full gc:发生在老年代的gc} 2)大对象直接进入老年代3)长期存活的对象将进入老年代4)若在survivor 空间中相同年龄all 对象大小的总和>survivor 空间的一半,则年龄>= 改年龄的对象直接进入老年代,无须等到

MaxTeuringThreshold(默认为15)中的要求。

空间分配担保

在发生minor gc 前,虚拟机会检测老年代最大可用的连续空间是否>新生代all 对象总空间,若这个条件成立,那么minor gc 可以确保是安全的。若不成立,则虚拟机会查看

HandlePromotionFailure    设置值是否允许担保失败。若允许,那么会继续检测老年代最大可用的连续空间是否>历次晋升到老年代对象的平均大小。若大于,则将尝试进行一次minor

gc,尽管这次minor gc 是有风险的。若小于或HandlePromotionFailure 设置不允许冒险,则这时要改为进行一次full gc。

类加载

类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载-验证-准备-解析-初始化-使用-卸载,其中验证-准备-解析称为链接。

在遇到下列情况时,若没有初始化,则需要先触发其初始化(加载-验证-准备自然需要在此之前):

1)1.使用new 关键字实例化对象2.读取或设置一个类的静态字段3.调用一个类的静态方法。

2)使用java.lang.reflect 包的方法对类进行反射调用时,若类没有进行初始化,则需要触发其初始化

3)当初始化一个类时,若发现其父类还没有进行初始化,则要先触发其父类的初始

化。

4)当虚拟机启动时,用户需要制定一个要执行的主类(有main 方法的那个类),虚拟机会先初始化这个类。


在加载阶段,虚拟机需要完成下面3 件事:

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

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

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


验证的目的是为了确保clsss 文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全。验证阶段大致会完成下面4 个阶段的检验动作:1)文件格式验证2)元数据验证3)字节码验证4)符号引用验证{字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全}。

准备阶段是正式为类变量分配内存并设置变量的初始化值得阶段,这些变量所使用的内存都将在方法区中进行分配。(不是实例变量,且是初始值,若public static int a=123;准备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被 final 修饰,public static final int a=123;在准备阶段后就变为了123)

解析阶段是虚拟机将常量池中的符号引用变为直接引用的过程。


静态代码块只能访问在静态代码块之前的变量,在它之后的变量,在前面的静态代码块中可以复制,但是不可以使用。


通过一个类的全限定名来获取定义此类的二进制字节流,实现这个动作的代码就是

“类加载器”。

比较两个类是否相同,只有这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个class 文件,被同一个虚拟机加载,只要加载他们的加载器不同,他们就是不同的类。


从Java 虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用c++实现,是虚拟机自身的一部分。另一种就是所有其他的类加载器,这些类加载器都由Java 实现,且全部继承自java.lang.ClassLoader。

从JAVA 开发人员角度,类加载器分为:

1)启动类加载器,这个加载器负责把\lib 目录中或者–Xbootclasspath

下的类库加载到虚拟机内存中,启动类加载器无法被Java 程序直接引用。

2)扩展类加载器:负责加载\lib\ext 下或者java.ext.dirs 系统变量指定路径下all 类库,开发者可以直接使用扩展类加载器。

3)应用程序类加载器,负责加载用户路径classpath 上指定的类库,开发者可以直接使用这个类加载器,若应用程序中没有定义过自己的类加载器,一般情况下,这个就是程序中默认的类加载器。

双亲委派模型:若一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把所这个请求委派给父类加载器去完成,每一层的加载器都是如此,因此all 加载请求最终都应该传送到顶级的启动类加载器。只有当父类加载器反馈自己无法加载时(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

双亲委派模型好处:eg,object 类。它存放在rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器加载,因此object 类在程序的各种加载环境中都是同一个类。


线程安全与锁优化东西太多了,直接见《深入理解Java 虚拟机:JVM 高级特性与最佳实践》最后一章。

当运行一个程序时,JVM 启动,运行bootstrap classloader ,该classloader 加载核心API

(Ext classloader 和app classloader 也在此时被加载),then 调用ext classloader 加载扩展API,最后app classloader 加载classpath 目录下定义的class,这就是一个程序最基本的加载流程。

通过classloader 加载类实际上就是加载的时候并不对该类进行解析,因此也不会初始化,而class 类的forName 方法则相反,使用forName 方法加载的时候会将class 进行解析与初始化。


Error 和exception 的区别?Error 类一般指与虚拟机相关的问题,比如系统崩溃,虚拟机错误,内存空间不足,对于这种错误导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误,建议让程序终止。Exception 表示程序可以处理的异常,遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。


1)final 定义的常量,一旦初始化,不能被修改,对基本类型来说是其值不可变,而对引用来说是其引用不可变。其初始化只能在两个地方1.其定义处2.构造函数中,二者只能选其一,不能在定义时给了值,又在构造函数中赋值。2)不管有无异常发生,finally 总会执行,若catch 中有return 语句,也执行,在return 语句之前执行。3)一个类不能既被声明为abstract,也被声明为final 4)finalize 方法定义在object 中。


在Java 中,内存泄漏就是存在一些被分配的对象,这些对象存在以下一些特点:1)对象是可达的,即在有向图中,存在通路可以与其相连2)对象是无用的,即程序以后不会再使用这些对象。这些对象不会被gc 所回收,然而他们却占用内存。

发生内存泄漏的第一个迹象通常是:在应用程序中出现了OutOfMemoryErroe(OOM)


内存溢出:程序在申请内存时,没有足够的内存空间供其使用。内存泄漏:分配出去的内存不再使用,但是无法回收。

JVM 中,一个字节以下的整形数据byte,-128 到127 会在jvm 启动时加载入内存,除非用new Integer()显示的创建对象,否则都是同一个对象。Integer 的value 方法返回一个对

象,先判断传入的参数是否在-128 到127 之间,若已经存在引用,则直接返回引用,否则返回new Integer(n).

Integer i1=59;

Int i2=59;

Integer i3=Integer.valueOf(59);

Integer i4=new Integer(59); i1==i2 是true,因为只有一份;i1==i3 是true,因为59 在-128 到127 之间,因为内存中已经有了59 的引用,则直接返回;i3==i4 是false ;i4==i2 是true,因为i2 为int 类型,在和i4 比较时,i4 会自动拆箱。


Integer i=100;相当于编译器为我们做了Integer i=Integer.value(100).


Java 虚拟机里的对象由3 部分组成:1)对象头:标记字段(在32 位和64 位jvm 中分别为32bits 和64bits)+类型指针2)实例数据3)对齐填充。

若对象是一个java 数组,则对象头中还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通java 对象的元数据信息确定java 对象的大小,但是从数组的元数据中无法确定数组的大小。

对象大小必须是8 字节的整数倍。

Xms:堆初始大小。Xmx:堆最大大小。


内存溢出:程序在申请内存时,没有足够的空间供其使用。内存泄漏:分配出的内存不再使用,但无法回收。

Xss:设置栈的大小。

标记-清除:首先标记出all 需要回收的对象,在标记完成后统一回收all 被标记的对象。

Cms:并发收集、低停顿。


Gc 进行时必须停顿all java 执行线程(stop-the-world),即使在号称几乎不会发生停顿的cms 收集器中,枚举根节点时必须停顿。(因为不可以发生在分析过程中对象引用关系还在不断变化)。


在大多数情况下,对象在新生代eden 区中分配,当eden 区域没有足够的内存空间时,虚拟机发起一次minor gc。

Xmn:分给新生代的大小。

虚拟机给每一个对象定义一个对象年龄计数器,若对象在eden 出生并经过第一次minor

gc 后仍然存活,并且能被survivor 容纳的话,将被移到survivor 空间中,并且对象年龄设为1.对象在survivor 中每熬过一次minor gc,年龄就+1,当他年龄达到一定程度(默认为15),就会晋升到老年代。


虚拟机可以从方法表中的acc_synchronized 访问标志得知一个方法是否为同步方法。当方法调用时,调用指令将会检查方法的acc_synchronized 访问标志是否被设置了。若被设置了,执行线程就要求先成功持有moniter,然后才能执行方法,最后当方法完成(无论是正

常完成还是非正常完成)时释放moniter。在方法执行期间,执行线程持有了moniter,其他任何线程都无法再获取到同一个moniter,若一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个方法所持有的moniter 将在异常执行到同步方法之外时自动释放。


加载、验证、准备、初始化卸载这5 个阶段的顺序是确定的。


到初始化的阶段才真正开始执行类中定义的Java 程序代码(或者说是字节码)。初始化阶段是执行类的构造器(())方法的过程。

()方法是由编译器自动收集类的all 类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。编译器收集的顺序是由语义在源文件中出现的顺序决定的。

()方法与类的构造函数不同,他不需要显示的调用父类的构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.object.

由于父类的()方法先执行,也就意味着父类中的静态语句块要优于子类的变量赋值。

接口中,不能有静态代码块。

虚拟机会保证一个类的()方法在多线程环境中被正确的加锁同步。若多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都要阻塞等待,直到活动线程执行()完毕。


若自己编写一个类java.lang.Object 并放在classpath 下,可以正常编译,但永远无法被加载。


若有一个类加载请求,先检查是否已经被加载,若没有被加载,则调用父类加载器的

loadclass    方法,若父类加载器为空,则默认使用启动类加载器作为父类加载器,若父类加载失败,抛出classNotFoundException,再调用自己的findclass 方法进行加载。


语法糖,在计算机语言中添加的某种语法。这种语法对语言的功能并没有影响,但是更方便程序员使用。java 中最常用的语法糖有:泛型、变长参数,自动拆箱/装箱。虚拟机运行时不支持这些语法,他们在编译阶段还原回简单的基础语法结构,这个过程为解语法糖。包装类的“==”运算,在不遇到算术运算的情况下,不会自动拆箱,遇到了就自动拆箱。


java 中的运算并非原子操作,导致volatile 变量的运算在并发下并不一定安全。


只有使用invokespecial 指令调用的私有方法,实例构造器,父类方法,以及invokestatic 指令调用的静态方法,才是在编译期进行解析的,除了上述四种方法,其他java 方法调用都需要在运行期进行方法接收者的多态选择,并且很有可能存在多于一个版本的方法接收者

(最多再除去被final 修饰的方法,尽管它用invokevirtual 调用,但也是非虚方法),java 中默认的实例方法是虚方法。


每次使用use volatile 变量之前,都必须先从主内存刷新最新的值,用于保证能看见其他线程对该变量所做的修改后的值。在工作内存中,每次修改volatile 变量后,都必须立刻同步回主内存中,用于保证其他线程可以看到自己对该变量所做的修改。volatile 修饰的变

量不会被指令重排序优化,保证代码的执行顺序与程序顺序相同。原子性、可见性、有序性。

Happen-before:对于一个volatile 变量的写操作,先行发生于后面对这个变量的读操作。


各个线程可以共享进程资源(内存地址、文件Io),也可以独立调度。线程是CPU 调度的基本单位。


synchronized 关键字经过编译后,会在同步块的前后分别形成moniterenter 和mointerexit 这两个字节码指令,这两个字节码指令都需要一个reference 类型的参数来指明要锁定和解锁的对象。若java 程序中的synchronized 明确指定了对象参数,那就是这个对象的reference;若没有明确指定,那就根据synchronized 修饰的是实例方法或类方法去取对应的对象实例或

class 对象来作为搜索对象。

在执行moniterenter 指令时,首先要尝试获取对象的锁,若这个对象没被锁定,或当前线程已经拥有那个对象的锁,把锁的计数器加一。相应的,在执行mointerexit 指令时,会将计数器-1,当计数器为零时,锁被释放。若获取对象锁失败,则当前线程就要阻塞等待,直到对象锁被另外一个线程释放。

对于线程的阻塞或唤醒,需要用户态和核心态切换,状态切换需要很多处理器时间。

类只可以是public 或包访问权限的。组合:只需要将对象引用置入新类中。

字符串与任意数据类型相连接都用“+”,最终都变为字符串。S.O.P(a);调用的是 a  的

toString 方法。“source”+aa;由于只能将一个 String 对象与另一个 String 对象相加,所以编译器会调用 aa 的 toString 方法,将 aa 转为 string 对象。对于空指针 Person p=null;S.O.P(p);不会报null 指针异常,而是直接打印null;

java 内存模型:工作内存,主内存。

Concurrenthashmap 和hashtable 不允许空键空值。

-xx:newRadio:设置 young 和 old 的比例;-xx:survivorRadio:设置 eden 和 survivor 的比例:

survivor 大了,会浪费空间,空间利用率低。若survivor 太小,会使一些大对象在minor gc

时直接从eden 区到old 区,让old 区的gc 频繁。

Xmx:堆到最大值;

Xms:堆到初始值;

Xmn:年轻代的大小;

Xss:栈的大小。我在测试OOM 时,Xmx 和Xms 都设为20M。。

JVM关闭:1)正常关闭:当最后一个非守护线程结束或调用了System.exit或通过其他特定于平台的方式,比如ctrl+c。2)强制关闭:调用Runtime.halt方法,或在操作系统中直接kill(发送single信号)掉JVM进程。3)异常关闭:运行中遇到RuntimeException 异常等。 

在某些情况下,我们需要在JVM关闭时做一些扫尾的工作,比如删除临时文件、停止日志服务。为此JVM提供了关闭钩子(shutdown hocks)来做这些事件。

Runtime类封装java应用运行时的环境,每个java应用程序都有一个Runtime类实例,使用程序能与其运行环境相连。 

关闭钩子本质上是一个线程(也称为hock线程),可以通过Runtime的addshutdownhock

(Thread hock)向主jvm注册一个关闭钩子。hock线程在jvm正常关闭时执行,强制关闭不执行。 

对于在jvm 中注册的多个关闭钩子,他们会并发执行,jvm 并不能保证他们的执行顺序。

JDBC:桥接模式

局部变量没有默认值。

-Xmx:最大堆大小

-Xms:初始堆大小

-Xmn:年轻代大小

-XXSurvivorRatio:年轻代中eden 和survivor 区的大小比值。

默认空余堆内存小于40%时,JVM 就会增大堆到-Xmx 的最大值;空余堆内存大于70%

时,JVM 会减小堆到-Xms 的最小值。


《深入理解java 虚拟机》68 页:1)对分配内存空间的动作进行同步处理——采用cas 配上失败重试的方式保证更新操作的原子性。2)把内存分配的动作按照线程划分在不同的空间上进行,每个线程在堆中预先分配1 小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的TLAB 上分配,只有TLAB 用完,要分配新的TLAB 时,才需要加锁。

分为新生代和老年代。Java 堆物理上可不连续,只要逻辑上连续。

堆归整(用了的在一边,没用的在另一边)。

在使用serial、parnew 等带compat 过程的收集器时,系统采用的分配方法是指针碰撞。而使用cms 这种基于mark-sweep 算法的收集器时,通常采用空闲列表。

CMS 收集器——目的:获取最短的回收停顿时间;基于标记-清除。初始标记,并发标记,重新标记,并发清除。初始标记和重新标记这两步需要“Stop the world”,初始标记只是标记GC root 能直接关联到的对象,速度快。并发标记就是进行GC root tracing 的过程,而重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。

并发标记和并发清除中,垃圾收集线程可以和用户线程同时工作。并发收集,低停顿。

缺点:1)对CPU 资源敏感,cms 默认启动的回收线程数是(cpu 数量+3)/4;2)无法处

理浮动垃圾,由于cms 并发清除阶段,用户线程还在继续执行,伴随程序进行,还有新的垃圾产生,这一部分垃圾发生在标记之后,cms 无法在当次收集时处理他们,只能留到下一次

gc。3)它基于标记-清除,产生大量内存碎片,大对象分配困难。

Offer、peek、pool 不抛异常。

Start()和run()——1)start 方法启动线程,真正实现多线程运行,通过调用Thread 类的

start 方法来启动一个线程,这时此线程是处于就绪状态,并没有运行,若cpu 调度该线程,则该线程就执行run 方法;2)run 方法当做普通方法的方式调用,程序要顺序执行,要等

run 方法执行完毕,才可以执行下面的代码,程序中只有主线程这一个线程(除了gc 线程)。

常量池——byte、short、int、long、char、boolean 包装类实现了的常量池(float、double 没有);

Integer i1=123;Integer i2=123;I1==i2;是true 的;Boolean b1=true;Booleanb2=true;b1==b2;是true 的;

常量池主要用于存放两大类常量:1)字面量、符号引用。字面量相当于java 语言层面常量的概念,符号引用包括类和接口的全限定名,字段名称和描述名称,方法名称和描述符。运行时常量池有动态性,java 语言并不要常量一定只有在编译时产生,也就是并非预置

入class 文件中常量池的内容才能放入常量池,运行期间有新的常量也可放入池中,比如

String 的intern 方法。优:对象共享,节省内存空间,节省运行时间。

单调栈——单调栈的维护是O(n)的时间复杂度。所有元素只会进入栈一次,并且出栈后再也不会入栈。性质:1)单调栈里元素有单调性;2)元素加入栈前,会在栈顶端把破坏栈单调性的元素都删除;3)使用单调栈可以找到元素向左遍历第一个比它小的元素,也就可以找到元素向左边遍历第一个比它大的元素。

CMS:由于在垃圾收集阶段用户线程还在运行,也就是还需要预留有足够的内存空间给用户线程使用,因此cms 收集器不能像其他收集器一样,等到老年代几乎完全被填满了再进行收集,需要预留一部分空间,提供并发收集时的线程序使用。Jdk1.5 中,默认老年使用

68%后就会被激活cms,jdk1.6 变为92%,要是cms 运行期间预留的内存不够,就会出现一次“concurrent mode failure”,这时 JVM 会启动后备预案,临时启用 serial 收集器来重新进行老年代的垃圾收集,这样停顿的时间就长了。

垃圾收集器是自动运行的,一般情况下,无须显示的请求垃圾收集器,调用System 类的gc 方法可以运行垃圾收集器,但这样并不能保证立即回收指定对象。

1)垃圾收集器并不是一个独立的平台,他具有平台依赖

2)一段程序可以建议垃圾回收执行,但是不能强迫他执行

3)当一个对象的all 引用都被置为null,这个对象就可以变为能被垃圾回收

调用System.gc(),这是一个不确定的方法。Java 中并不保证每次调用该方法就一定能够启动垃圾收集,他不过是会向JVM 发送这样一个申请,到底是否真正执行垃圾收集,一切都是未知数。

Java 虚拟机栈,每个方法对应一个栈帧,存放局部变量表、操作数栈、方法出口等信息。堆分为新生代和老年代。Java 堆物理上可不连续,只要逻辑上连续。

堆归整(用了的在一边,没用的在另一边)。

在使用serial、parnew 等带compat 过程的收集器时,系统采用的分配方法是指针碰撞。而使用cms 这种基于mark-sweep 算法的收集器时,通常采用空闲列表。

《深入理解java 虚拟机》68 页:1)对分配内存空间的动作进行同步处理——采用cas 配上失败重试的方式保证更新操作的原子性。2)把内存分配的动作按照线程划分在不同的空间上进行,每个线程在堆中预先分配1 小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在哪个线程的TLAB 上分配,只有TLAB 用完,要分配新的TLAB 时,才需要加锁。

强引用:类似Object o=new Object(),只要强引用还在,则垃圾收集器永远不会收集被引用的对象。软引用:有但并非必须的对象。Softreference 类实现软引用。弱引用:用

weakreference 类实现。虚引用:用phantomreference 类实现。

程序执行时并非在all 地方都能停顿下来开始gc,只能在到达安全点才可以。使用wpmap 来得知哪些地方存放着对象引用。

Jps:列出正在运行的虚拟机进程。

Jstat:虚拟机的运行状态信息。类加载、垃圾收集、运行期编译状况。

Jinfo:虚拟机参数。

Jmap:堆转储快照。Heapdump/dump 文件。

Jhat:分析Heapdump 文件。

Jstack:用于生成虚拟机当时时刻的线程快照。

Jconsole Visual VM

Memory analysis tool Support assistant

源文件通过编译变为字节码文件。

任何一个class 文件都对应着唯一一个类or 接口的定义信息。

Class 文件为二进制字节流,以8 位字节为基础。1)每个class 文件的头4 个字节成为魔数,他的唯一作用是确定这个文件是否为一个能被虚拟机接受的class 文件。2)紧接着魔数的4 个字节是class 文件的版本号(5、6 字节为次版本号,7、8 字节为主版本号)。3)紧接着主次版本号的是常量池入口。4)紧接着两个字节为访问标志。5)类索引、父类索引、接口索引集合。6)字段表。7)方法表。8)属性表(java 程序方法体中的代码经过java 编译后,最终变为字节码指令存在code 属性中)。

Java 虚拟机的操作码只有一个字节。

读屏障用于保证读操作有序,屏障之前的读操作一定会优先于屏障之后的读操作完成,写操作不受影响。

优化屏障用于限制编译器的指令重排。通用屏障则对读写操作都有用。

Animal a=new Dog();Animal 是静态类型,dog 是实例类型。虚拟机(准确的说是编译器),在重载时是通过参数的静态类型,而不是实例类型作为判断依据。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载。静态分配发生在编译阶段,因此确定静态分派的动作,实际上不是由虚拟机执行。

多态,重写,动态分派。

java 语言是一门静态多分派,动态单分派。

Java、c++都是静态类型语言,即在编译期进行类型检查。在运行期进行类型检查的是

动态类型语言。

运行时异常,就是只要代码不运行到这一行,就不会有问题。

JIT 编译,在虚拟机内部,把字节码转换为机器码。

Java 编译器是由java 语言写的程序。编译的三个过程:1)解析与填充符号表过程。2)插入或注解处理器的注解处理过程3)分析与字节码生成过程。

点代码需要即时编译,JIT

编译,判断一段代码是否是热点代码叫热点探测。1)基于采样的热点探测:采用这种方法的虚拟机会周期性的检查各个线程的栈顶,若发现某个或某些方法经常出

现在栈顶,则该方法为热点方法2)基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器,统计方法的执行次数,若执行次数超过一次阈值,则

认为是热点方法。

即时编译优化技术1)公共子表达式消除:如果一个表达式E 已经计算过了,且从先前的计算到现在,E

中所有变量的值都没有发生变化,则E

的这次出现就成了公共子表达式。对于这种表达式,没有必要花时间再对他进行计算,只需要直接用前面计算过的表达式结果替代E

即可2)数组边界检查消除3)方法内联4)逃逸分析。


处理器的指令:比较并交换,cas 指令。cas 指令需要三个操作数,分别为内存位置(在

java 中,可以简单理解为内存地址,用v 表示)、旧的预期值A 和新值B。cas 指令执行时,当且仅当V 符合旧的预期值A 时,处理器用新值B 更新V 值,否则他就不更新。但无论是否更新了V 值,都会返回旧的V 值。上述过程是一个原子过程。

cas 操作定义在unsafe 类中,但用户程序不能直接调用unsafe 类,要通过其他java api。比如整型原子类compareandset()和Increamentandget()等方法都使用了unsafe 类的cas 操作。

Increamentandget():以原子的方式将当前值加一,该方法在一个无限循环中,不断尝试把一个比当前值大一的值赋给自己。若失败了,则表明在compareandset 操作时已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。

Cas 的逻辑漏洞,若一个变量V 初次读取的时候是A 值,并且准备赋值的时候,检查到他仍然为A 值,那我们就能说它的值没被其他线程改变过吗?若在这段期间他的值曾被改变为B 值,后来又被改变回A,则cas 操作会误认为它从来没有被改变过,这个漏洞被称为

ABA 问题。解决办法:控制变量值的版本。

轻量级锁,在没有多线程竞争的条件下,减少传统的重量解锁使用操作系统互斥量带来的性能消耗(使用cas 操作)。如果说轻量级锁是在无竞争的情况下,用cas 操作消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步去掉,连cas 操作都不做了。

用遥控板(句柄)控操纵电视(对象),即使没有电视机,遥控板也可以单独存在,即拥有一个句柄,并不表示必须有一个对象同他联系。

{

Int x=100;

{

Int x=1; //不合法,因为编译器认为变量已经被定义

}

}

几个类通过编译后就产生几个字节码文件

持久化java 堆溢出:使用cglib 技术直接操作字节码运行,产生大量的动态类;年老代溢出:上万次字符串处理,创建上万个对象或在一段代码内申请上百M 甚至上G 的内存。

推荐阅读更多精彩内容