-
JDK 简单理解就是Java开发工具包,是java的核心所在;JRE 是 Java 的运行环境,JVM 也就是常常听到的 Java 虚拟机。
-
JDK是整个Java的核心,包括了 JRE ,同时在 jdk 文件夹 bin 目录中包含了一些Java开发工具(例如:javac、java、javadoc、jar等)。
-
JRE(Java Runtime Environment),即Java运行环境,支持Java程序运行的标准环境,包含JVM标准实现及Java核心类库。
-
JVM ,即Java虚拟机,运行在操作系统之上,存在于内存中,与内存打交道,与硬件没有直接交互,是Java语言实现跨平台的核心。
-
JDK是面向开发者的,JRE是面向使用JAVA程序的用户,JVM是java实现跨平台和系统的媒介所在。
- 发展会越来越趋于模块化
- 语言会越来越混合
- 多核并行
- 进一步丰富语法
- 64位虚拟机
JVM的种类有很多,总结出来一般有以下几种:
- Sun Classic Vm(这个是Sun公司出来的最开始的虚拟机)
- Exact Vm(这个也是最初的一款虚拟机和上面的Classic Vm齐名)
- HotSpot Vm(这个是目前用的最多的一款虚拟机,它结合了上面两款虚拟机的特点,还有自己的优势)
- KVM(它强调简单、轻量、高度可移植,但是运行速度比较慢。在 Android、iOS等智能手机操作系统出现前曾经在手机平台上得到非常广泛应用)
- JRockit(这款虚拟机和下面的J9以及上面的HotSpot合称为商业用的最大的三款虚拟机)
- J9(这一款和上面的JRockit和HotSpot合称为商业用的最大的三款虚拟机,这款虚拟机在某些方面做的比HotSpot还要更加的出色)
- Dalvik(这款虚拟机可谓是挑战者,它并不是JVM,不能直接执行Java的Class文件,但是好像Android在用,性能还挺好的)
- Microsoft JVM(这是一款没有成功,也没有失败的虚拟机)
- Azul VM 和 Liquid VM(这两款虚拟机可谓是软硬件的合并,需要和特定的硬件进行绑定从而可以有更加高效的性能)
我们使用的是HotSpot这一款,它的特点是:继承了Sun Classic Vm和Exact Vm的优点,此外还增加了热点代码侦测的技术
Java虚拟机顾名思义就是专门为运行Java程序所编写的一款虚拟机。
Java虚拟机运行的是java指令集
它与VMware的区别是,JVM只是一个虚拟的环境,它并没有去直接连接硬件,而是去模拟一个与硬件相似的环境,而VMware是直接去连接主机上的硬件来使用。
VMware虚拟机运行的是cpu指令集
系统虚拟机:Visual Box,VMware就属于系统虚拟机,他们完全是对物理计算机的仿真,提供了一个可运行完成操作系统的软件平台。
程序虚拟机:代表就是典型的Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟中执行的指令我们称之为Java字节码指令。
JVM由三个主要的子系统构成: - 类加载子系统 - 运行时数据区(内存结构) - 执行引擎
- 类加载子系统负责从文件系统或网络中加载Class信息,加载的类信息存放于方法区。除了类信息,方法区还会存放运行时常量池信息,包括字符串字面量和数字常量(Class文件中常量池部分的内存映射)。
- Java堆在系统启动的时候建立,几乎所有的Java对象都存放在Java堆中,堆空间是所有线程共享的。
- Java的NIO库允许Java程序使用直接内存,直接内存是在Java堆外的、直接向系统申请的内存。访问直接内存的速度会由于Java堆(避免了在Java堆和直接内存中来回复制数据),读写频繁的场合可能会使用直接内存。Java堆和直接内存的总和受限于操作系统的最大内存。
- 垃圾回收系统可以对方法区、Java堆和直接内存进行回收,其中Java堆是垃圾收集器的工作重点。对于不再使用的对象,垃圾回收系统会在后台默默查找、标识并释放垃圾对象。
- 每一个Java虚拟机线程都有一个私有的Java栈,Java栈在线程创建的时候被创建。Java栈中保存着帧信息(存储局部变量表,操作数栈、动态链接、方法出口等信息),方法执行的过程对应栈帧入栈与出栈的过程。
- 本地方法栈和Java栈类似,用于本地方法(通常用C编写)调用,Java虚拟机允许Java直接调用本地方法。
- PC(Program Counter)也是每个线程私有的空间,Java虚拟机会为每个Java线程创建程序计数器。任意时刻,1个Java线程总是在执行1个方法,正在被执行的方法称为当前方法。如果当前方法不是本地方法、PC寄存器就会指向当前被执行的指令,如果当前方法是本地方法,PC寄存器的值为undefined。
- 执行引擎负责执行虚拟机的字节码,现代虚拟机为了提高执行效率,会使用即时编译技术将方法编译成机器码后在执行。
- 加载:查找并加载类的二进制数据
- 连接:
验证:确保被加载的类的正确性
准备:为类的静态变量分配内存,并将其初始化为默认值
解析:把类中的符号引用转换为直接引用
符号引用:通俗的讲,是一种间接引用,如一个类中的方法引用了另外一个类,这是一种符号的表述。
直接引用:就是通过指针的方式,直接指向了目标对象内存的位置,这样能一下子找到特定的方法。 - 初始化: 为类的静态变量赋予正确的初始值
基于栈的指令集架构的优点是:
- 设计和实现更简单,适用于资源受限的系统;
- 避开了寄存器的分配难题:使用零地址指令方式分配。
- 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
- 不需要硬件支持,可移植性更好,更好实现跨平台
不是只能由 javac 编译而来的java源代码产生,还有Kotlin,Scala, JavaScript, Groovy等等都可以通过编译器,编译成字节码文件运行到Java虚拟机。
------------类加载相关------------ 1.jvm在什么情况下会加载一个类?问题:怎么测试一个类它被加载上了 2.类加载到jvm中的过程?每个阶段的工作?
类加载过程主要分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。
1、加载:
1)、通过一个类的全限定名加载该类对应的二进制字节流。主要通过类加载器实现。
2)、将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区各个类访问该类的入口。(Hotspot 在方法区生成该类)。
2、验证:
1)、文件格式验证:验证类文件的魔术版本号常量等是否符合当前虚拟机支持的范围。
2)、元数据验证:验证类的语义信息,是否符合java语言规范的要求。
3)、字节码验证:验证程序语义是合法的、合乎规范的。主要通过stackmapframe结构。
4)、符号引用验证:虚拟机在将符号引用转化为直接引用,验证符号引用全限定名代表的类是否能够找到,对应的域和方法是否能找到,访问权限是否合法。
3、准备:
准备阶段主要是将类变量(被static修饰符修饰)在方法区进行内存分配并进行初始化。
| 数据类型 | 零值 | 数据类型 | 零值 |
|---|---|---|---|
| int | 0 | boolean | false |
| long | 0L | float | 0.0f |
| short | 0 | double | 0.0d |
| char | ‘u0000’ | reference | null |
| byte | 0 |
4、解析:
1)、类或接口解析:将符合引用转化为类的直接引用,并检查访问权限。
2)、字段解析:将字段的符号引用转化为字段所属的类信息或其父类该字段的直接引用,并检查访问权限。
3)、类方法解析:将类方法的符号引用转化为类方法所属的类信息或其父类该字段的直接引用,并检查访问权限。
4)、接口方法解析:将接口方法的符号引用转化为接口方法所属的接口信息或其父类该字段的直接引用,并检查访问权限。
5、初始化:
初始化阶段编译器会将类文件声明的静态赋值变量和静态区域合并生成方法并进行调用。
类加载器:类加载器是“通过一个类的全限定名加载这个类的二进制字节流”的实现,对于任何一个类,都是由类加载器和该类的本身共同确定在虚拟机中的唯一性。
双亲委派模型:
1、Bootstrap ClassLoader 启动类加载器,负责加载
2、Extension ClassLoader 拓展类加载器,负责加载
3、Application ClassLoader 应用类加载器,负责加载CLASSPATH路径下的JAVA类库。
4、User ClassLoader 用户自定义的类加载器。
双亲委托模型采用这样的方式加载类:当类加载器收到加载类请求时,首先委托父类加载该类,所有类加载器都采用这种方式,因此所有类加载请求都会到达顶层父类,父类加载不到时再使用该类加载器中加载。这样,类加载器之间就有了一种层级关系,能够保证Java的基础类由相同的类加载器加载,对Java系统的稳定性起到至关重要的作用。
3.jvm中的类加载器的类型及它加载的目标路径?如何自定义一个类加载器加载一个指定目录下的class文件? 了解Java类加载机制应该就会知道,Java提供了3中类加载器:启动类加载器、扩展类加载器和应用程序类加载器。它们分别负责加载不同路径下的类库。
根加载器 (BootStrap ClassLoader)用来加载JVM提供的基础运行类,即位于%JAVA_HOME%jre/lib 这个目录下面的核心类库。根加载器是所有加载器的父加载器。
扩展类加载器 (Extension ClassLoader)是根加载器的子加载器,负责 $JAVA_HOME/lib/ext 目录下或者 java.ext.dirs 系统变量指定路径下扩展类的加载。
系统类加载器 (Application ClassLoader)也叫做应用程序类加载器。是扩展类加载器的子加载器。负责加载用户类路径 classpath 指定的类。
com.my.test.load3.ClassLoaderTest2
在idea中运行 ClassLoaderTest2 类 获取三类加载器的路径 ,可以看出:
启动类加载器所加载的是JVM中最底层的类,加载时的搜索路径是sun.boot.class.path。
扩展类加载器是用来加载java的一些库的,加载时的搜索路径是java.ext.dirs
应用类加载器搜索路径是java.class.path
4.什么是双亲委派模型,有什么作用?双亲委派模型有两个好处:
- 向上委托给父类加载,父类加载不了再自己加载,防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,如果加载过了,则不需要重新加载。保证了数据安全。
- 避免重复加载,防止Java核心api被篡改。通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全。
加载器自上而下分别为,启动类加载器(Bootstrap ClassLoader), 拓展类加载器(Extension ClassLoader), 系统类加载器(Application ClassLoader) , 自定义类加载器(Custom ClassLoader)
双亲委派模式是Java1.2之后引入的,其工作原理是,如果其中一个类加载器收到了类加载的请求,它并不会自己去加载而是会将该请求委托给父类的加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,如此递归,请求最终到达顶层的启动类加载器。如果父类能加载,则直接返回,如果父类加载不了则交由子类加载,这就是双亲委派模式。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素, java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java APlI发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
5.类加载器是如何确定一个类在jvm中的唯一性的? 两个类来源于同一个Class文件,被同一个虚拟机加载,这两个类一定相等吗?自定义的类加载器myLoader可以加载与自己在同一路径下的Class文件。我们使用这个类加载器去加载了一个名为“com.my.test.load4.MyClassLoaderTest ”的类,并实例化了这个类的对象。
com.my.test.load4.MyClassLoaderTest
两行输出结果中,从第一句可以看出,这个对象确实是类com.my.test.load4.MyClassLoaderTest实例化出来的对象,但从第二句可以发现,这个对象与类com.my.test.load4.MyClassLoaderTest 做所属类型检查的时候却返回了false。
这是因为虚拟机中存在了两个MyClassLoaderTest类,一个是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时结果自然为false。
6.tomcat的类加载器有哪些?Tomcat类加载器
一方面出于架构考虑,一方面也是出于servlet规范考虑(每个应用要有单独的类加载器),应用服务器通常自定义类加载器已实现更加灵活的控制。
隔离性: web应用类库要相互隔离,避免依赖库互相影响,
**灵活性:**指针对某一个应用重新部署时不用加载所有应用的类库。
性能: 每个应用都有一个类加载器,web类重新加载时不涉及其他应用的类,速度更快。
tomcat类加载方案(Tomcat Bootstrap类 是由 java类加载器中的 System Class Loader 来加载的)
tomcat也提供了三个基础类加载器和web应用类加载器
Common: 以System类加载器作为父类,是tomcat顶层的公用类加载器,路径由conf/catalina.properties中的common.loader指定,默认指向 CATALINA_HOME/lib下的包。负责加载Tomcat应用和Web应用可见的jar包,如Servlet规范包等。
**Catalina:**以Common类加强在其为父类加载器,是用于加载Tomcat应用服务器的类加载器,路径为server.loader,默认为空,使用Common类加载器进行加载。只负责加载tomcat应用类,这些类对web应用不可见。使得web应用和tomcat实现松耦合。
Shared: 以Common类加强在其为父类加载器,是所有web应用类加载器的父类加载器,路径为shared.loader,默认为空个,使用Common类加载器作为web应用类加载器的父类加载器。负责加载web应用共享的类,Tomcat服务器不可见。
**Web应用:**以shared类加载器为父类加载器,加载/WEB-INF/classes目录下未压缩的Class和资源文件以及/WEB-INF/lib下的jar包,该类加载器只对当前应用可见,其他应用不可见。
默认情况下三个基础类加载器都是同一个(Common)但是可以通过配置使三个基础类加载器各自生效。
CATALINA_HOME/bin下的类作为启动入口由System类加载器进行加载。
共享性: 通过common类加载器实现 tomcat应用和web应用的jar包共享,通过shared类加载器实现 web应用间jar包共享。
隔离性: 实现了tomcat服务器和web应用的隔离,以及各个web引用之间的隔离。
默认情况下三个基础类加载器都是Common,这种情况下是通过JVM的安全策略许可实现禁止web应用使用服务器实现类。
web应用类加载器
java默认类加载器的委派逻辑
1 从缓存中加载
2 如果缓存没有,从父类加载器进行加载
3 父类加载器没有则,从当前类加载器进行加载
4 如果都没有抛出异常
**tomcat类加载器的委派逻辑有所不同,进行类加载时除了JVM基础类库外,首先尝试通过当前类加载器进行加载,然后才进行委派。**Servlet规范相关的API禁止通过web应用类加载器进行加载,因此这些包不要放在web应用中。
web应用类加载器默认加载顺序如下:
1 从缓存中加载
2 如果没有 从JVM的Bootstrap类加载器加载
3 如果没有 从当前类加载器进行加载(WEB-INF)
4 如果没有 从父类加载器进行加载,由于父类加载器采用了默认的委派模式,所以加载顺序为System Common Shared
**Tomcat 提供了delegate属性用于控制是否启用java委派模式,**默认false(不启用),当配置为true时,tomcat使用java默认委派模式,顺序如下:
1 从缓存中加载
2 如果没有 JVMBootstrap类加载器加载
3 如果没有从父类加载器进行加载 System Common Shared
4 如果没有 从当前类加载器进行加载
也可以通过packageTriggerDeny 树形只让某些包路径采用 java委派模式。
部分源代码:
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if (commonLoader == null) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader = this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
7.双亲委派模型最大问题:底层的类加载器无法加载底层的类, 比如如下情况:
javax.xml.parsers包中定义了xml解析的类接口, Service Provider Interface SPI 位于rt.jar 即接口在启动ClassLoader中, 而SPI的实现类,通常是由用户实现的, 由AppLoader加载。
以下是javax.xmlparsers.FactoryFinder中的解决代码:
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
} else {
cl = ss.getContextClassLoader(); //获取上下文加载器
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return cl.loadClass(className); //使用上下文ClassLoader
}
}
}
else {
return cl.loadClass(className);
}
}
catch (ClassNotFoundException e1) {
if (doFallback) {
// Use current class loader - should always be bootstrap CL
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
}
更多可以参考理解: jdbc的SPI 加载方式. https://blog.csdn.net/syh121/article/details/120274044
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
a. Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent
b. OSGi的ClassLoader形成网状结构,根据需要自由加载Class
热替换是指在程序的运⾏过程中,不停⽌服务,只通过替换程序⽂件来修改程序的⾏为。
热替换的关键需求在于服务不能中断,修改必须⽴即表现正在运⾏的系统之中。
基本上⼤部分脚本语⾔都是天⽣热替换的,⽐如:PHP,只要替换了PHP源⽂件,这种改动就会⽴即⽣效,⽽⽆需重启Web服务器。
但对Java来说,热替换并⾮天⽣就⽀持,如果⼀个类已经加载到系统中,通过修改类⽂件,并⽆法让系统再来加载并重复定义这个类。因此,在Java中实现这⼀功能的⼀个可⾏的⽅法就是灵活运⽤ClassLoader。
方法是类构造器方法,只对类方法(static域和static代码块)进行初始化,方法是在:加载->链接->初始化的初始化阶段进行的。Java在编译之后会在字节码文件中生成方法,
将静态域和静态代码块收敛到方法中,收敛顺序为:
父类静态变量初始化(默认值0或null)
父类静态语句块初始化
子类静态变量初始化
子类静态语句块初始化
方法是实例构造器,在对象被创建(new)时,进行实例化操作的方法。并且子类的方法会首先调用父类的方法(即在constructor中首先调用super)。
Java在编译之后会在字节码文件中生成方法,该实例构造器会将变量,语句块初始化,调用父类的构造器等操作收敛到方法中,收敛顺序为:
父类变量初始化(这里的初始化,就是赋予默认值)
父类语句块初始化
父类构造函数(赋予变量用户值)
子类变量初始化
子类语句块初始化
子类构造函数
方法的的执行时间一定早于,因为方法是在类初始化过程中执行的,而方法是在对象实例化时执行的。
所以,整个顺序就是:
父类静态变量初始化
父类静态语句块初始化
子类静态变量初始化
子类静态语句块初始化
父类变量初始化(初始化非静态成员变量(赋予默认值))
父类语句块初始化
父类构造函数(构造函数中赋值(变量用户值))
子类变量初始化
子类语句块初始化
子类构造函数
========================================
从虚拟机层面来看,以下代码运行时
public class ClassInitTest {
private static int number = 10; //linking之prepare: number = 0 --> initial: 10 --> 20
static {
System.out.println(number);//10
number = 20;
System.out.println(number);//20
}
public static void main(String[] args) {
System.out.println(ClassInitTest.number);//20
System.out.println(ClassInitTest.number);//20
}
}
number值的变化历程???
======================================== 以下代码为输出类加载器的名字, 请给出你的结果:public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
========================================
给出以下代码输出的各个类加载器的路径( 注意:分别idea 测试和 直接在控制台下输出 测试两次,查看结果的不同,给出结论)
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
====================================
获取ClassLoader 实例的途径
- 获取当前类的 ClassLoader :clazz.getClassLoader()
- 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
- 获取系统的ClassLoader : ClassLoader.getSystemClassLoader();
- 获取调用者的ClassLoader : DriverManager.getCallerClassLoader()
那么以下代码输出的类加载器将是什么:
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.Class.forName().getClassLoader()
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader); // String 类由启动类加载器加载,我们无法获取
//2.Thread.currentThread().getContextClassLoader()
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.ClassLoader.getSystemClassLoader().getParent()
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
========================================
测试一下双亲委派模型在保证系统模型统一方面的作用
创建一个类: java.lang.String
package java.lang;
public class String {
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
以上代码的输出结果是什么?
如果修改一下代码如下:
package java.lang;
public class String {
static{
System.out.println("我是自定义的String类的静态代码块");
}
public static void main(String[] args) {
System.out.println("hello,String");
}
}
输出会是什么? 为什么? 这种机制就叫沙箱安全机制,请试着解释此术语及作用?
如果在自定义的java.lang包下定义自己的类,代码如下,会发生什么,请试分析为什么?
package java.lang;
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
}
}
========================================
每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
1.jvm运行时数据区的划分? 程序计数器程序计数器是一块较小的内存空间,它可以看作当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支,循坏,跳转,异常处理,线程恢复等基础功能都需要依赖计数器来完成。作用,是记住下一条jvm指令的执行地址 特点 是线程私有的 不会存在内存溢出
每个线程都有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,这类内存区域为线程私有的内存。
Java 虚拟机栈Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的线程内存模型:每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用到执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈本地方法栈与 Java 虚拟机栈所发挥的作用类似,其区别只是虚拟机栈为虚拟机执行 Java 方法(字节码)服务,而本地方法栈则是为虚拟机使用到本地方法服务。
Java 堆Java 堆是虚拟机管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java 堆就是存放 Java 对象实例的,同时也是垃圾回收的主要区域。
Java 堆既可以被实现成固定大小,也可以扩展。当前主流的 Java 虚拟机都是可扩展的。如:
-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2
堆内存初始化值 20M,堆内存最大值 20M,新生代最大值可用 1M,eden 空间和 survivor 空间大小比例为 2 :1
方法区也是各个线程共享的内存区域,它用于已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存数据。
2.根据jvm规范,这些数据区中哪些会出现 内存溢出异常,分别是什么场景下出现? 1、方法区溢出第一种原因:上线的时候MetaSpace区域直接用默认的参数,即根本不设置其大小,这会导MetaSpace区域可能才几十M而已或者设置的比较小,此时对于一个稍微大型系统,因为他有很多自己的类,还依赖了很多外部jar包的类,MetaSpace空间很容易不够的。
第二种原因:就是很多人开发系统代码都会用一些cglib之类的技术动态生成一些类,一旦代码没有控制好,导致生成的类过多的时候,就很容易MetaSpace给塞满,进而引起内存溢出。
2、虚拟机栈/本地方法栈溢出(1)StackOverflowError:当线程请求的栈的深度大于虚拟机所允许的最大深度,则抛出StackOverflowError,**简单理解就是虚拟机栈中的栈帧过多(一个线程嵌套调用的方法数量过多)时,就会抛出StackOverflowError异常。**最常见的场景就是方法无限递归调用。
Demo1_2.java
(2)OutOfMemoryError:如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError。
虚拟机中可以供栈占用的空间≈可用物理内存 - 最大堆内存 - 最大方法区内存,比如一台机器内存为 4G,系统和其他应用占用 2G,虚拟机可用的物理内存为 2G,最大堆内存为 1G,最大方法区内存为 512M,那可供栈占有的内存大约就是 512M,假如我们设置每个线程栈的大小为 1M,那虚拟机中最多可以创建 512个线程,超过 512个线程再创建就没有空间可以给栈了,就报 OutOfMemoryError 异常了。
(1)内存中加载的数据过多如一次从数据库中取出过多数据;集合对对象引用过多且使用完后没有清空;代码中存在死循环或循环产生过多重复对象;堆内存分配不合理;网络连接问题、数据库问题等。
(2)系统承载高并发请求,因为请求量过大,导致大量的对象都是存活的,所以要放入新的对象放不下了,此时就会引起内存溢出系统崩溃;
(3)系统有泄漏的问题,就是莫名其妙产生了很多对象,结果对象都是存活的,没有及时取消他们的引用,导致触发GC还是无法回收,此时只能引发内存溢出,因为实在是放不下更多的对象了。
本机直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但 Java 中用到 NIO 相关操作时(比如 ByteBuffer 的 allocteDirect 方法申请的是本机直接内存),也可能会出现内存溢出的异常
3.这些数据区哪些是线程独有的,哪些是线程共享区?堆区和⽅法区是所有线程共享的,虚拟机栈、本地⽅法栈、程序计数器是每个线程独有的
- 堆: 不用多说了,放对象的地方
- 方法区: 类定义的成员变量丶常量丶静态变量丶方法都在这里
- 虚拟机栈: 程序运行才有的,会把运行时的方法压入栈,里面有局部变量等东西
- 本地方法栈: 操作系统方法
- 程序计数器: 标记代码走到哪里了
- 程序计数器是一块较小的内存空间,几乎可以忽略;
- 是当前线程所执行的字节码的行号指示器;
- ava多线程执行时,每条线程都会有一个独立的程序计数器,各条线程之间互不影响;
- 是线程私有的,每个线程独立存储;
- 不存在内存溢出
- 无GC回收
- 方法区与堆一样是各个线程共享的内存区域
- 方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续
- 方法区的大小跟堆空间一样 可以选择固定大小或者动态变化
- 方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类导致方法区溢出,虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久带) Java 8之后是MetaSpace(元空间) )
- 关闭JVM就会释放这个区域的内存
- 是Java虚拟机所管理的内存中最大的一块。
- 堆是jvm所有线程共享的。
- 在虚拟机启动的时候创建。
- 唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
- Java堆是垃圾收集器管理的主要区域。
- 因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间。
- Java堆在计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
- 方法结束后,堆中对象不会马上移出,仅仅在垃圾回收的时候时候才移除。
- 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
-
线程私有
-
方法执行会创建栈帧,存储局部变量表等信息;
-
方法执行遵循(先进后出)FILO(First In, Last Out);
-
栈深度大于虚拟机所允许会抛出栈溢出 StackOverflowError
-
栈需要扩展而无法申请空间OutOfMemoryError;—hostspot虚拟机没有
-
栈里面运行方法,存放方法的局部变量名,变量名所指向的值都存在堆上;
-
栈一般不设置大小,栈所占的空间很小,可通过-Xss1M设置,如果不设置,默认为1M
-
随线程而生,随线程而灭
-
该区域不会GC
- Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法(一般非Java实现的方法)的调用。
- 本地方法栈,也是线程私有的。
- 允许被实现成固定或者是可动态拓展的内存大小。(和Java虚拟机栈在内存溢出方面情况是相同的)
- 本地方法就是java代码里面写的native方法,它没有方法体。是为了调用C/C++代码而写的。在JNI程序里面使用。
-
程序计数器是计算机处理器中的寄存器,它包含当前正在执行的指令的地址(位置)。当每个指令被获取,程序计数器的存储地址加一。在每个指令被获取之后,程序计数器指向顺序中的下一个指令。当计算机重启或复位时,程序计数器通常恢复到 [1] 零。
-
程序计数器是用于存放下一条指令所在单元的地址的地方。
-
是线程独有,不存在内存溢出
保存数据有:1、局部变量表2、操作数栈3、动态连接4、方法返回地址
通过一个栈帧一个栈帧的存到虚拟机栈,一个栈帧保存一个方法。对栈帧的压栈和出栈遵循“先进后出”/“后进先出”原则。
虚拟机栈的线程是私有的。
有内存溢出问题:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError。
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError。
优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
7.虚拟机栈的大小是否可动?是否会有异常出现?虚拟机栈的大小通过 -Xss(需要设置的栈内存大小) 设置
如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 outofMemoryError 异常。
8.如何设置虚拟机栈大小?官方文档:
-Xss size Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, or g or G to indicate GB.
The default value depends on the platform: Linux/x64 (64-bit): 1024 KB
OS X (64-bit): 1024 KB Oracle Solaris/x64 (64-bit): 1024 KB Windows:
The default value depends on virtual memory ( 默认值与虚拟内存有关 )
通过更改vm选项:-Xss256k // -Xss(需要设置的栈内存大小)
本地方法就是java代码里面写的native方法,它没有方法体。是为了调用C/C++代码而写的。在JNI程序里面使用。
10.什么叫本地方法栈?有什么作用?它是线程私有的吗? 它是否有可能抛出异常?- 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用非常相似,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
- 本地方法栈也是线程私有的。
- 和虚拟机栈一样,本地方法栈也会在栈深度溢出或者拓展失败时分别跑出StackOverFlowError和OutOfMemoryError异常。
- 本地方法是使用C语言实现的。
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界,它和虚拟机拥有同样的权限,
- 不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈使用的语言,具体实现方式,数据结构等。如果JVM不打算支持native方法,也可以无需实现本地方法栈。
- 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一。
不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈使用的语言,具体实现方式,数据结构等。
12.方法区是线程独有的吗?它是否有异常?它的作用?-
方法区是线程共享的;当有多个线程都用到一个类的时候,而这个类还未被加载,则应该只有一个线程去加载类,让其他线程等待;
-
如果方法区域中的内存无法满足分配请求,Java虚拟机将抛出一个OutOfMemoryError.
-
它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法(2.9)用于类和实例初始化以及接口初始化。
-
方法区的大小不必是固定的,jvm可以根据应用的需要动态调整。jvm也可以允许用户和程序指定方法区的初始大小,最小和最大限制;
-
方法区同样存在垃圾收集,因为通过用户定义的类加载器可以动态扩展Java程序,这样可能会导致一些类,不再被使用,变为垃圾。这时候需要进行垃圾清理。
-
在JDK7及以前,习惯上把方法区,称为永久代。jdk8开始,使用了元空间取代了永久代。
-
本质上,方法区和永久代并不等价。仅是对hotspot而言的。《Java虚拟机规范》对如何实现方法区,不做统一要求。
例如:BEA JRockit/ IBM J9 不存在永久代的概念。
-
现在来看,当年使用永久代不,不是好的注意。导致Java程序更容易OOM(超过-XX:MaxPermSize上限)
jdk7及以前:
通过-XX:PermSize 来设置永久代初始分配空间,默认值是20.75m
-XX:MaxPermSize来设定永久代最大可分配空间,32位是64m,64位是82mjdk8及之后:
通过-XX:MetaspaceSize 来设置永久代初始分配空间,默认值是21m
-XX:MaxMetaspaceSize来设定永久代最大可分配空间,值为-1
由于没有上限,因此当本机内存耗尽时,会抛出oom的错误,对于起始值21m来说,如果所使用的内存超过这个值,则会触发full gc卸载没用的类,之后将会重置这个初始值,新的初始值的高低在于gc后释放了多少的空间,释放得少则提升这个初始值,释放得多则降低这个初始值。因此在实际的开发场景中,为了减少full gc的频率,会将这个初始值设置大一些。
一、Java堆溢出
1、产生的原因:因为堆中存放的是对象实例和数组,所以当对象数量>最大堆容量限制时,就会发生内存溢出异常;
2、解决方案:
1)如果对象不是必须的,但是又有指向GC Root(后面章节介绍)的引用链,此时无法被GC,就会出现内存泄露,可通过定位泄露原因在代码中找到解决方案;
2)如果对象是必须的,就要检查虚拟机栈的堆参数能否调大(当虚拟机内存总容量小于物理内存时可以调大),如果能调大可通过修改该参数来解决:
-Xmx:最大堆内存
-Xms:最小堆内存
如果-Xmx和-Xms相同,则说明堆内存不可动态扩展。
二、虚拟机栈和本地方法栈溢出1、发生内存溢出的原因:
由于在HotSpot虚拟机中,不区分虚拟机栈和本地方法栈,所以设置本地方法栈大小的参数–Xoss无效,一般通过**–Xss参数设置栈容量**(我猜测ss是stack size的缩写,这样比较好记)
虚拟机中定义了两种异常情况:
1)当线程申请的栈深度超过虚拟机允许的最大栈深度时,会发生StackOverflowError异常;
2)当栈内存扩展时,如果不能申请到足够的内存,就会发生OutOfMemoryError异常
我们知道这部分内存是线程私有的,每个线程都需要分配一块内存,所以当线程很多时就会发生内存溢出,下面来分析一下这句话背后的原理:
①内存容量=堆内存+方法区内存+程序计数器内存(可忽略)+栈内存(虚拟机栈和本地方法栈);
②因为栈容量在编译器就可知,且一旦分配在运行期就不会改变,在栈容量一定的情况下,每个虚拟机栈分配到的栈容量越大,可以创建的线程数就越少;
③当线程过多时,就会导致栈容量不足,从而发生内存溢出;
2、解决方法:
首先,判断能不能减少线程数,如果能则减少线程数;如果不能减少线程数,就只能通过减小最大堆内存容量和最大栈容量来解决:
1)-Xmx:减少
2)-Xms:减少
三、方法区和运行时常量池溢出1、异常发生原因
方法区主要存储class的相关信息,如类,名、访问修饰符、常量池、字段描述信息、方法描述信息等,所以如果运行时产生大量的类去填满方法区,就能出现内存溢出异常。这里就涉及到如何动态产生大量类的方法,一般有如下两种:
1)使用反射机制或动态代理
2)使用CGLib直接操作字节码
2、解决方法:
通过调节方法区大小参数 -XX:PermSize 和 -XX:MaxPermSize 限制方法区大小,当设置成相同的值时不可扩展。
除以上三种虚拟机内存溢出情况之外,还有一种本机直接内存溢出,可通过调节参数 -XX:MaxDirectMemorysize 指定,若不指定,则和Java堆内存大小一样。
1.堆空间的空间划分 2.堆空间的常用参数
逃逸分析:栈上分配、标量替换、锁消除-XX:printFlagsInitial:查看所有的参数默认初始值
-XX:printFlagsFinal:查看所有参数的最终值(可能会存在修改的值,这里打印的是最终值)
-xms:堆空间的初始值大小,默认为内存的1/64
-xmx:对空间的最大大小,为内存的1/4
-xmn:设置新生代的大小
-XX:NewRatio:设置新生代和老年代的占比,默认为1:2,也就是值为2
-XX:SurvivorRatio:设置新生代Eden和s0/s1的占比,默认为是8:1:1,也就是值为8
-XX:MaxTenuringThreshold: 设置新生代垃圾的最大年龄数
-XX:+PrintGCDetails: 打印详细的gc处理日志
-XX:HandlePromotionFailure: 是否设置空间分配担保
-XX:-DoEscapeAnalysis : -为关闭栈上分配 +为打开
对象提升(Promotion)条件
TLAB是什么?解决什么问题?
逃逸分析是什么?通过它可以完成哪个优化?
GC日志阅读
MinorGC 触发机制, majorGC触发机制, full GC触发机制?



