Java虚拟机技术归总

Java虚拟机重点以及 知识

一、 JVM内存分区
分为程序计数器、虚拟机栈、本地方法栈、Java堆、方法区5个区域

其中Java堆和方法区是线程共享的,虚拟机栈、本地方法栈、程序计数器是线程隔离的。

程序计数器:
1.可以看作当前线程所执行的字节码的行号指示器
2.Java多线程之间进行切换的时候需要之后恢复到之前执行位置,所以每条线程需要一个程序计数器,程序计数器是线程隔离的。
3.不会发生内存溢出

虚拟机栈(堆内存):
描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧用于存储局部变量表什么的。方法的调用和执行完成对应着 栈帧在虚拟机栈中的入栈和出栈。
局部变量表里放的是编译期可知的基本类型和对象引用和returnAdress。局部变量表所需的内存空间在编译期间完成分配,运行期间不会改变。
线程请求的栈深度大于虚拟机允许会发生Stackoverflowerror,扩展栈时如果无法申请到足够空间会发生OutOfMemoryError.

本地方法栈:
与虚拟机栈类似,但是本地方法栈是为native方法服务的。
一个Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。
和虚拟机栈一样,Stackoverflowerror和OutOfMemoryError

Java堆:
被所有线程共享的区域,用来存放对象实例,几乎所有对象实例都在这分配内存
Java堆没有内存分配了并且无法扩展时抛出OutOfMemoryError

方法区:
所有线程共享的区域,用于存储已被加载的类信息、常量、静态变量等数据。
Java8之前HotSpot使用永久代实现方法区,现在Java8移除了永久代。改用了元空间,使用的是本地内存,非jvm区域。
无法满足内存分配时抛出OutOfMemoryError。

GC算法

知道的GC算法有四种:标记-清除算法、复制算法、标记整理、分代收集算法。

标记-清除算法:
分“标记”和“清除”两个阶段:先标记出所有需要回收的对象,标记完成后统一回收。
最基础的收集算法,有两个缺陷:1.效率不高 2.会产生大量不连续的内存碎片,导致之后分配大对象空间不够,而不得又触发垃圾回收。

复制算法:
将可用内存按照容量划分为大小相等的两块,每次只使用其中一块。第一块用完了,就将第一块里存活的复制到第二块,再回收掉第一块全部。
这样不用考虑内存碎片且运行高效。但是代价是缩小一半可用,代价大。
然后实际上会把可用内存空间按8:1:1分为Eden和两块Survivor 并称为新生代,每次使用Eden和一块Survivor也就是90%,回收时把存活的对象放到剩余的那块Survivor上,再回收。减少浪费。如果剩余那块Survivor放不下,就会放到Java堆的老年代里去。

标记-整理算法:
由于可能存在大量对象存活的情况,那么复制算法复制较多效率会变低。
而根据老年代存活对象较多的特点,用标记-整理算法,与标记清除算法区别在于标记后得把活着的整理到一端,再把另一端回收。

分代收集算法:
并没有新的收集算法,而是把Java堆按2:1分为新生代和老年代,根据特点选用收集算法。新生代采用复制算法,老年代采用标记-整理或标记-清除。

助于目录的生成

直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。

GC回收器

Serial收集器:
单线程收集器,只会使用一条线程去完成收集,在收集时必须暂停其它所有工作线程。
缺点很明显,但优点在于简单而高效,没有线程交互开销
在clien的用户程序模式下,暂停几十毫秒,用户感受不到,可以接收。

ParNew收集器:
Serial的多线程版本,多线程去垃圾回收.
Server模式下虚拟机首选的新生代收集器,原因在于除Serial收集器外只要它可以和CMS这款强大的并发老年代收集器配合。

ParallelScavenge收集器:
特点在于关注点和其它收集器不同,CMS等收集器关注点在于尽可能缩短用户线程的停顿时间,而Parallel Scavenge收集器目的是控制吞吐量,即控制用户代码时间在用户代码时间+GC时间中的比例。

SerialOld收集器:
Serial收集器的老年代版本,采用标记-整理算法。主要给客户端模式下的虚拟机使用。在Server模式下两个用途:jdk1.5之前和Parallel Scavenge收集器搭配使用,其次是作为CMS收集器的后备预案

ParallelOld收集器:
ParallelScavenge收集器的老年版本。与新生代收集器Parallel Scavenge 配合实现吞吐量的控制和利用多处理器能力。

CMS收集器:
以最短回收停顿时间为目标的收集器,“标记-清除”算法,过程分为四个步骤:

  1. 初始标记:标记GC Roots能直接关联到的对象
    
  2. 并发标记:顺着GC Roots寻找的过程
    
  3. 重新标记:修改并发标记期间程序运行而改动的对象
    
  4. 并发清除
    

其中初始标记、重新标记需要停止其它工作线程,并发标记和并发清除不需要。
由于整个过程中耗时最长的并发标记和并发清除过程是可以和用户线程一起的,所以总体上CMS收集器是可以和用户线程一起的。
优点:并发收集、低停顿
缺点:
5. 占用大量CPU资源
6. CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生。

浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”。
7. 标记-清除容易产生空间碎片,容易提前触发Full GC。

G1收集器:
希望未来可以替代CMS的收集器。将Java堆划分为多个大小相等的独立区域。
8. 并行和并发
9. 分代收集,且G1就可以独立管理整个Java堆
10. 整体上采用“标记-整理”、局部(两个独立区域之间)上是复制算法。
11. 可以指定某个时间片段上,垃圾收集不超过的时间:维护了独立区域的回收价值优先列表,优先回收回收价值高的(Garbage First名字由来),实现指定时间效率。

回收步骤:
12. 初始标记(标记GC Roots能直接关联到的对象)
13. 并发标记(顺着GC Roots寻找的过程,只有这个是并发的,表示和用户一起)
14. 最终标记(修改并发标记期间程序运行而改动的对象)
15. 筛选回收

Full GC、Minor GC等GC概念的总结

Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;Minor GC和Major GC是俗称,对应着Young
GC和Old GC
Minor GC ,FullGC
触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
1.包括复制算法移到这时,
2.堆上分配大对象或大数组直接进入老年代时,
3.通过Minor GC时,计算之前晋升到老年代的平均大小,当老年代剩余空间小于平均值时
(3)如果有永久代的话,永久代不够用也会触发
(4)CMS GC时出现concurrent mode failure

Java的四种引用

强引用、软引用、弱引用、虚引用。

强引用:是指创建一个对象并把这个对象赋给一个引用变量。
比如:Object object =new Object();,只有强引用还在,就不会回收

软引用:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。可以使用SoftReference类实现。
弱引用:弱引用也是用来描述非必需对象的,比软引用更弱,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。可以用WeakReference类实现

虚引用:不影响对象的生命周期。java中用PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。唯一目的是为了回收时有个系统通知。

可达性分析(判断哪些对象该回收)

主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。基本思路就是通过一系列名为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相关联,但它们到GCRoots是不可达的,所以它们将会判定为是可回收对象。

可作为GC Roots的对象

虚拟机栈中引用的对象、
方法区类静态属性引用的对象、
方法区常量引用的对象、
本地方法栈JNI(Native方法)引用的对象

内存泄漏和内存溢出的区别和联系

1.内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

2、内存溢出指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。

3、二者的关系
内存泄漏的堆积最终会导致内存溢出内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误。内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还,结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

JVM分代问题、新生代、老年代

商业虚拟机都采用分代收集算法,将Java堆按1:2的比例分成新生代和老年代。根据不同年代采用不同收集算法,新生代存的都是新生的对象,垃圾收集时大多都会死,适合采用复制算法。而老年代存储的都是生命周期较长的,回收的较少,适合采用标记-整理或标记-清除。

分代的目的:(缩短GC导致的停顿时间)
通常GC的整个工作过程中都要“stop-the-world”,如果能想办法缩短GC一次工作的时间长度就是件重要的事情。如果说收集整个GC堆耗时太长,那不如只收集其中的一部分

新生代和老年代存储的是什么:这就关于jvm内存分配的问题:

新生代:新创建的对象优先在新生代的Eden上分配。
老年代:
1.新生代对象每经历依次minor gc,年龄会加一,当达到年龄阀值会直接进入老年代。阀值大小一般为15
2.Survivor中年龄相同的对象数量的总和大于survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而无需等到年龄阀值
3.大对象大数组直接进入老年代
4.新生代复制算法需要一个survivor区进行轮换备份,如果出现大量对象在minor gc后仍然存活的情况时,就需要老年代进行分配担保,让survivor无法容纳的对象直接进入老年代

类加载机制

什么是类加载机制:
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。如下:

有加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析统称为连接。其中除解析外,其它都是按顺序开始的(几个步骤可能同时进行),解析则有可能在初始化之后才开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

虚拟机对于类的初始化阶段严格规定了有且仅有只有5种情况如果对类没有进行过初始化,则必须对类进行“初始化”!

  1. 遇到new、读取一个类的静态字段(getstatic)、设置一个类的静态字段(putstatic)、调用一个类的静态方法(invokestatic)。
  2. 使用java.lang.reflect包的方法对类进行反射调用时。
  3. 当类初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。(如果是接口,则不必触发其父类初始化)
  4. 当虚拟机执行一个main方法时,会首先初始化main所在的这个主类。
  5. 当只用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(暂未研究此种场景)

加载:
通过一个类的全限定名来获取定义此类的二进制字节流。 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。验证
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。而实例变量是在类实例化的时候初始化,和类加载没关系!
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的 Java
程序代码(或者说是字节码)。

双亲委派模型

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在Java虚拟机的唯一性。比较两个类是否相等不能光看他们本身来自同一个类,同时也要看加载它们的类加载器是否相同。

绝大部分Java程序会使用到以下3种系统提供的类加载器。

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现,是虚拟机自身的一部分。负责加载<JAVA_HOME>\lib中(或者-Xbootclasspath参数所指定路径),并且是虚拟机识别的类库 加载到虚拟机内存中。不可直接被Java程序引用。
  2. 扩展类加载器(Extension ClassLoader),这个类加载器由Java实现,独立于虚拟机外部。负责加载<JAVA_HOME>\lib\ext中(或者被java.ext.dirs系统变量所指定)的所有类库。开发者可直接使用扩展类加载器。
  3. 应用程序类加载器(Application ClassLoader),也称之为系统类加载器,同样也由Java实现,独立于虚拟机外部。负责加载用户类路径(ClassPath)上所指定的类库。开发者可直接使用这个类加载器。
  4. 如果有必要可以自己定义类加载器(继承ClassLoader,重写方法)

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
对Java程序稳定运作很重要,防止类混乱,比如你就不可能去编写java.lang.Object了,层层上交到顶层会发现已存在。

能不能自己写个类叫java.lang.System?
答案:通常不可以,但可以采取另类方法达到这个需求。
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

对象的创建

java是面向对象的语言,因此对象的创建无时无刻都存在。在语言层面,使用new关键字即可创建出一个对象。但是在虚拟机中,对象创建的创建过程则是比较复杂的。
  首先,虚拟机运到new指令时,会去常量池检查是否存在new指令中包含的参数,比如newPeople(),则虚拟机首先会去常量池中检查是否有People这个类的符号引用,并且检查这个类是否已经被加载了,如果没有则会执行类加载过程。
  在类加载检查过后,接下来为对象分配内存当然是在java堆中分配,并且对象所需要分配的多大内存在类加载过程中就已经确定了。为对象分配内存的方式根据java堆是否规整分为两个方法:1、指针碰撞(Bump
thePointer),2、空闲列表(Free List)。指针碰撞:如果java堆是规整的,即所有用过的内存放在一边,没有用过的内存放在另外一边,并且有一个指针指向分界点,在需要为新生对象分配内存的时候,只需要移动指针画出一块内存分配和新生对象即可;空闲列表:当java堆不是规整的,意思就是使用的内存和空闲内存交错在一起,这时候需要一张列表来记录哪些内存可使用,在需要为新生对象分配内存的时候,在这个列表中寻找一块大小合适的内存分配给它即可。而java堆是否规整和垃圾收集器是否带有压缩整理功能有关。
  在为新生对象分配内存的时候,同时还需要考虑线程安全问题。因为在并发的情况下内存分配并不是线程安全的。有两种方案解决这个线程安全问题,1、为分配内存空间的动作进行同步处理;2、为每个线程预先分配一小块内存,称为本地线程分配缓存(ThreadLocal
Allocation Buffer, TLAB),哪个线程需要分配内存,就在哪个线程的TLAB上分配。
  内存分配后,虚拟机需要将每个对象分配到的内存初始化为0值(不包括对象头),这也就是为什么实例字段可以不用初始化,直接为0的原因。
  接来下,虚拟机对对象进行必要的设置,例如这个对象属于哪个类的实例,如何找到类的元数据信息。对象的哈希吗、对象的GC年代等信息,这些信息都存放在对象头之中。
  执行完上面工作之后,所有的字段都为0,接着执行指令,把对象按照程序员的指令进行初始化,这样一个对象就完整的创建出来。

jvm中垃圾回收与finalize方法

即使在可达性分析算法中不可达的对象,也并非“非死不可”的,这时候它们处于“缓刑”阶段。一个对象的真正死亡,至少要经历两个标记过程:如果对象在进行可达性分析后发现没有与GC
Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况视为“没有必要执行”。

如果这个对象被判定为有必要执行finalize()方法,那么这个对象会被防止在一个叫做F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize方法中执行的很慢,或者产生了死循环,将很可能到值F-Queue队列中的其他对象永久处于等待,甚至导致真个内存回收系统崩溃。
Finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己-----只需要重新与因用力按上的任何一个对象建立关联即,比如把自己赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱那基本上就真的被回收了。

jvm常见参数

各主要JVM启动参数的作用如下:

-Xms:(堆内存初始值)Java Heap初始值,Server端JVM最好将-Xms和-Xmx设为相同值,开发测试机JVM可以保留默认值;

-Xmx:(堆内存最大值)Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定;

-Xmn:(堆年轻代大小)Java HeapYoung区大小(统一最大最小都是它),不熟悉最好保留默认值;

-Xss:设置每个线程的堆栈大小(也就是说,在相同物理内存下,减小这个值能生成更多的线程)

-XX:NewRatio:设置年轻代与老年代之比,如-XX:NewRatio=4就表示年轻代与老年代之比为1:4
-XX:SurvivorRatio=8:设置新域中Eden区与Survivor区的比值。则比例为1:8,总共1:1:8

十五、jvm调优工具与基本思路

工具有:jdk的bin目录下命令行工具JStack,Jmap等,可视化工具JConsole、visualVM

查看堆空间大小分配(年轻代、年老代、持久代分配)
垃圾回收监控(长时间监控回收情况)
线程信息监控:系统线程数量
线程状态监控:各个线程都处在什么样的状态下
线程详细信息:查看线程内部运行情况,死锁检查
CPU热点:检查系统哪些方法占用了大量CPU时间
内存热点:检查哪些对象在系统中数量最大

Javac字节码编译过程

大致可以分为3个过程:

1.解析与填充符号表过程。
–词法、语法分析
–填充符号表
2.插入式注解处理器的注解处理过程。
3.语义分析与字节码生成过程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值