Java虚拟机——进度1

Java 虚拟机

 
   

一、Java虚拟机的基本结构

 

①类加载子系统:从文件系统或者网络中加载Class信息,存放在方法区中。

②方法区中存放放进来的Class信息,也包括一些运行时常量池信息包括字符串字面量和数字字面量。

④java堆外的直接内存,访问速度优于Java堆。可以申请用于读写频繁的场合。不会受限于最大堆大小,但会受限于操作系统给出的最大内存。

⑥垃圾回收系统自动处理②③④的回收

⑧Java虚拟机线程都有一个私有的java栈,其中保存着帧信息,局部变量,方法参数和java方法的调用和返回密切相关。

⑨java栈用于java方法的调用,本地方法栈用于本地方法的调用。Java虚拟机支持本地方法的调用,通常本地方法用C语言完成。

⑩PC寄存器和java私有栈一样也是每个线程的私有空间。Java虚拟机会为每个线程创建一个PC寄存器。PC寄存器指向当前正在被执行的指令,不过如果当前正在被执行的方法是本地方法,PC寄存器的值就是UNDIFINED

⑦执行引擎是java虚拟机的最核心组件之一,负责执行虚拟机的字节码。

1.Java堆得结构

Java堆的结构决定于垃圾回收机制的不同而不同。常见的一种结构是新生代和老年代。

 

 
   

新生代存放新生对象或者年龄不大的对象,老年代则存放老年对象。新生代有可能分为eden区,s0区,s1区;s1和s0又被称为from区和to区,两块大小相等,可以互换角色的内存空间。绝大多说情况下,对象首先分配在eden区,再一次新生代回收后,如果对象还存活,就会进入s0或者s1,之后每经过一次新生代回收,对象如果存货,它的年龄就会+1.当对象的年龄达到一定条件后,就会被认为是老年对象,从而进入老年代。

Public class simpleHeap{

Private int id;

Public SimpleHeap(int id){

This.id = id;

}

Public void show(){

System.out.println(“my id is “+id);

}

Public static void main(String[] args){

SimpleHeap s1 = new SimpleHeap(1);

S1.show();

}

}

2.java栈

Java堆和程序数据密切相关,java栈和程序的调用密切相关。每次函数调用数据都是通过java栈传递的。Java栈中存放的内容为栈帧,每一次函数调用都有一个栈帧被压入栈中,函数调用结束该帧就会出栈。栈帧内保存着当前函数的局部变量,中间运算结果等数据。函数return或者发生异常就会被弹出栈。

一个栈帧中,至少包含局部变量表,操作数栈和帧数据区。

 

当请求的栈深度大区最大可用的栈深度时,系统刚跑出StatckOverFlowError。

①局部变量表用于保存函数的参数以及局部变量。函数调用结束,局部变量表也会消失。变量表的大小影响函数的调用层次。槽位复用!垃圾回收!System.gc().局部变量表对内存空间的强引用。

②操作数栈用于计算过程中的中间结果,同事最为计算过程中变量临时的存储空间。

③帧数据区保存着常量池的指针,方便程序访问常量池。支撑常量池解析。正常方法返回和异常处理等。异常处理表和函数返回!

④栈上分配,把线程私有的对象(不可能被其他项城访问的对象)大三分配到栈上,有利于函数调用结束后进行自行销毁,不需要垃圾回收器介入,然而是否能够进行打散分配需要进行逃逸分析。-server模式!逃逸分析!标量替换!

3.方法区

方法区是一块所用线程共享的内存区域。它用于保存系统的类信息,比如累的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类,如果系统定义了太多了类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。JDK1.6和JDK1.7中的动态代理永久去大小!

JDK1.8中永久区别彻底删除,取而代之的是元数据区可以使用-XX;maxMetaspaceSizezhiding .是一块堆外的直接内存。与永久区不同,如果不指定大小,默认情况下虚拟机会耗尽所用的可用系统内存。

二、垃圾回收算法

1.引用计数法(Reference Counting)

为每一个对象配备一个整型的的计数器,被另外一个对象每引用一次,计数器+1,引用失效则-1,计数器为0时,则对象不再被引用。

特点:无法处理循环引用的情况;引用计数器在引用的产生和消除的时候要进行加减操作对系统性能有影响。

循环引用!两对象互相引用并且引用计数器不为0,但是没有第三个对象对这两个对象的引用了,垃圾回收器无法对这两对象进行回收。

 

2.标记清除法

标记清除法分为标记阶段和清除阶段,标记阶段是标记从根节点出发所有从根节点的可达对象,在清除阶段清除我被标记的对象

特点:会产生空间碎片问题,因为对象空间的回收是不连续的,而在堆空间的分配中不连续的空间分配在大对象的空间分配中的效率是低于连续空间的。

3.复制法

复制法的思想是把内存空间分为两块,分配空间时使用其中的一块,在垃圾回收时把正在使用的存活对象复制到另一块中并清空原来的一块。完成角色的互换。

特点:高效性用在存活对象小,垃圾对象多的情况下。确保回收后的内存空间是没有碎片的,但是代价是系统内存折半

 

Java中新生代串行垃圾回收器。From块和to块!

 

Java中新生代串行垃圾回收器。From块和to块!

 

4.标记压缩法

(标记清除压缩法:进行一次标记清除法后再进行一次碎片整理

标记压缩法是一种老年代的回收算法,它是标记从根节点开始的可达对象,并把这些存活对象压缩到内存的一端,然后清除边界外的垃圾对象。

特点:既不会像标记清除法那样产生空间碎片又不会像复制法那样折半消耗系统内存。

 

5.分代算法

分代算法主要是因为对象的使用频率和回收频率不同的特性进行制定的,对于新对象很大可能会被回收,而对于老对象很有可能是永久内存。但是复制法和标记压缩和标记清除等对这两种情况展现不同的特性。所以把内存空间分为新生代和老年代。新生代用复制法进行垃圾回收,老年代使用标记清除或者标记压缩进行垃圾回收

但是,在新生代的垃圾回收中的频率较高,有根据老年代的回收频率较低,所以为了支持高频率的新生代回收,引入了卡表(Card Table)的功能。

卡表是指bit集合,每一位标识老年代中的对象是否含有对新生代中对象的引用。在新生代的垃圾回收中,首先便利卡表,对没被老年代对象引用的新生代对象进行垃圾回收。

 

 

6.分区算法

与分代算法将不同生命周期的对象分成两部分不同,分区算法是将内存空间分成连续不同的小空间每个小空间都独立使用和回收。因为程序的停顿时间和回收的内存空间的大小成正比,所以为了控制停顿时间把要回收的内存空间的单位进行缩小

特点:可以控制一次回收多少个小空间。

三、垃圾收集器和内存分配

1.串行回收器(一心一意一件事)

串行回收器指用单线程进行垃圾回收的回收器,每次回收时只有一个回收线程。适用于并行能力较弱的计算机。

特点:仅仅使用单线程进行垃圾回收;是独占试的垃圾回收。

-XX:+UserSerialGC,新生代和老年代都是用串行回收器

 

2.并行回收器(人多力量大)

使用多个线程同时进行垃圾回收。

特点:在高性能计算机上面进行并行垃圾回收,大大缩短了垃圾回收的时间!

① ParNew回收器是简单的将串行回收器进行多线程化。

-XX:+UseParNewGC

-XX:+UseParNewThreads:指定并行 线程数量,一般与cpu数量相同,当cpu数量多于8个时为:3+((5*CPU_COUT)/8)。

 

② 新生代ParallelGC回收器

它是使用复制算法的收集器,关注系统的吞吐量。

-XX+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行回收器。

-XX+UseParallelOldGC:新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。

-XXMaxGCPauseMillis:设置最大垃圾收集停顿的时间。

-XXGCTimeRatio:设置吞吐量的大小。默认情况下,吞吐量的值为991/(1+99)=1%,系统用不超过1%的时间用于垃圾收集。

ParallelGCParNew回收器支持一种自适应的GC调节策略。使用-xx+UseAdaptiveSizePolicy可以代开自适应GC策略。在这种模式下新生代的大小、edensurivivior的比例、晋升老年代的对象年龄等参数会被自动调整。以达到在堆大小、吞吐量和停顿时间之间的衡点。

③ 老年代ParallelOldGC回收器

ParallelOldGC回收器使用标记压缩算法,它在JDK1.6中才可以使用。其他和ParallelGC回收器一样是一种多线程并发的收集器。

3.CMS回收器(并发标记清除回收器Concurrent Mark Sweep

CMS回收器的工作过程与其他垃圾收集器相比比较复杂,主要步骤有初始标记、并发标记、预处理、重新标记、并发清理、并发重置。

初始标记,并发标记和重新标记都是为了标记出需要回收的对象,并发清理则是在标记完成后,正式回收垃圾对象。并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数,为下一次垃圾回收做好准备。并发标记,并发清理和并发重置都是可以和应用程序线程一起执行的

 

1.G1回收器(Garbage-First)

JDK1.7中亨氏使用的全新的垃圾回收器,取代了CMS回收器。

① 特点:

并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。

并发性:G1拥有与应用呈交替执行的能力

分代GC:同时兼顾年轻代和老年代

空间整理:G1在回收过程中,会进行适当的对象移动吗,不像CMS只是简单的标记清理对象,在若干次GC后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片。

可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿也能得到较好的控制。

② G1的内存划分和主要收集过程

G1收集器将堆进行分区,划分为一个个的区域,每次收集的时候,只收集其中几个区域,以此来控制垃圾回收产生的一次停顿。

③ G1的收集过程可能有4个阶段:

新生代GC,并发标记周期,混合收集,如果需要,可能会进行Full GC。

1)新生代GC:和其他收集算法相似,会复制eden区和suivivor区,另外是G1老年代的区域增多,为部分eden区和survivor区晋升到老年代。

2)并发标记周期:G1的并发标记周期和CMS有点类似,他们都是为了降低一次停顿时间,而将可以和应用程序并发的部分单独提取出来执行。初始标记,根区域扫描,并发标记,重新标记,独占清理,并发清理阶段,

初始标记,伴随一次新生代GC。并发根区域扫描过程中不能被xinshengdaiGC打断。并发标记可以被新生代GC打断。重新标记会引起全局停顿。独占清理会重新计算各个区域的存活对象并以此可得到每个区域进行GC的效用。并发清理时并发执行的,它会根据独占清理阶段得出的每个区域的存活对象数量直接回收已经不包含存活对象的区域。

③ 混合回收

在并发标记周期阶段,对象被回收的比例还是很小的,但在并发周期之后,G1就会明确知道哪些区含有比较多的垃圾对象,在混合回收阶段可以专门针对这些区域进行回收。G1会优先回收垃圾比例较高的区域,因为回收这些区域的性价比也比较高

 

  

full GC

CMS类似,并发收集优于让应用程序和GC线程交替工作,因此总是不能完全避免在特别繁忙的场合会出现在回收过程中内存不足的情况。当遇到这种情况时,G1也会转入一个FUllGC进行回收。

内存分配

TLAB上分配对象,线程本地分配缓存。线程专属的区间,TLAB启用的情况下,虚拟机会为每一个java线程分配一块TLAB的空间。

 

 

        对象分配的简要流程

四、Class装载系统

1.class文件的装载过程

 

装载的条件是这个类必须满足主动使用的条件,初始化子类时会首先初始化父类。

在被动引用中如下

Public class Parent{

Static{

System.out.println(“parent init”);

}

Public static int v = 100;

}

Public calss Child extends Parent{

Static{

System.out.println(“Child init”):

}

}

Public class UseParent{

Public static void main(String[] args){

System.out.println(Child.v);

}

}

main函数中通过子类访问父类字段,输出结果是 Parent Init100,可以看到只有父类被初始化了,子类虽然没有被初始化但是已经被加载了。引用一个字段时只有直接地定义这个字段的类才能被初始化。另外如果引用一个类中的final常量时,该类不会被加载。因为final常量被存放在常量池中。

-XX+TraceClassLoading

① 类的加载
过程:通过类的全名,获取累的二进制数据流。解析类的二进制数据流为方法区内的数据结构。创建java.lang.Class类的实例。

 什么时候加载:当需要某个类的时候就会提前加载这个类,比如当出现某个类的名字的时候;还有一种是“主动加载”,也就是有一些虚拟机比如hotpot是在真正需要某个类的时候才加载这个类。

从哪加载:通过绝对路径加载Class类的文件、jar文件,还有非class文件,在jvm运行前会被转为字节码文件;还有的是网络加载,以前用的技术比如applet小程序,就是通过网络进行加载的;当然还有一种是自动生成的类,像动态代理设计模式一样!

加载到哪:把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

需要注意的点:类的加载和下一个阶段连接(验证,准备,解析)是交叉进行的。

② 验证类

 

③ 准备

当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置初始值。

 需要注意:为类的静态变量分配内存并设为JVM默认的初值(注意是JVM的初值哦),对于非静态变量,则不会为他们分配内存。

    JVM的初值:基本类型(int、long、short、char、byte、boolean、float、double)的默认值为 0;

          引用类型的默认值为null;

          常量的默认值就是程序中设定的值。

④ 解析类

在准备阶段完成后就进入了解析阶段,解析阶段的工作就是将类、接口、字段和方法的符号引用转为直接引用。

符号引用转直接引用是找到内存地址。

⑤ 初始化

类的初始化时类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。初始化阶段的重要工作是执行累的初始化方法《clinit》。方法是有编译器自动生成的,它是由类静态成员的赋值语句以及static语句块合并产生的。在编译器生成类的初始化函数clini()时,首先生成的是父类的初始化函数。

 直接引用:

  • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
  • 通过反射方式执行以上三种行为。
  • 初始化子类的时候,会触发父类的初始化。
  • 作为程序入口直接运行时(也就是直接调用main方法)。

在被动引用中如下

Public class Parent{

Static{

System.out.println(“parent init”);

}

Public static int v = 100;

}

Public calss Child extends Parent{

Static{

System.out.println(“Child init”):

}

}

Public class UseParent{

Public static void main(String[] args){

System.out.println(Child.v);

}

}

在main函数中通过子类访问父类字段,输出结果是 Parent Init100,可以看到只有父类被初始化了,子类虽然没有被初始化但是已经被加载了。引用一个字段时只有直接地定义这个字段的类才能被初始化。另外如果引用一个类中的final常量时,该类不会被加载。因为final常量被存放在常量池中。

类的初始化顺序:按照顺序自上而下运行类中的变量赋值语句和静态变量语句,首先按照顺序初始化父类中的变量赋值语句和静态变量语句。

2.ClassLoader类装载器

 

它主要工作在Class装载的加载阶段,主要作用是从系统外部获得Class二进制数据流。

在标准的java程序中,java虚拟机会创建3类classloader为每个应用程序服务,BootStrap ClassLoader(启动器加载类)Extendion ClassLoader(扩展类加载器)和APPClassLoader(应用类加载器,也成为系统类加载器)。另外,每一个应用程序还可以拥有自定义ClssLoader。

 

五、分析java堆

1.内存溢出的原因

① 堆溢出,

原因是大量的对象分配在堆上,占据了堆空间,这些对象都持有强引用,导致无法回收。使用-Xmx参数分配更大的对空间或者优化对象。

② 直接内存溢出,

直接内存中存放一些需要频繁访问的可复用的空闲,原因是没哟被Java虚拟机完全托管,解决办法是保证FUll GC的合理进行。

③ 线程过多溢出,

原因是java进程已经达到了可使用的内存上限。解决办法是减少堆空间,这样系统会预留更多的内存用于线程创建。或者可以减少每一个线程所占的内存空间,使用-XSS参数制定线程所占的栈空间

④ 永久区溢出,

永久区是存放类元数据的区域,JDK1.8中永久区被一块元空间替代,原因是系统不断地产生新类,而没有回收。解决办法是增加MaxPermSize或者说是MaxMeteSpaceSize的值,减少系统需要的类的数量和使用ClassLoader合理地装载各个类,并定期回收。

⑤ GC效率低下引起的OOM,

系统的堆空间太小,并且回收所释放的内存就会较少。根据GC所占用系统的时间和所释放的内存大小来评定GC的效率。一旦GC的效率很低,虚拟机就会抛出OOM异常。

2.String在虚拟机中的实现

特点:

① 不变性,是在多线程的访问中,保持对象的不变性的话就不需要实现同步,省略了同步和锁的等待时间。提高了多线程的访问性能,堆所有线程都是只读的。它的修改操作都是创建新的对象来实现的。

② 针对常量池的优化,是指两个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

③ 类的final定义,final类型的对象在系统中不可能有任何子类,这对系统的安全性保护,

String的内存泄漏:

String的内存泄漏在JDK1.7之前,其中的一个SubString()函数在截取字符串时,是直接调整偏离值off和长度count,通过直接对原来Value的引用实现新的字符串。但是当垃圾回收器对原有数据进行回收后,除了被引用的字符外其他字符依然占用内存而没有被清理,造成了内存泄漏。解决办法是通过在JDK1.7中对字符串中的长度进行value的实际length进行计算,并且在SUB新的字符串时,直接创建一个新的字符串而不是使用引用。

String常量池的位置变化:

JDK1.6中还是在永久区,在JDK1.7中就在堆空间中了。

 

 

 

 

原文地址:https://www.cnblogs.com/jinb/p/6287854.html