Java虚拟机

Java内存区域

JVM的主要组成部分及作用

在这里插入图片描述
JVM主要由四个部分组成:

1. 类加载器(ClassLoader)
2. 运行时数据区(Runtime Data Area)
3. 执行引擎(Execution Engine)
4. 本地库接口(Native Interface)

各组件的作用:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

Java程序运行机制步骤

在这里插入图片描述

  • 首先利用IDE集成开发工具编写Java源代码,源文件的后缀名为.java
  • 编译器(javac)将源代码编译成字节码文件,后缀名.class
  • 解释器(java命令)运行字节码

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。

JVM运行时数据区

在这里插入图片描述

  • 程序计数器:字节码解释器工作就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成;
  • Java虚拟机栈:生命周期和线程一致,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直到执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程;
  • 本地方法栈:与Java虚拟机栈不同的是,Java虚拟机栈是为执行Java方法(字节码)服务,而本地方法栈是为虚拟机使用到的本地方法服务。
    Java虚拟机栈和本地方法栈都有可能抛出StackOverFlowError【线程请求的栈深度大于虚拟机所允许的深度】和OutOfMemoryError【如果虚拟机可以动态扩展,而扩展时无法申请到足够的内存】。
  • Java堆:主要用来存放对象实例和数组,垃圾回收的主要区域;
  • 方法区:用来存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
  • 运行时常量池:方法区的一部分,编译器生成的字面量和符号引用会在类加载后放入这个区域。

堆栈的区别

  • 可见度:堆线程共享,栈线程私有;
  • 存储内容:堆中主要存放对象实例,数组,栈中主要存放基本数据类型,对象的引用;
  • 作用:栈主要解决程序的运行问题,堆主要解决的是数据的存储问题;
  • 内存分配:堆是不连续的,分配的内存是在运行期确定的,大小不固定,栈是连续的,分配的内存大小要在编译期确定,大小固定。

HotSpot虚拟机对象探秘

对象创建的几种方式

  • 使用new关键字
  • 使用Class的newInstance方法
  • 使用Constructor类的newInstance方法
  • 使用clone方法
  • 使用反序列化

对象创建的主要流程

虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载,类加载通过后,接下来分配内存,若Java堆中内存是绝对规整的,使用“指针碰撞”方式分配内存;如果不是规整的,就从空闲列表中分配,叫做“空闲列表”方式。划分内存时还需要考虑一个并发问题,也有两种方式:CAS同步处理或者本地线程分配缓冲,然后内存空间初始化操作,接着是做一些必要的对象设置,最后执行<init>方法。

在这里插入图片描述
在这里插入图片描述

对象的访问定位

Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄直接指针 两种方式。

指针: 指向对象,代表一个对象在内存中的起始地址。
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。

句柄访问
Java堆中划分出一块内存来作为句柄池,Java栈的局部变量表中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
在这里插入图片描述
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

直接指针
如果使用直接指针访问,引用中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。

内存溢出异常

Java会存在内存泄漏吗?

内存泄漏是指不再被使用的对象或者变量一直被占据在内存中,虽然Java由GC垃圾回收机制,不再被使用的对象会被GC自动回收,但是还是存在内存泄漏问题,比如:

  • 长生命周期的对象持有短生命周期对象的引用,尽管短生命周期已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。
  • 监听器:释放对象的时候没有删除监听器;
  • 各种连接:比如数据库连接(dataSourse.getConnection()),网络连接(socket) 和 IO 连接,除非其显式的调用了其 close() 方法将其连接关闭,否则是不会自动被 GC 回收的;

垃圾收集器

Java中都有哪些引用类型

  • 强引用:发生GC的时候不会回收
  • 软引用:在发生内存溢出之前会被回收
  • 弱引用:在下一次GC时会被回收
  • 虚引用:又称幽灵引用,无法通过虚引用来获得对象,主要用于在GC时返回一个通知

怎么判断对象是否可以被回收

  • 引用计数器法:缺点是不能解决循环引用问题
  • 可达性分析:从GC Roots开始向下搜素,搜素所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是可以被回收的。

JVM的永久代中会发生垃圾回收吗?

垃圾回收不会发生在永久代,如果永久代满了或是超过了临界值,会触发完全垃圾回收(Full GC),另外,Java8中已经移除了永久代。

JVM的垃圾回收算法

  • 标记-清除算法:标记无用对象,然后进行清除回收
    优点:实现简单,不需要对象进行移动;
    缺点:标记、清除过程效率低,会产生大量不连续的内存碎片;
  • 复制算法:把内存空间划分为两个相等的区域,每次只使用其中一个区域,垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用区域的可回收对象进行回收。
    优点:按顺序分配内存即可,实现简单,运行高效,不用考虑内存碎片;
    缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制;
  • 标记-整理算法:在标记可回收的对象后将所有存活的对象压缩到内存的一端,使它们紧凑地排列在一起,然后对端边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。
    优点:解决了标记-清理算法存在的内存碎片问题
    缺点:仍需要进行局部对象移动,一定程度上降低了效率
  • 分代收集算法:针对不同情况采用不同的垃圾回收算法。

分代收集下的年轻代和老年代采用的垃圾回收算法

新生代:主要以复制算法为主

  • 年轻代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0、Survivor1)区,大部分对象在Eden区中生成,回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存满了时,则将Eden区和Survivor0区存活的对象复制到另一个Survivor1区,然后清空Eden区和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空,如此往复。
  • 当Survivor1区不足以存放Eden区和Survivor0区存活的对象时,则将存活对象直接放到老年代,若是老年代也满了,就会触发一次Full GC(Major GC),年轻代和老年代都进行回收。
  • 年轻代发生的GC叫做Minor GC,Minor GC发生的频率比较高(不一定等Eden区满了才触发)
  • 每次从Survivor0到Survivor1移动存活的对象,年龄就加1,当年龄到达15时(默认值),升级为老年代;
  • 大对象直接进入老年代
    老年代:主要以标记整理为主
  • 在年轻代中经历了N次垃圾回收仍然存活的对象就会被放到老年代中,因此,可以认为老年代中存放的都是一些生命周期比较长的对象;
  • 老年代内存比年轻代内存大很多,当老年代内存满时会触发Major GC(Full GC)。

JVM常见的垃圾回收器

在这里插入图片描述

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

详细介绍一下CMS垃圾回收器

CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。

CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。

虚拟机类加载机制

Java类加载机制

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
注意:类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类,如果一次性加载所有类,那么会占用很大的内存。

类的加载方式有两种:

  1. 隐式加载,程序在运行过程中遇到new等方式生成对象时,隐式调用类加载器来加载对应的类到JVM中;
  2. 显式加载:通过class.forName()等方法显式加载需要加载的类。

类的生命周期(前5个步骤是类加载过程)

在这里插入图片描述

  1. 加载:
    完成以下三件事:

    • 通过类的完全限定名称获取定义该类的二进制字节流;
    • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构;
    • 在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口;

    获取二进制字节流有以下几种方式:

    • 从ZIP包读取,常见的有:JAR,WAR
    • 从网络中获取,如:Applet
    • 运行时计算生成,如:动态代理技术,使用ProxyGenerator.generateProxyClass的代理类的二进制字节流
  2. 验证:
    确保Class文件的字节流中包含的信息是符合当前虚拟机的要求,不会危害虚拟机自身的安全

  3. 准备:
    为类变量分配内存并设置初始值,使用的是方法区的内存。
    注意:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。(单例实现,为什么非静态方法中不能引用静态方法,等等)

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

  5. 初始化:
    初始化阶段才是真正开始执行类中定义的Java程序代码,初始化阶段是虚拟机执行类构造器方法的过程,并且根据程序去初始化类变量和其他资源。
    注意的是:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。

    public class Test {
    	static {
    		i = 0; // 给变量赋值可以正常编译通过
    		System.out.print(i); // 这句编译器会提示“非法向前引用”
    	} 
    	static int i = 1;
    }
    
  6. 使用:

  7. 卸载:

什么是类加载器,类加载器有哪些?

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

主要有以下四种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
  4. 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。

双亲委派模型

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定在JVM中的唯一性,每个类加载器都有一个独立的类名称空间,类加载器就是根据指定的全限定名称将class文件加载到JVM内存,然后再转化为java.lang.Class对象。
在这里插入图片描述
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成(注意:这里的父子关系一般是通过组合关系来实现的,而不是继承实现的),每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

JVM调优

说一下 JVM 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。

jconsole:用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。

常用的 JVM 调优的参数都有哪些?

-Xms2g:初始化推大小为 2g;
-Xmx2g:堆最大内存为 2g;
-XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
-XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
–XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
-XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
-XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
-XX:+PrintGC:开启打印 gc 信息;
-XX:+PrintGCDetails:打印 gc 详细信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值