java虚拟机简单入门

本文是关于Java虚拟机(JVM)的简单入门,旨在帮助读者理解和学习JVM的重要概念,包括内存结构(如堆、栈、方法区等)、类加载器、沙箱安全机制和native关键字等。文章还提到了JVM调优、双亲委派机制、安全管理和内存溢出(OOM)问题,是面试和深入学习Java的宝贵资料。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java虚拟机概述

首先问自己个问题:为什么学虚拟机?

对我来说,我目前这个阶段:

  1. 第一肯定是为了面试!
  2. 其次也是为了更加深入的了解Java这门语言
  3. 当然也能为了今后的工作打下基础,能够对jvm调优
  4. 能够从不同的方面去看待我所编写出来的程序
  5. 多了解一些知识也不是坏处

清楚了目的才能更有效的坚持学习下去,不会迷茫,不会半途而废。

1 jvm内存结构

image-20210217234053929

附加:

  • 虚拟机调优主要都是在堆中调优
  • 垃圾收集器只需要收集方法区和堆中的垃圾,栈和pc中是不需要进行内存回收的

2 类加载器

作用:加载class文件

机制:双亲委派机制

app–>EXC---->BOOT(最终执行)

  1. 类加载器收到类加载请求 Application
  2. 将这个请求向上委托给父类加载器完成,一直向上委托,知道启动类加载器Bootstrap类
  3. 启动类加载器会检查是否能够加载当前这个类能加载就结束,使用当前加载器,否则抛出异常,通知子类加载器进行加载
  4. 重复步骤3

image-20210218002039037

面试问题

  • 为什么需要双亲委派机制?(也就是双亲委派的优点)

    ①双亲委派机制使得类加载出现层级,父类加载器加载过的类,子类加载器不会重复加载,可以防止类重复加载

    ②使得类的加载出现优先级,防止了核心API被篡改,提升了安全,所以越基础的类就会越上层进行加载,反而一般自己的写的类,就会在应用程序加载器(Application)直接加载。

  • 如何打破双亲委派?

    ①自定义类加载器,重写loadClass方法

    ②使用线程上下文类加载器

3 沙箱安全机制

1、什么是沙箱

沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。

2、组成沙箱的基本组件

  • 字节码校验器(bytecode verifier):确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。

  • 类装载器(class loader):其中类装载器在3个方面对Java沙箱起作用

    • 它防止恶意代码去干涉善意的代码;
    • 它守护了被信任的类库边界;
  • 它将代码归入保护域,确定了代码可以进行哪些操作。

虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。

类装载器采用的机制是双亲委派模式

  1. 从最内层JVM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
  2. 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
  • 存取控制器(access controller):存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

  • 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。

  • 安全软件包(security package):java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:

    • 安全提供者
    • 消息摘要
  • 数字签名

    • 加密
  • 鉴别

4 native关键字

举例:object中的native:

凡是带了native关键字的方法,说明java的作用范围达不到了,会去调用底层c语言的库

首先会进入本地方法栈,然后再调用本地方法接口JNI,调用本地方法库

JNI作用:扩展java的使用,融合不同的编程语言为java所用 最初:c,c++

它在内存区域中专门开辟了一块标记区域,Native Method Stack,登记Native方法

最终执行的时候,就会通过JNI加载本地方法库中的方法

5 PC寄存器

程序计数器:Program Counter Register

每个线程都有一个程序计数器, 是线程私有的,就是一个指针, 指向方法区中的方法字节码(用来存储指向像一条指令的地址, 也即将要执行的指令代码),在执行引擎读取下一条指令, 是一个非常小的内存空间,几乎可以忽略不计

1.使用PC寄存器存储字节码指令地址有什么用呢(为什么使用PC寄存器记录当前线程的执行地址呢)

(1)多线程宏观上是并行(多个事件在同一时刻同时发生)的,但实际上是并发交替执行的

(2)因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行

(3)JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

2.PC寄存器为什么会设定为线程私有?

(1)我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停滴做任务切换,这样必然会导致经常中断或恢复,如何保证分毫无差呢?

(2)为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

6 方法区

方法区:Method Area

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关。简单的来说就是:static、final、Class、常量池

java8在元空间中实现方法区的内容

元空间在1.8中不在与堆是连续的物理内存,而是改为使用本地内存(Native memory)。元空间使用本地内存也就意味着只要本地内存足够,就不会出现OOM的错误。java.lang.OutOfMemoryError:内存用完了

7 栈

  • 程序 = 数据结构 + 算法 【持续学习】
  • 大多数人程序 = 框架 + 业务逻辑【饭碗】

1、栈里面存放什么

栈:8大基本类型 + 对象的引用 + 实例的方法

2、栈运行原理

栈帧

image-20210201182918164

栈满了:StackOverflowError

三种JVM:

  • Sun公司 HotSpot Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
  • BEA JRockit
  • IBM J9VM

8 堆

1、堆的内存模型

Heap:一个JVM只有一个堆内存,堆内存的大小是可以调节的。

类加载器读取了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量,保存我们所有类型的真实对象

堆内存中还要细分为三个区域:

  • 新生代
  • 老年代
  • 永久代

GC垃圾回收,主要是在新生代和老年代

假设内存满了,会OOM,堆内存不够!java.lang.OutOfMemoryError: Java heap space

JDK8之后,元空间替代了永久代,它是对JVM规范中方法区的实现,区别在于元数据区不在虚拟机当中,而是用的本地内存,永久代在虚拟机当中,永久代逻辑结构上也属于堆,但是物理上不属于。

内存模型如下:(叫区或代都可以,能理解就行)

image-20210218211919635

2、新生代与老年代

新生代:

  • 类诞生和成长的地方,甚至死亡;

  • 新生代还能细分为伊甸园区、幸存者0区和幸存者1区(暂且叫01,GC篇详细解释)

  • 伊甸园区:所有对象都在这个区中new出来的,当对象new满这个区之后会触发一次轻GC,存活的对象将会放入幸存者0或1区

当新生代的伊甸园区和幸存0或1区满了就会触发一次GC(GC具体机制后续再介绍),将新生区存活时间足够长的对象(用被GC的次数来判断,默认为15次,也就是被GC15次之后还没死的对象就会转移)以及其他GC机制判断的某些对象放入老年区。

真理:99%的对象都是临时对象!正常能进老年代的不多。

3、永久代

这个区域是常驻内存的,用来存放JDK自身携带的class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭JVM的时候就会释放这个区域的内存

什么情况在永久区崩盘:一个启动类加载了大量的第三方jar包;Tomcat部署了太多的应用;大量动态生成的反射类等 不断的被加载,直到内存满,就会出现OOM。

  • jdk1.6之前:永久代,常量池是在方法区
  • jdk1.7 :永久代,但是慢慢退化了,出现了个概念:去永久代,常量池在堆中。
  • jdk1.8之后:无永久代,常量池在元空间中。

1.8之前:

image-20210218211244134

1.8之后:

image-20210218211634854

但是,元空间:逻辑上存在,物理上不存在

package com.draco.heapOverflow;

/**
 * 元空间逻辑上存在,物理上不存在
 */
public class SanQu {
    public static void main(String[] args) {
        // 返回jvm试图使用的最大内存
        long max = Runtime.getRuntime().maxMemory();
        // 返回jvm的初始化内存
        long total = Runtime.getRuntime().totalMemory();

        System.out.println("max="+max+"字节\t"+(max/(1024*1024))+"MB");
        System.out.println("total="+total+"字节\t"+(total/(1024*1024))+"MB");

        //默认情况下,试图分配的最大内存是电脑内存的1/4,而初始化的内存是1/64
        // -Xms1024m -Xmx1024m -XX:+PrintGCDetails
    }
}

当修改了VM选项后:-Xms1024m -Xmx1024m -XX:+PrintGCDetails,输出结果:

image-20210201230953431

让我们来算一笔账,

新生区:305664k;养老区:699392k

加在一起:1,005,056k,除以1024后 = 981.5MB,等于jvm试图分配的最大内存,所以说元空间逻辑上存在,物理上不存在。

4、 OOM

GC过程:添加虚拟机参数-XX:+PrintGCDetails打印信息查看:

image-20210218215132221

模拟OOM代码:

出现OOM如何解决?

  1. 尝试扩大堆内存去查看内存结果

    -Xms1024m -Xmx1024m -XX:+PrintGCDetails

  2. 若不行,分析内存,看一下是哪个地方出现了问题(专业工具)

    • 能够看到代码第几行出错:内存快照分析工具,MAT(eclipse),Jprofiler
    • Dubug,一行行分析代码!(不现实)

MAT,Jprofiler作用:

  • 分析Dump内存文件,快速定位内存泄漏
  • 获得堆中的数据
  • 获得大的对象

5、VM options参数

-Xms 设置初始化内存分配大小,默认1/64

-Xmx 设置最大分配内存,默认1/4

-XX:+PrintGCDetails 打印GC垃圾回收信息

-XX:+HeapDumpOnOutOfMemoryError 生成oomDump文件

-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-Xms1024m -Xmx1024m -XX:+PrintGCDetails

9 结尾

虚拟机的基本结构大体就是这样了,当然jvm还有最重要的GC垃圾回收机制,我另外开一篇文章详细描写,链接放下面。
java虚拟机简单介绍

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值