JVM基础
Posted 小田mas
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM基础相关的知识,希望对你有一定的参考价值。
1、JVM位置
JVM运行在操作系统之上,虽然说是个环境,其实就相当于一个软件,jre里面包含了jvm
2、JVM体系结构
栈里面是不会有垃圾的,main方法在最底层,来一个方法就会弹出,所以不可能存在垃圾回收。
方法区就是一个特殊的堆,所谓的JVM调优99%就是在调方法区和堆这个区,大部分时间在调堆。
引用在栈里面,实例在堆里面。
3、类加载器
加载class文件
- 虚拟机自带的加载器
- 启动类(根)加载器
- 扩展类加载器
- 应用程序(系统类)加载器
从下往上一层一层地递进
null说明是不存在,或者说不是用java写的。 rt.jar(runtime.jar)根加载器里面的一些东西,在jvm里面。
PlatformClass Loader
ex 扩展类加载器在 jre/lib/ext 下,如果jar在目录下,也会到相应的地方(向上)去找。
应用加载器,相当于继承了ClassLoader。
打开rt.jar可以解压,相应的jar包是可以修改的。
突然想知道 什么是字节码文件?
字节码文件是经过编译器预处理过的一种文件,是JAVA的执行文件存在形式,
Java源程序(.java)要先编译成与平台无关的字节码文件(.class),然后字节码文件再解释成机器码运行。解释是通过Java虚拟机来执行的。
它本身是二进制文件,但是不可以被系统直接执行,而是需要虚拟机解释执行,由于被预处理过,所以比一般的解释代码要快,但是仍然会比系统直接执行的慢。
根加载器 bootstrapClassLoader加载器。
这里的直接去根加载器找String类(Student对应的student类),看到了ToString()方法,哎,就直接显示找不到main方法了。
没人写java.lang包,都是com.什么什么的写。
native 本地的方法,是c、c++写的,因为线程级别的东西java处理不了。
new Thread().start(); 其实最后调用了一个本地方法start0,还是建议稳一点。
双亲委派机制
下面的一段代码可以解释一下类加载机制。
public Class<?> loadClass(String name) throws ClassNotFoundException
return loadClass(name, false);
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null)
try
// 存在父加载器,递归的交由父加载器
if (parent != null)
c = parent.loadClass(name, false);
else
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
catch (ClassNotFoundException e)
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
if (c == null)
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
return c;
- 类加载器收到类加载的请求;
- 将这个请求委托给父类加载器去完成,一直向上委托,知道启动类加载器;
- 启动加载器检查是否能够加载这个类,能就加载,结束。使用当前的加载器,否则,抛出异常,通知子加载器进行加载
- 重复步骤3
那什么是双亲委派机制?
当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
下面的源码非常适合理解
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
synchronized (getClassLoadingLock(name))
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null)
long t0 = System.nanoTime();
try
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null)
c = parent.loadClass(name, false);
else
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
catch (ClassNotFoundException e)
if (c == null)
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
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;
类加载器的类别
BootstrapClassLoader(启动类加载器)
c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
ExtClassLoader (标准扩展类加载器)
java编写,加载扩展库,如classpath中的jre ,javax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
AppClassLoader(系统类加载器)
java编写,加载程序所在的目录,如user.dir所在的位置的class
CustomClassLoader(用户自定义类加载器)
java编写,用户自定义的类加载器,可加载指定路径的class文件
委派机制的流程图
双亲委派机制的作用
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
JNI
java native interface 本地方法接口
native:凡是带了native关键字的,说明java的作用范围到达不了,会去调用底层c的库。
会进入本地方法栈;
调用本地方法本地接口;JNI。
JNI作用:扩展java的使用,融合不同的编程语言为Java所用!最初:c、c++;
Java诞生的时候必须要有调用c、c++的程序;
在内存区域中专门开辟了一块标记区域:native mothod Stack,登记native方法。
在最终执行的时候,加载本地方法库中的方法通过JNI。
但是现在native越来越少用了,除非写java想操作硬件。比如用java驱动打印机,管理系统,在企业级应用中较为少见。
调用其他接口:socket,webservice,http
字符串常量池JDK 1.7之前在方法区,1.7之后在堆上。
字符串一般在常量池里面。
类模板在方法去之中。
静态变量(static)、常量(final)、类信息(构造方法、接口定义)(class)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,与方法区无关。
栈
一种数据结构,程序=数据结构+算法。
栈内存,主管程序的运行,生命周期和线程同步;
线程结束,栈内存也就释放。不存在垃圾回收,一旦线程结束,栈也就结束。
栈里面可以放什么:八大基本类型,引用类型,实例方法。
栈的运行原理:
额,看的网上的一个理解:
方法的代码,当然是在方法区。
栈上的那个包含有方法局部变量的,是在栈帧里面的,每一个方法调用,都会有一个对应的栈帧的创建。
但是老师讲的时候是将实例方法存放在了栈中,我看网上大部分说,方法的代码存放在方法去,方法的变量存放在栈,方法一调用就会有对应栈帧的创建。
我觉得两个的区别就在于时间上,经过探索,看看是不是执行到,没执行到之前确实是在方法区,执行到就会进栈。
等我一点一点探索,一点一点改!
java的本质是值传递。
栈中是怎么存数据的?
一个对象在内存中是怎么实例化的?
参考Java对象在内存中实例化的过程
堆
Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读取了类文件之后,一般会把什么东西放到堆中?类、方法、常量、变量,保存引用类型的真实对象。
堆内存还细分为三个区域:
- 新生区(伊甸园区)
- 养老区
- 永久区
特别长的字符串内存中是存不下的,
GC 垃圾回收算法
引用计数法
用的少
复制算法
谁空谁是to
每次清理完成之后伊甸园区是空的 to区是空的。
好处:没有内存的碎片
坏处:浪费了内存空间,永远多了一块空的内存to,假设对象百分之百成活,占了整个幸存区,(极端情况下)
复制算法最佳使用场景:
对象存活度较低的情况,新生区
标记压缩清除算法
先标记清除几次,再压缩。
缺点:两次扫描严重浪费时间,会产生内存碎片
优点:不需要额外的空间(复制算法需要to)
标记压缩:
防止产生内存碎片,就是再次扫描,向一端移动存活的对象。多了一个移动成本。
总结
内存利用率:复制算法(没有浪费的空间)>标记清除算法>标记压缩算法(时间复杂度)
内存整齐度:复制算法=标记压缩算法>标记清除算法
内存利用率:标记压缩算法=标记清除算法>复制算法
没有最好的算法,只有最合适的。
GC:分代收集算法
年轻代:
- 存活率低,复制算法
老年代:
- 区域大
- 存活率高
- 标记清除+标记压缩混合实现
-jvm就在调这个(几次+几次)
JMM Java memory model
1.什么是JMM
java内存模型
2.是干嘛的?
作用:缓存一致性协议,用来定义数据读写的规则。
JMM定义了线程工作内存和主内存之间的抽象关系:线程之间的共享变量都在主内存(Main Memory),每个线程都有一个私有的本地内存(Local memory)。
Java有一个主内存。
线程工作内存,每个线程都有自己的工作区域,是从主内存拷贝的。
JMM和底层实现原理。
volatile:解决共享对象可见性这个问题的关键字。保证刷新到内存。
3.该如何学习?
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存(volatile)
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
以上是关于JVM基础的主要内容,如果未能解决你的问题,请参考以下文章