JVM最完整最深入解析

Java运行时数据区

  1. 程序计数器:指向当前线程正在执行的字节码指令。线程私有的。
  2. 虚拟机站:虚拟机站是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压入栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。

         (1) 栈帧:栈帧存储方法的相关信息,包含局部变量表、返回值、操作数栈、动态链接

         a) 局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不  会改变局部变量数组的大小。

         b) 返回值:如果有返回值的话,压入调用者栈帧的操作数栈中,并且把PC的值指向方法调用指令后面的一条指令地址

         c) 操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks顶中)。操作数栈的元素可以是任意java类型,包括long和double,32位数据占用栈空间为1,64位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。

        d) 动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。

      (2)线程私有

    3. 本地方法栈

       1)调用本地native的内存模型

       2)线程独享

     4. 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据

    (1)线程共享的
    (2)运行时常量池: a) 是方法区的一部分。b)存放编译期生成的各种字面量和符号引用。c)Class文件中除了存有类的版本、字段、方法、接口等描述信息,还有一项是常量池,存有这个类的编译期生成的各种字面量和符号引用,这部分内容在类加载后,存放到方法区的运行时常量池中。

     5. 堆(Heap):Java对象存储的地方

      1)Java堆是虚拟机管理的内存中最大的一块

      2)Java堆是所有线程共享的区域

      3)在虚拟机启动时创建

      4)此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组

      5)Java堆是垃圾收集器管理的内存区域,很多时候成为GC堆

JMM Java内存模型

1. Java并发采用“共享内存”模型,线程之间通过读写内存的公共状态进行通讯。

2. 主要目的是定义程序中各个变量的访问规则

3. Java内存模型规定所有变量都存储在主内存中,每个线程还有自己的工作内存。

  1)线程的工作内存中保存了被该线程使用到的变量的拷贝(从主内存中拷贝过来),线程对变量的所有操作都必须在工作内存中执行,而不能直接访问主内存中的变量。

  2)不同线程之间无法访问对方工作内存的变量,线程间变量值的传递都要通过主内存来完成的。

  3)主内存主要对应Java堆中实例数据部分。工作内存对英语虚拟机站中部分区域。

  

  3)每个线程有一个私有的本地内存,里面存储了读/谢共享变量的副本。

4. Java线程之间的通信由内存模型JMM(Java Memory Model)控制。

  1)JMM决定一个线程对变量的写入何时对另一个线程可见

  2)线程之间共享变量存储在主内存中

  3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本

  4)JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证

堆的内存划分

Java堆的内存划分分别为年轻代、Old Memory(老年代)、Perm(永久代),在1.8中,永久代被移除,使用MetaSpace代替

1、新生代

  1. 使用复制算法(Coping算法),因为年轻代每次GC都要回收大部分对象。新生代里分成一份较大的Eden空间和两份较小的Survivor空间。每次使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor空间中,清空Eden和刚使用过的Survivor空间
  2. 分为Eden、Survivor From、Survivor To,比例默认为8:1:1
  3. 内存不足时发生Minor GC

2、老年代

  1. 采用标记-整理算法(mark-compact),原因是老年代每次GC只会回收少部分对象

3、永久代

    用来存储类的元数据,也就是方法区

  1. Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进场内存不够用,或者发生内存泄漏
  2. MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

GC垃圾回收

一、判断对象是否要回收的方法:可达性分析法

1、可达性分析法:

      通过一系列“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不一定会成为可回收对象。进入DEAD状态的线程还可以恢复,GC不会回收它的内存。(把一些对象当做root对象,JVM认为root对象是不可回收的,并且root对象引用的对象也是不可回收的)

2、以下对象会被认为是root对象:

  1. 虚拟机栈(栈帧中本地变量表)中引用的对象
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中Native方法引用的对象

3、对象被判定可被回收,需要经历两个阶段:

  1. 第一个阶段是可达性分析,分析该对象是否可达
  2. 第二个阶段是当对象没有重写finalize()方法或者finalize()方法已经被调用过,虚拟机认为该对象不可以被救活,因此回收该对象。(finalize()方法在垃圾回收中的作用是,给该对象一次救活的机会)

4、方法去中的垃圾回收:

  1. 常量池中一些常量、符号引用没有被引用,则会被清理出常量池
  2. 无用的类:被判定为无用的类,会被清理出方法区。判定方法如下:a) 该类的所有实例被回收。b) 加载该类的ClassLoader被回收。c) 该类的class对象没有被引用。

5、finalize():

  1. GC垃圾回收要回收一个对象的时候,调用该对象的finalize()方法。然后在下一次垃圾回收的时候,才去回收这个对象的内存,
  2. 可以在该方法里面,制定一些对象在释放前必须执行的操作。

二、发现虚拟机频繁的full GC时应该咋办:

    full GC指的是清理整个堆空间,包括年轻代和永久代

  1. 首先用命令查看触发GC的原因是什么jstat -gccause进程id
  2. 如果是System.gc(),则看下哪里调用了
  3. 如果是heap inspection(内存检查),可能是哪里执行jmap -histo[:live] 命令
  4. 如果是GC locker,可能是程序依赖的JNI库的原因

三、常见的垃圾回收算法:

1、Mark-Sweep(标记-清除算法):

  1. 思想:标记-清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。
  2. 优缺点:实现简单,容易产生内存碎片

2、Copying(复制清除算法):

  1. 思想:将可用内存划分为大小相等的两块,每次只使用其中一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉
  2. 优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低

3、Mark-Compact(标记-整理算法):

  1. 思想:先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存
  2. 优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下

4、分代手机算法(大部分JVM的垃圾收集器所采用的算法):

  1. 因为新生代每次垃圾回收都要回收大部分对象,所以新生代采用Copying算法。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象放到未使用的Survivor(划分出from、to)空间中,清空Eden和刚才使用过的Survivor空间
  2. 由于老年代每次只回收少量的对象,因此采用mark-compact算法
  3. 在堆区外有一个永久代。对永久代的回收主要是无效的类和常量

5、GC使用时对程序的影响?

    垃圾回收会影响程序的性能,Java虚拟机必须要追踪运行程序中的有用对象,然后释放没用对象,这个过程消耗处理器时间

6、几种不同的垃圾回收类型:

  1. Minor GC:从年轻代(包括Eden、Survivor区)回收内存
  2. Major GC:清理整个老年代,当eden区内存不足时触发
  3. Full GC:清理整个堆空间,包括年轻代和老年代。当老年代内存不足时触发

JVM优化

1、一般来说,当survivor区不够大或者占用量达到50%,就会把一些对象放到老年区。通过设置合理的eden区,survivor区及使用率,可以将年轻对象保存在年轻代,从而避免full GC,使用-Xmn设置年轻代的大小。

2、对于占用内存比较多的大对象,一般会选择在老年代分配内存。如果在年轻代给大对象分配内存,年轻代内存不够了,就要在eden区移动大量对象到老年代,然后这些移动的对象可能很快消亡,因此导致full GC。通过设置参数:-XX:PetenureSizeThreshold=1000000,单位为B,标明对象大小超过1M时,在老年代(tenured)分配内存空间。

3、一般情况下,年轻对象放在eden区,当第一次GC后,如果对象还存活,放到survivor区,此后,每GC一次,年龄增加1,当对象的年龄达到阈值,就被放到tenured老年区。这个阈值可以同构-XX:MaxTenuringThreshold设置。如果想让对象留在年轻代,可以设置比较大的阈值。

4、设置最小堆和最大堆:-Xmx-Xms稳定的堆大小堆垃圾回收是有利的,获得一个稳定的堆大小的方法是设置-Xms和-Xmx的值一样,即最大堆和最小堆一样,如果这样子设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间可以减少GC次数,因此,很多服务端都会将这两个参数设置为一样的数值。稳定的堆大小虽然减少GC次数,但是增加每次GC的时间,因为每次GC要把堆的大小维持在一个区间内。

5、一个不稳定的堆并非毫无用处。在系统不需要使用大内存的时候,压缩堆空间,使得GC每次应对一个较小的堆空间,加快单次GC次数。基于这种考虑,JVM提供两个参数,用于压缩和扩展堆空间。

  1. -XX:MinHeapFreeRatio 参数用于设置堆空间的最小空闲比率。默认值是40,当堆空间的空闲内存比率小于40,JVM便会扩展堆空间
  2. -XX:MaxHeapFreeRatio 参数用于设置堆空间的最大空闲比率。默认值是70, 当堆空间的空闲内存比率大于70,JVM便会压缩堆空间
  3. 当-Xmx和-Xmx相等时,上面两个参数无效

6、通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器

  1. -XX:+UseParallelGC:年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器,可以尽可能的减少垃圾回收时间
  2. -XX:+UseParallelOldGC:设置老年代使用并行垃圾回收收集器

7、尝试使用大的内存分页:使用大的内存分页增加CPU的内存寻址能力,从而系统的性能。-XX:+LargePageSizeInBytes 设置内存页的大小

8、使用非占用的垃圾收集器。-XX:+UseConcMarkSweepGC老年代使用CMS收集器降低停顿

9、-XXSurvivorRatio=3,表示年轻代中的分配比率:survivor:eden = 2:3

10、JVM性能调优的工具:

  1. jps(Java Process Status):输出JVM中运行的进程状态信息(现在一般使用jconsole)
  2. jstack:查看java进程内线程的堆栈信息
  3. jmap:用于生成堆转存快照
  4. jhat:用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer)
  5. jstat是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等
  6. VisualVM:故障处理工具

类加载机制:

一、概念

    类加载器把class文件中的二进制数据读入到内存中,存放在方法区,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类加载的步骤如下:

  1. 加载:查找并加载类的二进制数据(把class文件里面的信息加载到内存里面)
  2. 连接:把内存中类的二进制数据合并到虚拟机的运行时环境中

       (1) 验证:确保被加载的类的正确性,包括:

   A、类文件的结构检查:检查是否满足Java类文件的固定格式
   B、语义检查:确保类本身符合Java的语法规范
   C、字节码验证:确保字节码流可以被Java虚拟机安全的执行。字节码流是操作码组成的序列。每一个操作码后面都会跟着一个或者多个操作数。字节码检查这个步骤会检查每一个操作码是否合法。
   D、二进制兼容性验证:确保相互引用的类之间是协调一致的

      (2) 准备:为类的静态变量分配内存,并将其初始化为默认值

      (3) 解析:把类中的符号引用转化为直接引用(比如说方法的符号引用,是有方法名和相关描述符组成,在解析阶段,JVM把符号引用替换成一个指针,这个指针就是直接引用,它指向该类的该方法在方法区中的内存位置)

    3. 初始化:为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用static代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用static初始化。

二、双亲委派模型

1、当一个类加载起收到类加载请求的时候,他首先不会自己去加载这个类的信息,而是把该请求转发给父类加载器,依次向上。所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中无法加载到所需的类,子类加载器才会自己尝试去加载该类。当当前类加载器和所有父类加载器都无法加载该类时,抛出ClassNotFindException异常。

2、意义:

    提高系统的安全性。用户自定义的类加载器不可能加载应该由父加载器加载的可靠类。(比如用户定义了一个恶意代码,自定义的类加载器首先让系统加载器去加载,系统加载器检查该代码不符合规范,于是就不继续加载了)

三、特点

  1. 全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。
  2. 缓存机制:所有的Class对象都会被缓存,当程序需要使用某个Class时,类加载器先从缓存中查找,找不到,才从class文件中读取数据,转化成Class对象,存入缓存中

四、类加载器

    两种类型的类加载器:

1、JVM自带的类加载器(3种):

    (1) 根类加载起(Bootstrap):

        a) C++编写的,程序员无法在程序中获取该类

        b) 负责加载虚拟机的核心库,比如java.lang.Object

        c)没有继承ClassLoader类

    (2) 扩展类加载器(Extension):

        a) Java编写的,从指定目录中加载类库

        b) 父加载器是根类加载器

        c) 是ClassLoader的子类

        d) 如果用户把创建jar文件放到指定目录中,也会被扩展加载器加载

    (3) 系统加载器(System)或者应用加载器(App):

        a) Java编写的
        b) 父加载器是扩展类加载器
        c) 从环境变量或者class.path中加载类
        d) 是用户自定义类加载的默认父加载器
        e) 是ClassLoader的子类

2、用户自定义的类加载器

(1)Java.lang.ClassLoader类的子类
(2)用户可以定制类的加载方式
(3)父类加载器是系统加载器
(4)编写步骤:
A、继承ClassLoader
B、重写findClass方法。从特定位置加载class文件,得到字节数组,然后利用defineClass把字节数组转化为Class对象
(5)为什么要自定义类加载器? 
A、可以从指定位置加载class文件,比如说从数据库、云端加载class文件
B、加密:Java代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用Java自带的ClassLoader来加载这个类了,需要自定义ClassLoader,对这个类进行解密,然后加载。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值