Есть игра на JavaFX. И я уже несколько дней пытаюсь бороться с это проблемой. Все мои сильные ссылки на объекты я удалил и не могу их найти уже на протяжении 5 дней, так что думаю проблема не у меня. Кода очень много, поэтому буду вставлять по чуть-чуть.
Есть экземпляр объекта Game в главном классе.
public class Main extends Application {
private static Game game;
@Override
public void start(Stage mainStage) throws Exception{
mainStage = new Stage();
game = new Game();
mainStage.setScene(game.scene);//Ставлю сцену на Stage из game
mainStage.show();
}
}
В классе Game есть поля Scene scene
и Fighting fighting
и вот такие методы и конструктор
public Game(){
OnStart();/*Просто подгружает инфу из файлов и заполняет
статические коллекции всякими объектами с характеристиками(Оружие)*/
setMainMenu();
scene.setOnKeyPressed(event -> {
keys = event.getText();
if (event.getCode()==KeyCode.ENTER) setFighting();
if (event.getCode()==KeyCode.ESCAPE) setMainMenu();
});
}
public void setMainMenu(){
if (scene == null)
scene = new Scene(createMainMenu());
else
scene.setRoot(createMainMenu());
if(fighting != null) {
//Наверняка удаляю ссылку на объект Knight
//getFighting() возвращает Pane fighting
fighting.getFighting().getChildren().clear();
fighting = null;
System.gc();//Безуспешная попытка бороться с GC
}
}
public void setFighting() {
fighting = new Fighting();
//Этот метод возвращает Pane с картинкой(типо загрузка началась)
scene.setRoot(fighting.createLoading());
/*Этот костыль нужен, чтобы поток JavaFX отвлёкся от выполнения
этого метода и картинка, показывающая, что загрузка началась
появлялась на экране, иначе на экране остаётся главное меню и потом
сразу появляется боевая сцена*/
new Thread(() -> {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
Platform.runLater(() -> fighting.createPane());
}).start();
}
Класс Fighting. Вся игра проходит в Pane fighting
public class Fighting {
public ArrayList<Rectangle> collisions = new ArrayList<>();
Pane fighting = new Pane();
Pane loading = new Pane();
//Те самые объекты на которые где-то появляется ссылка и GC не может их удалить
public Knight player, enemy;
//С этим тоже самое, но не всегда
public FightingUI fightingUI;
private ImageView[] bg = new ImageView[3];
private ImageView[] bgShadow = new ImageView[3];
public Pane createLoading(){
loading.setPrefSize(1920, 1080);
loading.getChildren().add(bg[1]);
if (bgShadow[1] != null)
loading.getChildren().add(bgShadow[1]);
return loading;
}
public void createPane() {
fighting.setPrefSize(1920, 1080);
//Читает инфу из файла и returns экземпляр в методе read()
KnightReader kr = new KnightReader();
player = kr.read("resources/Save/player.sm", false);
enemy = kr.read("resources/Save/enemy00" /*+ league[0] +
league[1]*/ + ".sm", true);
collisions.add(player.collision);
collisions.add(enemy.collision);
//Интерфейс(UI)
fightingUI = new FightingUI(player, enemy);
//Просто ставит расположение "камеры"
updateLayout();
fighting.getChildren().addAll(enemy, player, fightingUI);
Main.getGame().scene.setRoot(fighting);
loading = null;
}
}
Knight extends Pane
. И в Knight создаётся около 15 объектов Animation, которые не удаляются из-за наличия ссылки на них в Knight. На каждую анимация есть как отдельная ссылка, так и ссылки в коллекции. Пробовал делать метод destroy() и обнулять все сслыки на Animation, несмотря на жизнь объекта Knight, но они и тогда не удаляются тоже.
Теперь Animation. Оно работает, по такому принципу, что загружаются картинки - элементы анимации через объекты AnimElement. Тобишь есть красный шлем и синий сапог, и их можно удобно комбинировать в зависимости от экипировки игрока.
!!!Все AnimElement удаляются из памяти, скорее всего с содержащимися картинками. В профайлере объектов AnimElement не видно и без них это всё ЖРЁТ 500 мб, а с ними 730 мб(если я не обнуляю коллекцию)!!!
Так же в AnimElement
есть static HashMap<String, AnimElement>
. Сейчас этот кэш не используется
public class Animation extends Pane{
ArrayList<AnimElement> allAnimElems = new ArrayList<>();
public ImageView allFrames = new ImageView();
boolean play = false, endAnim = false, puddle = false;
public int currFrame = 0;
private boolean alignmentRight, takingDamage = false;
public final int frameCount, delay;
double width, height;
public String pathToDir;
ArmorList[] armor_id;
WeaponList weapon_id;
BodyColor[] bodyColor;
String[] elemList = new String[]{"ArmB", "OversleevesB", "", "Body",
"Hair", "", "Eyes", "Eyebrows", "Mouth", "Ears", "Nose",
"BootsB", "PantsB", "BootsF", "PantsF", "Cuirass", "Helmet",
"PantsFC", "ArmF", "OversleevesF"};
//Only for taking damage animations
private AttackType attackType = null;
private int weaponId, life;
//Поток в котором анимация работает(Переставляет ViewPort на картинке)
Thread thread;
//Основной конструктор
public Animation(String pathToDir, double width, double height, int
frameCount, int delay, ArmorList[] armor_id, WeaponList weapon_id,
BodyColor[] bodyColor, boolean alignmentRight) {
this.frameCount = frameCount;
this.delay = delay;
this.pathToDir = pathToDir;
this.width = width;
this.height = height;
this.armor_id = armor_id;
this.weapon_id = weapon_id;
this.bodyColor = bodyColor;
this.alignmentRight = alignmentRight;
FillAllFrames();
this.setPrefSize(this.width, this.height);
}
//Специальный простой конструктор
public Animation(String pathToDir, double width, double height, int
frameCount, int delay, boolean alignmentRight,
String nameOfBlood) {
this.frameCount = frameCount;
this.delay = delay;
this.pathToDir = pathToDir;
this.width = width;
this.height = height;
this.alignmentRight = alignmentRight;
takingDamage = true;
AnimElement animElement;
if (AnimElement.allElems.containsKey(pathToDir + "/" + nameOfBlood))
animElement = new AnimElement(AnimElement.allElems.get(pathToDir + "/" + nameOfBlood), frameCount);
else
animElement = new AnimElement(pathToDir, nameOfBlood, false, frameCount, false);
allFrames = new ImageView(animElement.getImage());
animElement.destroy();
if (alignmentRight)
mirrorAnim();
allFrames = ImageTools.changeSizeBy2(allFrames.getImage());
this.width *= 2;
this.height *= 2;
setAlignment();
this.getChildren().add(allFrames);
this.setPrefSize(this.width, this.height);
}
public void play(){
play = true;
endAnim = false;
thread = new Thread(() -> {
while(play) Update();
});
thread.start();
}
public boolean isPlay() {
return play;
}
public void endAnim() {
currFrame = 0;
setAlignment();
if(takingDamage)
Platform.runLater(() -> Puddle.addPuddle(attackType, weaponId, 2));
endAnim = false;
play = false;
if(thread != null)
thread.interrupt();
thread = null;
}
public void endAnimRequest(){
endAnim = true;
}
public void FillAllFrames(){
//Создаёт экземпляры AnimElement и добавляет их в коллекцию allAnimElems
AddAllElems();
//Объединяет все элементы в одну картинку
allFrames.setImage(ImageTools.mergeImages(allAnimElems));
//Рабочее удаление AnimElement
allAnimElems.clear();
allAnimElems = null;
//Если надо отражает картинку
if (alignmentRight)
mirrorAnim();
//Увеличивает картинку в 2 раза
allFrames = ImageTools.changeSizeBy2(allFrames.getImage());
width *= 2;
height *= 2;
setAlignment();
this.getChildren().add(allFrames);
}
private void AddAllElems(){
setElemList();
for (int i = 0; i < elemList.length; i++) {
if (elemList[i].isEmpty())
continue;
if (checkFile("resources/" + pathToDir + "/" + elemList[i] +
".png")) {
if (AnimElement.allElems.containsKey(pathToDir + "/" +
elemList[i]))
allAnimElems.add(new
AnimElement(AnimElement.allElems.get(pathToDir + "/" + elemList[i]),
frameCount));
else
allAnimElems.add(new AnimElement(pathToDir,
elemList[i], false, frameCount, false));
}else {
if (AnimElement.allElems.containsKey("Animations/Knight/"
+ elemList[i]))
allAnimElems.add(new
AnimElement(AnimElement.allElems.get("Animations/Knight/" +
elemList[i]),
frameCount));
else
allAnimElems.add(new AnimElement("Animations/Knight",
elemList[i], true, frameCount,
true));
}
}
}
private void setElemList(){
//Меняет значение String elemList[]. Нужно для загрузки изображений
}
private boolean checkFile(String path) {
return new File(path).exists();
}
private void setAlignment(){
Platform.runLater(() -> {
if (alignmentRight)
allFrames.setViewport(new Rectangle2D(width * (frameCount
- 1), 0, width, height));
else
allFrames.setViewport(new Rectangle2D(0, 0, width,
height));
});
}
public void Update() {
if (endAnim) {
endAnim();
}
if (play) {
Platform.runLater(() -> {
if (alignmentRight)
allFrames.setViewport(new Rectangle2D(width *
(frameCount - (currFrame+1)), 0, width, height));
else
allFrames.setViewport(new Rectangle2D(width *
currFrame, 0, width, height));
});
currFrame++;
}
if (currFrame > frameCount - 1) {
endAnim();
}
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
endAnim();
}
}
public void mirrorAnim(){
allFrames = ImageTools.MirrorImageView(allFrames.getImage());
}
//Не помогает
public void destroy(){
allFrames = null;
}
}
И теперь класс AnimElement
public class AnimElement {
private String pathToDir, name;
private Image allFrames;
public ArmorItems armorItemNumber;
public WeaponList weaponList;
public BodyItems bodyItemNumber;
int frameCount;
boolean isStatic = false;
public boolean relocate = false, diffRelocate = false;
//Не используется
static Map<String, AnimElement> allElems = new WeakHashMap<>();
public boolean isStatic() {
return isStatic;
}
public int getFrameCount() {
return frameCount;
}
public AnimElement(String pathToDir, String name, boolean isStatic,
int frameCount, boolean relocate) {
this.isStatic = isStatic;
this.frameCount = frameCount;
this.pathToDir = pathToDir;
this.name = name;
this.relocate = relocate;
//Это чёрная магия и вам это по идее не нужно
checkRelocatableItems(name.substring(0, name.length()-1));
boolean attackDir = pathToDir.split("/")[1].equals("Attack") ||
pathToDir.split("/")[1].equals("Block");
boolean lowAttackDir = false;
if (pathToDir.split("/").length > 2)
lowAttackDir = pathToDir.split("/")[2].substring(0,
3).equals("Low");
if (attackDir && armorItemNumber != null && ((!lowAttackDir &&
armorItemNumber == ArmorItems.Cuirass) ||
armorItemNumber == ArmorItems.OversleevesF ||
armorItemNumber == ArmorItems.OversleevesB))
this.diffRelocate = true;
else if (attackDir && bodyItemNumber != null && (bodyItemNumber ==
BodyItems.ArmF ||
bodyItemNumber == BodyItems.ArmB))
this.diffRelocate = true;
allFrames = new Image(pathToDir + "/" + name + ".png");
//Game.fightingFastLoading == false. Этот кэш не используется
if (Game.fightingFastLoading)
allElems.put(pathToDir + "/" + name, this);
}
//Это конструктор как и кэш не используются
public AnimElement(AnimElement animElement, int frameCount) {
this.isStatic = animElement.isStatic;
this.frameCount = frameCount;
allFrames = animElement.allFrames;
this.pathToDir = animElement.pathToDir;
this.name = animElement.name;
this.relocate = animElement.relocate;
this.armorItemNumber = animElement.armorItemNumber;
this.weaponList = animElement.weaponList;
this.bodyItemNumber = animElement.bodyItemNumber;
}
private void checkRelocatableItems(String name){
try {
armorItemNumber = ArmorItems.valueOf(name);
}catch (IllegalArgumentException ex){}
try {
weaponList = WeaponList.valueOf(name);
}catch (IllegalArgumentException ex){}
try {
bodyItemNumber = BodyItems.valueOf(name);
}catch (IllegalArgumentException ex){}
}
public Image getImage(){
return allFrames;
}
}
Я думаю методы из ImageTools не нужны там просто Canvas, с которого потом берётся snapshot. И полей в классе нет, все методы static и все ссылки только локальные.
Теперь, что мне удалось раздобыть из профайлера. Если я захожу на боевую сцену и потом выхожу ничего не трогая и ни на что не наводя(На панели снизу на слотах, стоят onMouseEntered() и Exited()), то 97% памяти unrecheable from GC roots или не имеют сильных ссылок. Они чистятся сразу, несмотря на System.gc, но они чистятся из профайлера. Вот что в дампе. Можно увидеть, что и экземпляры Knight не имеют сильных ссылок
Но если я нажимаю просто на экран(по сути на Pane fighting)(на сцене нет setOnMouseClicked) или нажимаю или навожу на слот на нижней панели, то уже после выхода в главное меню 2% памяти не имеют сильных ссылок. Вот что есть в merged paths в профайлере
Ну и вот здесь вставлю все лямбда-слушатели
public Slot(){
this.setOnMouseEntered(event -> toggleActive());
this.setOnMouseExited(event -> toggleActive());
this.setPrefSize(128, 128);
imageActive.setVisible(false);
this.getChildren().addAll(imageDefault, imageActive);
}
public static Slot getActionSlot(int i) {
Slot slot = new Slot();
switch (i){
case 0:
slot.setOnMouseClicked(event ->
Main.getGame().fighting.getAttacker().attack(AttackType.HIGH));
break;
case 1:
slot.setOnMouseClicked(event ->
Main.getGame().fighting.getAttacker().attack(AttackType.MIDDLE));
break;
case 2:
slot.setOnMouseClicked(event ->
Main.getGame().fighting.getAttacker().attack(AttackType.LOW));
break;
case 3:
slot.setOnMouseClicked(event ->
Main.getGame().fighting.getAttacker().move(true));
break;
case 4:
slot.setOnMouseClicked(event ->
Main.getGame().fighting.getAttacker().move(false));
break;
case 5:
slot.setOnMouseClicked(event ->
Main.getGame().fighting.getAttacker().sleep());
break;
}
return slot;
}
private void toggleActive(){
if(imageDefault.isVisible()){
imageDefault.setVisible(false);
imageActive.setVisible(true);
}else {
imageDefault.setVisible(true);
imageActive.setVisible(false);
}
}
Rectangle eventHandler = new Rectangle(64, 64);
eventHandler.setOnMouseEntered((event -> {
description.setVisible(true);
}));
eventHandler.setOnMouseExited((event -> {
description.setVisible(false);
}));
eventHandler.setOnMouseEntered((event ->
description.setVisible(true)));
eventHandler.setOnMouseExited((event ->
description.setVisible(false)));
public class PotionSlot extends Slot{
ImageView potion;
Label numberLabel = new Label();
int number;
public PotionSlot(PotionList potionList, int lvl) {
number = getNumber(lvl, potionList);
if(number > 0)
potion = new ImageView("Interface/Potions/" +
potionList.toString() + "Lvl" + lvl + ".png");
else
potion = new ImageView("Interface/Potions/EmptyLvl" + lvl +
".png");
numberLabel.setFont(new Font(Game.font.getFamily(), 24));
numberLabel.setTextFill(Paint.valueOf(String.valueOf(Color.BLACK)));
numberLabel.relocate(90, 95);
numberLabel.setText(String.valueOf(number));
numberLabel.setAlignment(Pos.BOTTOM_RIGHT);
numberLabel.setPrefSize(30, 30);
this.setOnMouseClicked(event -> {
if(number > 0)
Main.getGame().fighting.player.usePotion(potionList, lvl);
});
this.getChildren().addAll(potion, numberLabel);
}
private int getNumber(int lvl, PotionList potionList){
if(lvl == 1)
return MoneyManager.potionsLvl1[potionList.ordinal()];
else if(lvl == 2)
return MoneyManager.potionsLvl2[potionList.ordinal()];
else
return MoneyManager.potionsLvl3[potionList.ordinal()];
}
public static PotionSlot getPotionSlot(PotionList potionList, int lvl)
{
return new PotionSlot(potionList, lvl);
}
}
public void exitPotionMenu(){
potions[0] = new PotionMenuSlot(PotionList.Heal);
potions[1] = new PotionMenuSlot(PotionList.Energy);
potions[2] = new PotionMenuSlot(PotionList.Concentration);
potions[3] = new PotionMenuSlot(PotionList.Strength);
setPotionSlots();
}
public void goToPotionMenu(PotionList potionList){
potions[0] = PotionSlot.getPotionSlot(potionList, 1);
potions[1] = PotionSlot.getPotionSlot(potionList, 2);
potions[2] = PotionSlot.getPotionSlot(potionList, 3);
potions[3] = new ExitPotionMenuButton();
potions[3].setOnMouseClicked(event -> exitPotionMenu());
setPotionSlots();
}
private void setPotionSlots(){
int count = 0;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
i++;j++;
potions[count].setTranslateX(807 + (j*4) + (j*128 - 128));
potions[count].setTranslateY(4 + (i*4) + (i*128 - 128));
i--;j--;
count++;
}
}
for (int i = 0; i < 4; i++) {
this.getChildren().set(potionIndexes[i], potions[i]);
}
}
public class PotionMenuSlot extends Slot{
ImageView potion;
public PotionMenuSlot(PotionList potionList) {
int number = 0;
number += MoneyManager.potionsLvl1[potionList.ordinal()];
number += MoneyManager.potionsLvl2[potionList.ordinal()];
number += MoneyManager.potionsLvl3[potionList.ordinal()];
if(number > 0)
potion = new ImageView("Interface/Potions/" +
potionList.toString() + "Lvl1.png");
else
potion = new ImageView("Interface/Potions/EmptyLvl1.png");
this.setOnMouseClicked(event -> Main.getGame().fighting.fightingUI.bottomPanel.goToPotionMenu(potionList));
this.getChildren().add(potion);
}
}
public void goToPotionMenu(PotionList potionList){
potions[0] = PotionSlot.getPotionSlot(potionList, 1);
potions[1] = PotionSlot.getPotionSlot(potionList, 2);
potions[2] = PotionSlot.getPotionSlot(potionList, 3);
potions[3] = new ExitPotionMenuButton();
potions[3].setOnMouseClicked(event -> exitPotionMenu());
setPotionSlots();
}
private void setPotionSlots(){
int count = 0;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
i++;j++;
potions[count].setTranslateX(807 + (j*4) + (j*128 - 128));
potions[count].setTranslateY(4 + (i*4) + (i*128 - 128));
i--;j--;
count++;
}
}
for (int i = 0; i < 4; i++) {
this.getChildren().set(potionIndexes[i], potions[i]);
}
}
Помогите пожалуйста, я уже не знаю, что делать
Виртуальный выделенный сервер (VDS) становится отличным выбором
Как найти самое длинное и самое короткое предложение в строке s без использования split(), ArrayList и так далееВ данном коде я уже нашел количество...
Использую community версию JasperReportsВнутри отчета конструкция $X{IN,db_field,parameter}