JVM简单总结

1 JVM运行时数据区

Java 虚拟机在执行 Java 程序的过程中(java进程)会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。

JDK 1.8之前:

JDK 1.8 :

 

1.1 线程独享

①虚拟机栈:描述的是java方法执行的动态内存模型,里面有一个个栈帧组成:

栈帧:每个方法执行都会为其创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等。 局部变量表:局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

局部变量表用slot存储数据(一般数据类型使用一个slot,long和double则占用两个slot),为了尽可能节省栈空间,slot是可以复用的(当一个变量的pc寄存器的值大于slot的作用域的时候,slot是可以复用的)。slot复用详情

异常:StackOverFlowError、OutOfMemoryError

②本地方法栈:为虚拟机执行native方法服务(与虚拟机栈类似)

③程序计数器:小的内存空间。可以看作当前线程所执行代码的行号。如果线程执行java方法,这个计数器记录的是正在执行虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器的值为undefined。没有规定任何溢出的异常。(JVM中唯一没有规定异常的区域)

1.2 线程共享

①堆内存:存放对象实例,垃圾收集器管理的主要区域、新生代、老年代(jdk1.7之前方法区称为永久代(hotspot),也在堆内存中)

方法区:jdk1.7方法区存储信息:jvm加载的类信息(类的版本,字段,方法,接口),常量(存储在常量池中),静态变量,即使编译后的代码数据,会存在垃圾回收(方法区回收详细信息)。

异常:OutOfMemoryError

常量池(运行时常量池):字符串创建会在常量池中。new对象则会在堆中创建。(.intern()产生运行时常量)

1.2.1 对象的创建过程

 

1.分配对象

①指针碰撞:堆内存的对象地址是规整的,类似于链表那样一个一个指针碰撞似的探索,直到有一个空闲的区域分配。

②空闲列表:堆内存是不规整的,直接查表找到空闲区地址分配对象。

2.线程安全性问题

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

①本地线程分配缓冲(TLAB)

1,堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的。
2,Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配。
3,TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
4,所有新创建的Object 都将会存储在新生代Yong Generation中。
   如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到OldGeneration。   新的Object总是创建在Eden Space。

②线程同步(加锁,CAS等)

CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。

3.对象的结构

①header(对象头):

markword自身运行时数据,哈希值,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,类型指针(对象指向类的指针)

②InstanceData

③Padding:对象大小为八字节整数倍,当未满足时则用来填充。

4,对象访问定位

①使用句柄:栈中对象引用指向堆中句柄池,句柄池指向对象。

②直接指针(hotspot):直接指向堆中对象

1.2.2 jdk1.7与1.8区别

jdk1.7及以往的版本中,Java类信息、常量池、静态变量都存储在Perm(永久代)也就是方法区里面。

jdk1.8,则删除了方法区,直接使用本地内存存放Java类信息,把这个区域称作元数据区(MetaSpace,元空间)。常量池、静态变量则直接存储在堆中。

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。   -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:   -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集   -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

元空间的另一个优势

HotSpot的永久代:原来的jar包及你自己项目的class存放的内存空间,这部分空间是固定的,启动参数里面permSize确定,如果你的jar包很多,经常会遇到永久代溢出,且每个项目都会占用自己的permGen空间。 改成用元空间后,各个项目会共享同样的class内存空间,比如两个项目都用了fast-json开源包,在元空间里面只存一份class,提高内存利用率,且更利于垃圾回收。

提高GC的性能

如果你理解了元空间的概念,很容易发现GC的性能得到了提升。

  • Full GC中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了。

  • 元空间只有少量的指针指向Java堆。这包括:类的元数据中指向java/lang/Class实例的指针;数组类的元数据中,指向java/lang/Class集合的指针。

  • 没有元数据压缩的开销

  • 减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)

  • 减少了Full GC的时间

  • G1回收器中,并发标记阶段完成后可以进行类的卸载

1.3 垃圾回收

1.3.1 垃圾回收对象的判定

1.引用计数法:对象中添加引用计数器,有变量指向它,值+1。引用失效时,值-1。当值为0时,成为垃圾对象。

2.可达性分析算法:从GCRoot对象出发,没有任何引用链所指向即为垃圾。

GCRoot:虚拟机栈(局部变量表)的对象;方法区的类属性所引用对象;方法区中常量所引用对象;本地方法栈中锁引用的对象。

1.3.2 垃圾回收算法

1.回收策略

①标记——清除算法

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

②复制算法

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

③标记——整理算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。

④分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

 

年轻代(Young Generation)的回收算法

a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。

d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。

年老代(Old Generation)的回收算法

a) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

b) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

2.垃圾收集器

JVM年轻代与老年代分别用自己的收集器,搭配使用,达到分代收集性能的最好效果。(下图为垃圾收集器搭配图)

  • Serial收集器(复制算法) 新生代单线程收集器,标记和清理都是单线程,优点是简单高效。是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定。

  • Serial Old收集器(标记-整理算法) 老年代单线程收集器,Serial收集器的老年代版本。

  • ParNew收集器(停止-复制算法)  新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

  • Parallel Scavenge收集器(停止-复制算法) 并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数。

  • Parallel Old收集器(停止-复制算法) Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先。

  • CMS(Concurrent Mark Sweep)收集器(标记-清理算法) 高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择。

  • G1

1.3.3 GC是什么时候触发的(面试最常见的问题之一)

  由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

  对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:

a) 年老代(Tenured)被写满;

b) 持久代(Perm)被写满;

c) System.gc()被显示调用;

d) 上一次GC之后Heap的各域分配策略动态变化;

1.3.4 堆的内存分配策略

①优先分配到Eden区

②大对象直接分配到老年代——配置大对象阈值——-XX:PertenureSizeThresgold=1M

③长期存在的对象分配到老年代——配置对象进入老年代的年龄——XX:MaxTenuringThreshold=。。。

④空间分配担保(空间分配担保详情)——禁用空间分配担保——XX:+HandlePromotionFailure

⑤动态对象年龄判断

一些关于堆的参数配置

Xmx72G:设置JVM最大可用内存为72G

Xms72G:设置JVM初始内存为72G

Xmn4G:设置年轻代大小为4G

-XX:NewRatio=4:设置年轻代与老年代大小比值1:4

-XX:SurvivorRatio=4:设置年轻代中Eden与两个Survivor区(Survivor0,Survivor1)比值:4:1:1

2 类加载机制

2.1 概念

JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

2.2 加载过程

2.2.1 加载

①通过一个类的全限定名来获取定义此类的二进制流(文件、网络io等)。

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

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

类加载器

类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制流”这个动作在JVM外部实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为类加载器。

注:只有被同一个类加载器加载的类才可能会相等。相同的字节码被不同的类加载器加载的类不相等。

类加载器分类,这里简单介绍了类加载器的类型及作用,详情可以查看这篇博客,说的很全面:https://www.cnblogs.com/developer-ios/p/5550789.html

启动类加载器:用于加载JAVAHOME的lib下的类

? ?

扩展类加载器:加载JAVAHOME的lib/ext目录中的类

? ?

应用程序类加载器:加载用户类路径上的所指定的类库

? ?

自定义加载器

双亲委派原则:先尽量让父类加载,父类无法加载则抛出异常,子类捕获异常去加载。

2.2.2 连接

①验证:确保Class文件字节流的信息符合当前虚拟机的要求,并且不会危害虚拟机安全

②准备:正式为类变量分配内存并设置变量的初始值(final:指定值;其他:默认值)。这些变量使用的内存都将在方法区中进行分配。

③解析:将常量池中的符号引用替换为直接引用的过程。

方法调用

①方法调用

JVM在执行一个方法的时候,它是如何找到这个方法的?

Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。另一部分将在每次运行期间转化为直接引用,称为动态连接(动态分派)。栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区:虚拟机栈(不同于堆、方法区)中的内容,栈帧存储了方法的局部变量表、操作数栈、动态连接、和方法返回地址等信息。这里所说的动态连接,就是:一个指向运行时常量池中该栈帧所属方法的引用,虚拟机就是根据这个信息知道要调用哪个具体的方法

概念:唯一的任务时确定被调用方法的版本(不等同于方法执行)

②解析调用

概念:写完后方法不会有版本的改变(不能被重写或者重载),所以可以直接解析。

静态方法、构造方法、私有方法、final修饰的方法等。

③ 分派调用

静态分派:方法重载

动态分派:方法重写(@override):从下往上(子类->父类)找方法,有则返回,无则抛出异常

2.2.4 初始化

①被初始化的对象:new、static 、反射获取的类、main方法的类

②不被初始化的对象:子类引用父类静态字段或方法,子类不会被初始化

通过数组定义的引用类

调用类的常量(例:static final int a = 10;)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值