首页 > 其他分享 >JVM

JVM

时间:2022-10-22 17:32:47浏览次数:65  
标签:对象 回收 线程 引用 JVM 加载 内存

如何判断对象可以被回收

五种引用(面试常考):

  • 强引用
  • 弱引用
  • 软引用
  • 虚引用
  • 终结器引用​

    JVM_加载

image-20220811163328291

1 强引用

  • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
    如上图、只有B、C对象都不引用A1对象时,A1对象才会在垃圾回收时被回收;

2 软引用(SoftReference)

  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象(实在不行了才回收软引用对象)
  • 可以配合引用队列来释放软引用自身(因为软引用对象自身也是占一点内存的)

软引用的使用

JVM_加载_02

image-20220811163603544

JVM_类加载器_03

image-20220811163628973

可见前四次已经被回收了;

引用队列的使用

如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理(就是上图结果中的null值)

如果想要清理软引用,需要使用引用队列

JVM_JMM_04

image-20220811163843846


3 弱引用(WeakReference)

  • 仅有弱引用引用该对象时,在垃圾回收时,**(full gc时)无论内存是否充足都会回收**弱引用对象
  • 可以配合引用队列来释放弱引用自身

JVM_初始化_05

image-20220811164041023

结合引用队列同软引用相似


4 虚引用(PhantomReference)

  • 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存

5 终结器引用(FinalReference)

  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象

一、垃圾回收

1、垃圾回收算法(重要)

标记清除算法

定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间

JVM_类加载器_06

image-20220811164255455

清除后,对于腾出内存空间并不是将内存空间的字节清0,而是会把被清除对象所占用内存的起始结束的地址记录下来,放入空闲的地址列表中,下次分配内存的时候,再选择合适的位置存入,直接覆盖

优点:速度快;

缺点:容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致jvm启动gc,一旦启动gc,我们的应用程序就会暂停,这就导致应用的响应速度变慢


标记整理算法

标记-整理 会将不被GC Root引用的对象回收,清楚其占用的内存空间。然后整理剩余的对象,速度慢、可以有效避免因内存碎片而导致的问题,但是因为整体需要消耗一定的时间,所以效率较低


复制算法

复制算法将内存分为等大小的两个区域,FROM和TO(TO中始终为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。这样也可以避免内存碎片的问题,但是会占用双倍的内存空间。

JVM_JMM_07

image-20220811164455902


2、垃圾回收机制

回收流程

① 新创建的对象都被放在新生代的伊甸园

JVM_JMM_08

image-20220811164541767

② 当伊甸园空间不足时,会采用复制算法进行垃圾回收,这时的回收叫做Minor GC;把伊甸园和幸存区From存活的对象先复制到幸存区To中,此时存活的对象寿命+1,并清理掉未存活的对象,最后再交换幸存区From和幸存区To;

JVM_类加载器_09

image-20220811164612870

JVM_JMM_10

image-20220811164720253

JVM_加载_11

image-20220811164727571

③ 再次创建对象,若新生代的伊甸园又满了,则同上;

④ 如果经历多次垃圾回收,某一对象均未被回收,寿命不断+1,当寿命达到阈值时(最大为15,4bit)就会被放入老年代中;

JVM_JMM_12

image-20220811164852925

⑤ 如果老年代中的内存都满了,就会先触发Minor GC 如果内存还是不足,则会触发Full GC,扫描新生代和老年代中所有不再使用的对象并回收、

总结

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

GC 分析

大对象处理策略:

当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代


线程内存溢出:

某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行

这是因为当一个线程抛出OOM异常后它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常

3、垃圾回收器

串行

  • 单线程
  • 堆内存较小,适合个人电脑

吞吐量优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高(少餐多食)

响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5(少食多餐)

串行

JVM_JMM_13

image-20220811170019886

安全点

让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象;因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态

Serial 收集器

Serial收集器是最基本的、发展历史最悠久的收集器

特点:单线程、简单高效(与其他收集器的单线程相比),用于新生代采用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)

Serial Old 收集器

Serial Old是Serial收集器的老年代版本

特点:同样是单线程收集器,采用标记-整理算法


吞吐量优先

-XX:+UseParallelGC ~ -XX:+UserParallelOldGC

开启吞吐量优先的回收器,1.8版本默认开启;UseParallelGC是新生代的,采用复制算法;UserParallelOldGC是在老年代的,采用标记整理算法

JVM_加载_14

-XX:+UseAdaptiveSizePolicy

开启这个将采用自适应的大小调整策略,调整新生代的大小,包括堆的大小和晋升老年代的阈值大小等;种调节方式称为GC的自适应调节策略

-XX:GCTimeRatio=ratio

调整吞吐量的目标,即垃圾回收的时间与总时间的占比(1/(1+ratio)),默认ratio=99,1/(1+ratio))= 0.01,即垃圾回收的时间不能超过总时间的1%(比如总时间100分钟,垃圾回收的时间不能超过1分钟,如果超过1分钟,则GC会自适应的调整大小)

-XX:MaxGCPauseMillis=ms

指的是暂停的毫秒数,默认200ms,即上图红线(和Ratio对立,折中选取)

-XX:ParallelGCThreads=n

设置垃圾回收时运行的线程数


响应时间优先(CMS)

JVM_JMM_15

image-20220811165832530

CMS 收集器

Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器

特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片

应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务

CMS收集器的运行过程分为下列4步:

初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题

并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行

重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题

并发清除:对标记的对象进行清除回收

CMS收集器的内存回收过程是与用户线程一起并发执行

图解:

JVM_加载_16

4、垃圾回收调优

2.5.1 确定调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • IO
  • GC

2.5.2确定目标

低延迟/高吞吐量? 选择合适的GC

  • CMS(JDK8默认)、 G1(JDK9推荐) 、ZGC(JDK12体验)
  • ParallelGC(高吞吐量)
  • Zing(另一种虚拟机)

2.5.3最快的GC是不发生GC

首先排除减少因为自身编写的代码而引发的内存问题

  • 查看Full GC前后的内存占用,考虑以下几个问题
  • 数据是不是太多?(比如select *)
  • 数据表示是否太臃肿
  • 对象图
  • 对象大小(比如用Integer换成int会小很多)
  • 是否存在内存泄漏(采用软、弱引用;第三方缓存实现等)

2.5.4新生代调优

新生代的特点

  • 所有的new操作分配内存都是非常廉价的
  • 死亡对象回收零代价
  • 大部分对象用过即死(朝生夕死)
  • MInor GC 所用时间远小于Full GC

问:新生代内存越大越好么?

答:不是

  • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
  • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长
  • 新生代内存设置为能容纳所有【并发量*(请求-响应)】的数据为宜

2.5.5幸存区调优

  • 幸存区需要能够保存当前活跃对象+需要晋升的对象
  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistribution


2.5.6老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3

-XX:CMSInitiatingOccupancyFraction=percent


二、类加载阶段(重要)

1、加载

  • 将类的字节码载入方法区(1.8后为元空间,在本地内存中)中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
  • _java_mirror 即 java 的类镜像,例如对 String 来说,它的镜像类就是 String.class,作用是把 klass 暴露给 java 使用
  • _super 即父类
  • _fields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法
  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行

JVM_初始化_17

image-20220810223332427

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内,而元空间又位于本地内存中),但 _java_mirror
    是存储在堆中
  • InstanceKlass和*.class(JAVA镜像类 _java_mirror)互相保存了对方的地址
  • 类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息

2、链接

验证

验证类是否符合 JVM规范,安全性检查

例如:

用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数(3.1.1),在控制台运行

JVM_初始化_18

image-20220810230057883


准备

为 static 变量分配空间,设置默认值

  • static变量在JDK 7以前是存储与instanceKlass末尾。但在JDK 7以后就存储在_java_mirror末尾了(即堆中)
  • static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型(比如 new Object()),那么赋值也会在初始化阶段完成

解析

  • 将常量池中的符号引用解析为直接引用
  • 符号引用:仅仅是个符号,不知道这个类或者方法、属性具体在内存的哪个位置
  • 直接引用:知道这个类或者方法、属性具体在内存的哪个位置
package cn.itcast.jvm.t3.load;
/**
* 解析的含义
*/
public class Load2 {
public static void main(String[] args) throws ClassNotFoundException,IOException {
ClassLoader classloader = Load2.class.getClassLoader();
// loadClass 方法不会导致类的解析和初始化
Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
// new C(); new会导致类的解析和初始化
System.in.read();
}
}

class C {
D d = new D();
}

class D {

}

3、初始化

初始化即调用 ()V ,虚拟机会保证这个类的『构造方法』的线程安全

  • clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的
  • 所以验证类是否被初始化,可以看该类的静态代码块是否被执行

发生时机:

类的初始化的懒惰的,以下情况会初始化

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

以下情况不会初始化

  • 访问类的 static final 静态常量(基本类型和字符串)
  • 类对象.class 不会触发初始化
  • 创建该类对象的数组
  • 类加载器的.loadClass方法
  • Class.forNamed的参数2为false时

如下代码验证

package cn.itcast.jvm.t3.load;

import java.io.IOException;

public class Load3 {
static {
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException, IOException {
// // 1. 静态常量不会触发初始化
// System.out.println(B.b);
// // 2. 类对象.class 不会触发初始化
// System.out.println(B.class);
// // 3. 创建该类的数组不会触发初始化
// System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("cn.itcast.jvm.t3.load.B");
// // 5. 不会初始化类 B,但会加载 B、A
// ClassLoader c2 = Thread.currentThread().getContextClassLoader();
// Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
System.in.read();


// // 1. 首次访问这个类的静态变量或静态方法时
// System.out.println(A.a);
// // 2. 子类初始化,如果父类还没初始化,会引发
// System.out.println(B.c);
// // 3. 子类访问父类静态变量,只触发父类初始化
// System.out.println(B.a);
// // 4. 会初始化类 B,并先初始化类 A
// Class.forName("cn.itcast.jvm.t3.load.B");
}
}

class A {
static int a = 0;
static {
System.out.println("a init");
}
}

class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}

三、类加载器

1、类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等

以JDK 8为例

名称

加载的类

说明

Bootstrap ClassLoader(启动类加载器)

JAVA_HOME/jre/lib

无法直接访问

Extension ClassLoader(拓展类加载器)

JAVA_HOME/jre/lib/ext

上级为Bootstrap,显示为null

Application ClassLoader(应用程序类加载器)

classpath

上级为Extension

自定义类加载器

自定义

上级为Application

  • 各司其职,每个加载器只加载自己负责目录下的所有的类
  • 层级关系:
  • 自底向上询问有没有加载过,例如String类
  • 自定义类加载器 问 应用程序类加载器有没有加载String,如果没有,继续往上,到达启动类加载器中已经加载过了,则String不用再加载
  • 如果都没有加载过则由最顶级开始往下,查找自己负责的目录下能不能加载;例如自定义的Student类
  • 先往上询问,肯定都没有加载过,然后再一步步下来到应用程序加载器

2、启动类加载器

可通过在控制台输入指令,使得自定义类被启动类加器加载

在正确的路径下执行:java -Xbootclasspath/a:.cn.itcast.jvm.t3.load.Load5

3、扩展类加载类

如果classpath和 JAVA_HOME/jre/lib/ext 下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已将该同名类加载过了,则不会再次加载

4、自定义加载器

使用场景

  • 想加载非 classpath 随意路径中的类文件
  • 通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤

  • 继承ClassLoader父类
  • 要遵从双亲委派机制,重写 findClass 方法
  • 不是重写loadClass方法,否则不会走双亲委派机制
  • 读取类文件的字节码
  • 调用父类的 defineClass 方法来加载类
  • 使用者调用该类加载器的 loadClass 方法

5、双亲委派模式

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

注意
这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系

loadClass源码

递归查找

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先查找该类是否已经被该类加载器加载过了
Class<?> c = findLoadedClass(name);
//如果没有被加载过
if (c == null) {
long t0 = System.nanoTime();
try {
//看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,但它显示为null
if (parent != null) {
//有上级,就委派上级 这里是递归
c = parent.loadClass(name, false);
} else {
//如果没有上级了(ExtClassLoader),则委派BootstrapClassLoader 看是否被启动类加载器加载过
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//捕获异常,但不做任何处理
}

if (c == null) {
//如果还是没有找到,先让拓展类加载器调用findClass方法去找到该类,如果还是没找到,就抛出异常
long t1 = System.nanoTime();
c = findClass(name);

// 记录时间
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

为了防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性。


四、Java内存模型(JMM)

1、介绍

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(也叫工作内存),本地内存中存储了该线程共享变量的副本。

多线程对共享变量进行读/写操作,只会在自己的工作内存中,对共享变量副本进行读/写操作,所以各个线程间不互通。


JVM_初始化_19

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

只有对共享变量设置为可见性(使用volatile 修饰),多线程对共享变量只会在主内存中进行读写操作,不会再去工作内存操作共享变量副本。从而实现各个线程间通信


2、JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,下图展示了Java内存模型在JVM中的逻辑视图:

JVM_初始化_20

线程栈:JVM中运行的每个线程都拥有自己的线程栈。包含了当前线程执行的方法调用相关信息、当前方法的所有本地变量信息。

一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。


堆区:包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。

多线程各个线程创建的对象也存储在堆区中。


下图展示了调用栈和本地变量都存储在栈区,对象都存储在堆区:

JVM_类加载器_21

一个本地变量如果是原始类型,那么它会被完全存储到栈区。

一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。


对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。

对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。


Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。


堆中的对象可以被多线程共享。如果一个线程获得一个对象的应用,它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。


下图展示了上面描述的过程:

JVM_类加载器_22






标签:对象,回收,线程,引用,JVM,加载,内存
From: https://blog.51cto.com/u_15709549/5786031

相关文章

  • 请问Java语言是跨平台的吗?JVM是跨平台的吗?为什么?
    Java语言是跨平台的,JVM是不跨平台的。Java能跨平台正是以JVM不跨平台为代价的。Java的跨平台性由JVM实现,即JVM用于保证java的跨平台的特性。原理分析:JVM是在机器和编译......
  • JVM 性能调优实战之:一次系统性能瓶颈的寻找过程
    玩过性能优化的朋友都清楚,性能优化的关键并不在于怎么进行优化,而在于怎么找到当前系统的性能瓶颈。性能优化分为好几个层次,比如系统层次、算法层次......
  • JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码
    本文是《​​JVM性能调优实战之:一次系统性能瓶颈的寻找过程​​》的后续篇,该篇介绍了如何使用JDK自身提供的工具进行JVM调优将TPS由2.5提升到20(提升了7倍),并......
  • JVM垃圾回收机制
    Jvm虚拟机的垃圾回收机制1.垃圾回收发生在哪里?首先就需要知道Jvm的内存结构了:有五种(方法区、堆、虚拟机栈、本地方法栈、程序计数器);new就进堆,堆存储对象数据,而垃圾回收......
  • eclipse设置JVM内存堆
     前言环境不会改变,解决之道在于改变自己。最近笔者再整理堆内存溢出方向的课程,写些杂项资料,留着自用的,有需要的看看~一、JVM内存堆参数含义-Xmx128m#最大内存设......
  • JVM、JDK、JRE你分的清吗
    JVM、JDK、JRE你分的清吗前言在我们学习Java的时候,就经常听到"需要安装JDK"、"运行需要JRE"、"JVM调优"等等,这里面的JVM、JDK、JRE你真的分得清吗,今天我们就来讲讲它们......
  • 深入理解JVM(五)-JVM设置参数大全
    标准参数(-)所有的JVM实现都必须实现这些参数的功能,而且向后兼容;命令java-help可以列出java应用启动时标准选项(不同的JVM实现是不同的)。标准参数比较常用的配置参数说明-ve......
  • JAVA获取jvm和操作系统相关信息
    JAVA获取jvm和操作系统相关信息背景今日搬砖......
  • JVM面试基础
    JVM包含JVM面试入门必知.一.概述1.JDK,JRE,JVM关系JDK:JavaDevelopmentKits,Java开发工具包,包括JRE和Java开发辅助工具;JRE:JavaRuntimeEnvironment,J......
  • JVM体系结构
    java代码的编译过程Java源代码-->Java字节码-->类加载器、字节码验证-->JVM-->操作系统-->硬件体系结构......